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 startstops before making anything whensshorssh-keygenis 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 bygcloud auth application-default loginand 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 disconnectdeletes it. - Every other plugin acts as an account through its adapter only, with
the account
lnk cloud envgives:lnk bucketasks the adapter'sstoragefor 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, aslnk bucketkeeps 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_*andCLOUDSDK_*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 listnever includes keys;lnk cloud envdoes.
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.cominto~/.config/lnk/aws. On Linux, its zip andinstallscript; on macOS, its package, for the current user only. - Google's archive from
dl.google.cominto~/.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-boxand Google Cloud firewall rulelnk-box-sshallow 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 addslnk-box-denythere, 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 listasks 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-keysmetadata, never replacing the others. A box takes only its own keys: it's made withblock-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 boxappends it toauthorized_keysover 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_hostsbefore the first SSH connection, and kept inboxes.toml. They're kept under the box's own name,lnk-<cloud>-<its id>(ssh'sHostKeyAlias), 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-newaccepts 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-keyson Google Cloud; on AWS, the ed25519 key in alnk-host-keytag, 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 runcalls 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, andPermitLocalCommand=no, so noLocalCommandruns here.ClearAllForwardings=yes, on every connection butlnk box forward's, so noLocalForward,RemoteForwardorDynamicForwardof yours opens.lnk box forwardexists to forward one port or socket, and ssh can't clear the file's without clearing it too: a forward of yours underHost *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
lnklogin to the relay: a token of its own, fromlnk auth loginthere, 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] versionismajor.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;namehas a box name's characters. [lnk] versionfrom a file can't be older than this computer'slnk([lnk]). It reaches the box's setup script quoted, asinstall.sh'sLNK_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 startsays the machine, its disk, thelnkit gets and the price when the cloud gives one, then asks;--yestakes it unasked. A pinned size has nomaxto 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 copywrites 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 forubuntu(umask 077) when it's missing.lnk box setupruns your script on this computer, as you, withsh -e, the wayshwould run it: Link adds onlyLNK_BOXand readssh'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 ashthat doesn't number lines (Ubuntu'sdash), 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 --specon 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 withsudomust 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
lnklogin:lnk box image savesigns the box'slnkout first (lnk auth logoutthere), asks it again, and refuses to save a box still signed in. The old token's file is deleted, andfstrim -a(withsudo) 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
lnkprints: an SSH error is never taken for "none". - No secrets where they're usually left: before it changes anything,
saveruns a script over SSH that looks inubuntu's home folder, and refuses the box, listing what it found:~/.git-credentials,~/.config/git/credentials, a~/.netrcwith a password, the GitHub CLI's login, an SSH private key (~/.ssh/id_*but.pub), a token in an.npmrc,.yarnrc.ymlor.pypircin the home folder or up to three folders down (but innode_modules,.gitand.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
--hashis taggedlnk-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-hashtakes the newest ready image of yours with that tag, as--imagetakes 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
lnkis set up there, the saved box's machine ID is deleted, and the box gets an ID of its own. - The image's
lnkand keys: a box from an image keeps thelnkon 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 upgradethere 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, wherelnk box startreads 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>andlnk-owner=github-<id>(on AWS its snapshot too).lnk box image listlists only yours. A box is made only from the account's own image (--owners selfon AWS, the project's own on Google Cloud), never a public one by the same name.lnk box image removedeletes 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 uninstalldeletes 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 specprints of a box. --yesmakes 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.
