What's on a Box

What a box holds, how it installs lnk, how its working users and its people are kept apart, and how a spec and a setup reach it (Security).

What's on a box

What says a machine is a box, and what it holds once it's made.

A box's ID and name

They're in the box's ~/.lnk/machine.toml, written by lnk box create over SSH before lnk first runs there, never in the first boot's metadata, and in its tags. A box made before IDs gets its ID from that file, or a new one written there, the next time Link reaches it.

That it's a box

/etc/lnk/kind.toml, root's, written at its first boot (cloud-init, as root) by lnk box create, says it's a box, and its OS and processor. Written by anyone but root, it's refused, so no one but root can make a machine a box; without it, a machine is a computer.

Its own lnk login

A key pair it made itself, whose public half the computer that made it added to the account (lnk auth key there, lnk iam machine add here, then lnk auth login --granted there with the answer, which holds no credential); or, where it can't, from lnk auth login there, which you approve. Its private half never leaves the box. lnk iam machine list lists it, signed in by that computer.

What else it holds

Whatever you run on it. The first boot (cloud-init) holds nothing secret, since a cloud's metadata is readable from the machine. It turns on lingering for ubuntu, and installs nothing.

The install on a box

A box installs lnk from the releases this machine's lnk uses: Link's own, unless LNK_RELEASES_URL names others, at LNK_VERSION if set (a box spec's [lnk] version wins). Whichever they are, they're trusted only as this machine's lnk trusts them, by the keys it was built with.

Checked here first

Before the cloud is asked for a box, lnk box create fetches the release's install.sh, VERSION, SHA256SUMS and SHA256SUMS.sig here, and goes on only when the sums are signed by a key this lnk trusts (a release's trusts the release key alone), the script's checksum is the signed one, and the release is one lnk upgrade would install.

Which release

The version asked for (a spec file's or LNK_VERSION's, neither behind a released lnk doing the asking, or one typed with --lnk-version, which may be behind on purpose), or the latest, which must say its version and be at or above that lnk.

Its signed version

A release whose signed VERSION is missing, or says another version than the one asked for, is refused here, and what it installs is checked again on the box.

The script on the box

The box runs that script as it was checked, copied into its setup: it fetches no script of its own, from a relay or anywhere. It runs it at the version checked, the latest's too, so the box installs that version or nothing, whatever the latest is by then.

Checked again there

The script checks the release's signed VERSION, and the version the lnk inside reports, against it; then the setup checks again that the lnk installed says it's that version, and removes it and fails if not. So an old signed release put under a newer version's tag installs nothing.

What it adds

That lnk checks its archives and plugins with the keys the script holds, which were signed with it, and adds the accounts, secret and sandbox plugins, the Linux keeper and the worker launcher.

LNK_RELEASES_URL

When it's set, every lnk that lnk box run runs there follows it, so a move's upgrade installs what this machine did, checked by the box's own lnk. A debug build also takes http:// to this machine.

What it may be

It's https:// with no login (user@), \, query or fragment, a bucket of yours (s3://, gs://) or a folder here (file://); anything else fails. The box can't read the last two, so lnk box create copies it the checked release over SSH (Where releases are read from).

Before an agent comes

