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.
Links it refuses
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.
Hard links
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.