Security

What's protected: your cloud accounts, whose keys lnk cloud keeps, and the machines lnk box makes in them, against anyone but you reaching them. Neither plugin talks to the relay, and Local Link holds no cloud account: everything runs in, and is billed to, yours. What they do is clouds and boxes.

Limits

  • AWS and Google Cloud only, in your own accounts and bills.
  • Boxes are Ubuntu 24.04 or 22.04 LTS on x86_64 or arm64; an arm64 box has no GPUs.
  • Boxes need the cloud's CLI (aws, gcloud) and OpenSSH on your computer. lnk box start stops before making anything when ssh or ssh-keygen is missing.

Cloud accounts

Keys

  • A reused aws login (~/.aws) is named, not copied: the AWS CLI, and rclone for buckets, read it when they run.
  • A Google Cloud account is one credential file, kept in ~/.config/lnk/gcp/credentials/<name>.json, mode 600 in a 700 folder: a service account's key (--key-file), or your login's application default credentials, made once by gcloud auth application-default login and copied there. gcloud (with a service account's key), rclone and Google's libraries all act as that file, so boxes and buckets are the same identity. It holds a refresh token or a private key: anyone who reads it acts as the account. lnk cloud disconnect deletes it.
  • Every other plugin acts as an account through its adapter only, with the account lnk cloud env gives: lnk bucket asks the adapter's storage for its sign-in on each command, and keeps none of it.
  • An access key given instead is kept in ~/.config/lnk/cloud/accounts.toml, mode 600 in a 700 folder, as lnk bucket keeps its own. On a Mac its secret part is in the Keychain instead (Secrets in the Keychain).
  • A key reaches the CLI only in its environment, never on a command line, where other local users could see it.
  • The shell's own AWS_* and CLOUDSDK_* variables are removed first, so a profile set in the shell can't redirect a call to another account.
  • Adapters print an account on stdout to the plugin that ran them. lnk cloud list never includes keys; lnk cloud env does.

Adapters

lnk-aws and lnk-gcp run with your keys and the cloud's CLI. They're plugins like any other: installed only from Link's signed release, found next to lnk, else on the PATH.

The cloud's CLI

Link uses the CLI on your PATH, else one lnk cloud connect installed when you said yes:

  • AWS's installer from awscli.amazonaws.com into ~/.config/lnk/aws. On Linux, its zip and install script; on macOS, its package, for the current user only.
  • Google's archive from dl.google.com into ~/.config/lnk/gcp.

Both come over HTTPS only. Link doesn't pin them or check their signatures: it trusts those sites, as their own install instructions do. Neither needs administrator rights, and neither changes your shell's settings.

Reaching a box

What a box exposes

  • Only SSH, port 22, open to the whole internet. It's key-only: Ubuntu's cloud images refuse passwords.
  • The key is Link's own: ~/.config/lnk/box/id_ed25519, ed25519, no passphrase, in a 700 folder. Whoever reads it reaches every box.
  • Nothing else is open. AWS security group lnk-box and Google Cloud firewall rule lnk-box-ssh allow only TCP 22. Link makes them the first time and removes them with the last box in their region or project. Boxes go in the account's default VPC on AWS, and its default network on Google Cloud. That network's own rules let in every port from inside it, RDP and ping, so Link adds lnk-box-deny there, refusing everything else to its boxes ahead of those rules. A rule you add yourself at a normal priority still wins.
  • AWS instances take instance metadata only with a session token (IMDSv2), and have no instance role. Google Cloud instances have no service account (--no-service-account --no-scopes). So the metadata service, which any program on the box can reach, gives no cloud credentials: Link acts on your cloud as your account, from your computer, never as the box.
  • Every agent made on a box goes through Link's proxy (lnk agent route direct --default), never straight from the box or to the cloud's metadata service.

Which boxes are yours

  • Each box's machine is named lnk-<machine id> and tagged with its name (lnk-box), its machine ID (lnk-machine) and your GitHub user id (lnk-owner=github-<id>), which is public. On AWS its disk has the same tags, so the bill says whose each disk is.
  • lnk box list asks each connected account for the boxes with your tag, so a project shared with another Local Link user lists only yours.
  • The tags are as trusted as the account: anyone who can tag instances there could make a machine look like yours. So lnk agent move <account> asks before moving to a box it found by its tags, and takes one unasked only if this computer made it (agents).
  • What a listing brings back goes into file names and prompts only as a valid name and ID; anything else is skipped.

Getting in from another computer

A box made on another of your computers is reached the first time by pinning its host keys from the cloud (below). Then, if SSH doesn't let this computer in, its key is added through the account:

  • On Google Cloud, merged into the instance's ssh-keys metadata, never replacing the others. A box takes only its own keys: it's made with block-project-ssh-keys=TRUE, so the project's SSH keys, which anyone who can edit the project's metadata adds, don't let anyone in, and with OS Login off. A box made before this takes them until it's made again.
  • On AWS, pushed with EC2 Instance Connect, which lets it in for 60 seconds, during which lnk box appends it to authorized_keys over SSH.

Each computer's key has the comment lnk <machine id> <machine name>, so a lost computer's key can be found and removed. Whoever controls the account could add a key this way too: the account is trusted, as it could replace the machine anyway.

Host keys

  • A new box's SSH host keys are read from its console output, the block cloud-init prints at the first boot, through the cloud's API as your account.
  • They're pinned in Link's own known_hosts before the first SSH connection, and kept in boxes.toml. They're kept under the box's own name, lnk-<cloud>-<its id> (ssh's HostKeyAlias), never its address, so a box at a new address, restarted here, from the cloud's console or by another computer, is checked against the same keys. SSH then refuses any other key: StrictHostKeyChecking=accept-new accepts only hosts it doesn't know.
  • When the console shows none within 5 minutes, or the adapter can't read it, the key is accepted on first contact and pinned after, under the box's name too. That trusts the network between you and the cloud once.
  • A removed box's keys are forgotten, since its address can come back as another machine's.
  • The computer that pinned them keeps them in the cloud, since the console's first-boot output doesn't last: metadata lnk-host-keys on Google Cloud; on AWS, the ed25519 key in a lnk-host-key tag, which holds 256 characters. Another computer pins those before its first connection, and reads the console only when the cloud keeps none.
  • Anyone who controls the cloud account can change the host keys kept there.

One connection per box

  • The lnk box run calls a command makes to one box share one SSH connection, which the first opens with the key and host keys above. It closes a minute after the last one ends, and when the box is removed or a new box gets its address.
  • Its socket is in ~/.config/lnk/box/mux, a 700 folder. While it's open, any program running as you can run commands on the box through it without the key, as it could with the key, which it can read too.
  • When that folder's path is too long for a socket (over 42 characters), each call connects on its own.

Your own SSH config

Every connection to a box runs your ssh, which reads your ~/.ssh/config, so a ProxyJump you need still works. What would hand the box something of this computer is turned off on the command line, which wins over that file:

  • ForwardAgent=no: a box never gets your SSH agent, which would let whoever controls it sign as you to any host your keys reach.
  • ForwardX11=no, and PermitLocalCommand=no, so no LocalCommand runs here.
  • ClearAllForwardings=yes, on every connection but lnk box forward's, so no LocalForward, RemoteForward or DynamicForward of yours opens. lnk box forward exists to forward one port or socket, and ssh can't clear the file's without clearing it too: a forward of yours under Host * opens on that connection as well.

What's on a box

A box's ID and name

They're in the box's ~/.config/lnk/machine.toml, written by lnk box start 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.

What a box holds

  • Its own lnk login to the relay: a token of its own, from lnk auth login there, which you approve.
  • 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

install.sh from the relay you use (else local.link), over HTTPS, as on your own machine, with the accounts plugin alone: a relay on plain http:// is refused. Its plugins come from Link's signed release. Before an agent comes (lnk agent move, lnk agent new --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.

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:

  • Every value is typed: [lnk] version is major.minor.patch, 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 can't be older than this computer's lnk ([lnk]). It reaches the box's setup script quoted, as install.sh's LNK_VERSION.
  • Your tags 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.
  • A spec has no paths, no scripts and no account: nothing runs from it.
  • Values reach the cloud's CLI as separate arguments, and its JSON (--block-device-mappings, --tag-specifications) is built by a serializer.
  • lnk box start 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.

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

  • lnk box copy writes files to a box over its SSH, with the key, pinned host keys and options above, never through the relay. 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 hard link is a file, and is copied. The folder's name reaches the box's shell quoted, so nothing in it runs; ~/ is the home folder there. Each file is written beside its place and moved in once whole, with its permissions, in a folder made only for ubuntu (umask 077) when it's missing.
  • lnk box setup runs your script on this computer, as you, with sh -e, the way sh would run it: Link adds only LNK_BOX and reads sh's trace (-x) to say which stage failed. The trace, expanded commands and all, is never shown or kept: a failed stage is said by its line in your script, as written, or, under a sh that doesn't number lines (Ubuntu's dash), by its number. A script someone else wrote runs with everything you can do: read it first.
  • 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.

Images

A box's image

Each OS is an image from a fixed table, found by its publisher, never by searching image names, which anyone can publish under: Canonical's SSM parameters on AWS and its ubuntu-os-cloud project on Google Cloud. A box with GPUs gets NVIDIA's driver from AWS's Deep Learning Base AMI (AWS's own SSM parameter) or Canonical's ubuntu-os-accelerator-images project.

Images you save

lnk box image save keeps a box's disk as an image in its cloud account, for lnk box start --image.

  • What it holds: everything on the box's disk, but its lnk login: lnk box image save signs the box's lnk out first (lnk auth logout there), asks it again, and refuses to save a box still signed in. The old token's file is deleted, and fstrim -a (with sudo) drops the disk's freed blocks before it's saved, where the disk takes it; on one that doesn't, the old file's blocks can stay in the image until written over. Anything else you put on the box is in the image, and in every box made from it.
  • Fails closed: every check on the box (its agents, its secrets, its login) refuses the save when the box's answer fails or isn't what lnk prints: an SSH error is never taken for "none".
  • No secrets where they're usually left: before it changes anything, save runs a script over SSH that looks in ubuntu's home folder, and refuses the box, listing what it found: ~/.git-credentials, ~/.config/git/credentials, a ~/.netrc with a password, the GitHub CLI's login, an SSH private key (~/.ssh/id_* but .pub), a token in an .npmrc, .yarnrc.yml or .pypirc in the home folder or up to three folders down (but in node_modules, .git and .venv; one written as ${VARIABLE} is read from the environment, and isn't one), and the AWS, gcloud, Azure, Docker and Kubernetes logins. It looks for Link's own keys too: lnk cloud's accounts (~/.config/lnk/cloud/accounts.toml) and Google Cloud logins (~/.config/lnk/gcp/credentials), lnk bucket's keys and encryption passwords (~/.config/lnk/bucket/rclone.conf), lnk box's SSH key, an agent's folder in ~/Link/Agents, and ~/Link/Agents/.moved, where an agent moved off the box leaves its files and backup for 30 days. It removes nothing: the box is yours, and still in use. A secret anywhere else, under another name, or in another user's home (root's) isn't looked for.
  • Hashes: an image saved with --hash is tagged lnk-hash=<hash> too. The hash is yours, made from what you choose; Link checks only that it's a tag's lowercase letters, digits and dashes. lnk box start --image-hash takes the newest ready image of yours with that tag, as --image takes one by name: the tag is as trusted as the account, where anyone who can tag images could make one answer a hash.
  • No agents: a box with agents on it is refused, since each box from the image would run them as themselves, with their keys and channels.
  • Its own identity: a box from an image signs in as any new box, with a token of its own you approve. Before lnk is set up there, the saved box's machine ID is deleted, and the box gets an ID of its own.
  • The image's lnk and keys: a box from an image keeps the lnk on the image, at its version: a spec's [lnk] version, and the rule that it's no older than this computer's, aren't applied to it (lnk upgrade there updates it). It also keeps the image's ~/.ssh/authorized_keys: a computer's key removed from boxes after the image was saved still opens every box made from it. Save the image again after removing a key.
  • Its own host keys: Ubuntu's cloud-init deletes an image's SSH host keys and makes new ones at the first boot of each new machine (ssh_deletekeys, run once per instance), and prints them on its console, where lnk box start reads them as for any box. They're pinned under the new box's own name (lnk-<cloud>-<its id>), so the saved box's keys are never trusted for it. Nothing Link keeps on the saved box's instance (its host keys, its tags) is in the image.
  • Consistent: the box is stopped while its disk is saved, so no file is saved half-written, and started again after.
  • Yours only: an image is tagged lnk-image=<name> and lnk-owner=github-<id> (on AWS its snapshot too). lnk box image list lists only yours. A box is made only from the account's own image (--owners self on AWS, the project's own on Google Cloud), never a public one by the same name. lnk box image remove deletes an image only when it's tagged as Link's for you.
  • Cost: the question before saving says the image is billed every month by its size. lnk uninstall deletes no image: it says they stay in your accounts.

Spot boxes

A spot box the cloud takes back is stopped, never deleted: its disk, with its lnk login and an agent's state, stays, and is billed, without you stopping it. What a spot box does on each cloud: box spec.

Removing

lnk box remove deletes the machine and its disk: on AWS it's terminated, its volume deleted with it; on Google Cloud it's deleted with --delete-disks all. Snapshots or anything else you made there yourself stay. Images you saved stay until lnk box image remove. A lnk box start whose machine couldn't be made deletes what Link made for boxes the same way.

What lnk cloud disconnect keeps: Disconnect an account.

Gaps

  • Keys are in a file.
  • One SSH key, without a passphrase, reaches every box.
  • SSH is open to the internet.
  • The first contact's host key is trusted when neither the cloud nor a box's console has it.
  • The cloud account is trusted for tags, keys and host keys, and for what lnk box spec prints of a box.
  • --yes makes a box from a spec without asking, however new the spec. What it makes is printed first.
  • An image holds whatever else is on the box's disk: Link checks for its own login, agents and the usual places secrets are left in the home folder, not for a secret anywhere else.
  • Two images of one name in a Google Cloud project shared with another Local Link user collide: the second save is refused.

See Known limitations.