Before lnk agent move or lnk agent create --on, the harness plugin adds itself and the sandbox there (with the bucket plugin for an agent's backups), and lnk sandbox ready --yes installs bubblewrap with apt if it's missing and adds the AppArmor profile letting it make user namespaces, as lnk agent start offers on Ubuntu, through the box's sudo, which asks nothing.

Working users

An ordinary box keeps trusted controllers and proxies under its owner, while each sandboxed agent runs as a fresh locked Linux user. A box never runs an unsandboxed agent.

A fresh user

Its groups are fresh too: the controller's sudo membership is never inherited. The first program inside is root's installed sandbox helper; it drops supplementary, real, effective and saved IDs, clears all capabilities and sets no_new_privs before confinement and workload code. A worker cannot use sudo or read the keeper's or controller's private files outside its grants.

Its view of the state

The launcher makes a private mount namespace and clones the granted state with kernel identity mappings. Controller-owned inodes appear as the worker's there; new 0600 files remain controller-owned underneath. Replacement, backup and moves can therefore read private state without recursive ownership changes or lasting ACL grants.

What it can't see

Read-only grants stay read-only, and nested mounts keep covered data hidden. The worker's empty home is a private tmpfs. The installed helper is bound read-only; its executable does not need a mapped filesystem.

What the kernel needs

The kernel must support openat2, pidfds and recursive idmapped mounts (Linux 5.12 or newer, with filesystem support); the worker requires seccomp on x86_64 or arm64. Unsupported grants fail before any workload runs. The launcher uses the kernel mount API, without depending on a newer version of the distribution's mount command.

Its numbers

Root journals setup before account mutations in /var/lib/lnk/workers. Its durable next-id counter reserves UID/GIDs above automatic account and subordinate-ID ranges, skips existing delegated ranges, and never reuses a retired identity. Subordinate allocation must use files, not an external NSS provider. Missing, malformed or exhausted state refuses new work.

Keeping the range

On an existing or custom box that range must never have been delegated to an untrusted user namespace. Preserve the counter and range across upgrades; never reset it or delegate retired IDs while older file capabilities could survive.

Old descriptors

A replacement cannot gain fresh path access through an old worker's retained idmapped directory descriptor. Already opened file capabilities (including directory listings) follow Unix semantics; a run may copy or share data and plain secrets explicitly granted to it.

Cleanup

The supervisor pins both its launch front and controller with pidfds across sudo. Normal completion, signals and either process's death kill and reap every worker process before account removal. Cleanup uses pinned process handles, validates journalled UID/GIDs and detaches mounts before removing only their empty mount points.

Cleanup it can't prove

If process cleanup cannot be proved, the account stays. Later failures retain the remaining journal or runtime cleanup for the next launch and boot's lnk-workers-reap service to retry. Sandboxed programs cannot create setuid or setgid artifacts in state; openat2 receives ENOSYS so callers use the inspected openat.

Its setup

Setup installs /etc/lnk/workers.json and root's /usr/local/lib/lnk/lnk-sandbox. After upgrading the sandbox plugin, refresh it with lnk sandbox workers --install as the box controller. A sudo command default preserves the launcher's explicit descriptors and PATH; it grants workers no sudo authorization.

Other users

Named lnk box user users retain their existing separate, non-sudo identities, validated against root's user record. When their writable folder contains their normal settings, session control files live in a private temporary directory outside it and are removed on normal exit. Packed machines retain their slot-user boundary and declare that mode in root's worker configuration.

A box spec

A spec may be someone else's, so it's checked when it's read, before anything is asked of a cloud. A spec has no paths, no scripts and no account: nothing runs from it.

Every value is typed

[lnk] version is a version (0.14.0, or a prerelease as 0.14.0-qa.202610131900), 0.11.0 or later; a region and a machine type are lowercase letters, digits, dashes and dots; the OS, a GPU model and a disk type are words from Link's lists; name has a box name's characters.

[lnk] version

From a file it can't be behind this computer's lnk ([lnk]). It reaches the box's setup script quoted, as install.sh's LNK_VERSION.

Your tags

They keep to the tag rules: at most 40, so Link's own always fit (AWS takes 50), and none named as Link's or the cloud's. On Google Cloud they're labels, never network tags, which the firewall's rules match.

To the cloud's CLI

Values reach the cloud's CLI as separate arguments, and its JSON (--block-device-mappings, --tag-specifications) is built by a serializer.

Asked first

lnk box create says the machine, its disk, the lnk it gets and the price when the cloud gives one, then asks; --yes takes it unasked. A pinned size has no max to keep it small, and a fast disk is billed for its speed too.

What lnk box spec prints

lnk box spec <box> is read from this computer's boxes.toml and what the cloud says of the box, never from the box, where whatever runs could write the spec your next boxes are made from. It's printed by the toml serializer, so a name from a cloud can't add a line.

Setting a box up

What lnk box copy writes to a box, and what your script and its stages can reach.

lnk box copy

It writes files to a box over its SSH, with the key, pinned host keys and options of Security, never through the relay. The folder's name reaches the box's shell quoted, so nothing in it runs; ~/ is the home folder there.

It refuses a link rather than follow it, the file itself or any folder on the way to it in your home, so a link planted where you copy from can't send what it points to (an SSH key) to the box. It opens each folder from the one before without following a link, so a folder swapped for a link after the first check is refused too.

A file with another name (a hard link), which could be one anywhere on the disk, is refused as well: copy it to a new file first.

How it's written

Each file is written beside its place, its size checked, and moved in once whole, with its permissions, in a folder made only for ubuntu (umask 077) when it's missing. A file whose size changed after it was opened is refused.

A setup script

It runs on this computer, as you, with everything you can do: Link runs none of it. A script someone else wrote: read it first.

Stages

Stages are what your script makes them. Each in its own sandbox spec (lnk sandbox run --spec on the box) reaches only what its spec opens, so a stage installing dependencies can't reach your repository's host, and one building reaches no network (sandbox). A stage outside any sandbox, as installing system packages with sudo must be, runs as root there.

A box's users

A box's user (lnk box user add) is a Linux user of their own, made by root with their own home, the one folder their sandbox writes, in the group lnk-users. Their login shell is /bin/sh, which reads no file of theirs before the command sshd runs.

Root's record of them

Root keeps each in /etc/lnk/users/<name>/ (755, its files 644, root's): their folder and key, their sandbox's spec with its paths absolute, and authorized_keys, whose one line is restrict,pty,command="/usr/local/lib/lnk/lnk-sandbox sandbox enter --spec <spec> --folder <home>" <key> lnk-user-<name>.

Their keys, root's file

sshd reads that file for the group (/etc/ssh/sshd_config.d/60-lnk-users.conf), never their home's .ssh, which their sandbox writes: nothing they run lets in another key or one without the sandbox. The drop-in turns off forwarding, ~/.ssh/rc and ~/.ssh/environment for them too, as restrict does on the line. A file there that anyone but root could write is refused.

Their sessions

A shell, a command, scp or sftp all run in their sandbox (Sessions over SSH). The programs their sessions run are root's copies of lnk and lnk-sandbox in /usr/local/lib/lnk, since the box's own user's home is closed to the rest; each add brings them up to the lnk that ran it.

Adding and removing them

Their sandbox is shown and asked about before they're added (--yes skips it), read as their session reads it. A name that's already a Linux user lnk box user add didn't make is refused, as is root: it never takes over a user. lnk box user remove takes their key away first, then their user; their home stays unless --delete-folder.