Security
What's protected: what runs as lnk and Link's plugins is what Link
released, a plugin from outside Link is what its maker released under
the key you trusted, and relay servers run only signed builds.
Which program runs
lnk <group> runs lnk-<plugin> from lnk's own folder first, then
from the PATH, the way git does. So whoever can write to a folder on
the user's PATH can already replace what lnk tunnel open runs. Empty
and relative PATH entries, such as the current folder, are skipped:
lnk drops them from the PATH it runs with and hands its plugins, so
neither runs a tar or systemctl from the folder it was started in.
What installs plugins
lnk up,lnk plugin add, a missing plugin's install offer andlnk upgradeinstall Link's plugins only from a release whoseSHA256SUMSis signed by the keys built intolnk, and check each archive against it.lnk plugin addtakes the release oflnk's own version, so plugins and core match.- The install offer asks first, and only in a terminal.
install.shinstallslnkalone.- Link's plugins come only from Link's own releases. Prebuilt programs exist for macOS (Apple Silicon, Intel) and Linux (x86_64, arm64) only; other platforms build from source.
Plugins from outside Link
lnk plugin add <url>takes a GitHub repository's latest release (https://github.com/<owner>/<repo>only), itsSHA256SUMSsigned with that repository's own key in thelink-releasenamespace, and each archive checked against it. Link's keys never vouch for it.- The first
addtrusts the key the release itself offers (release-key.pub), after showing its fingerprint and asking, or--yes. Whoever controls the repository on that first day is trusted: compare the fingerprint with one its maker publishes elsewhere. lnkpins the repository and the key in~/.config/lnk/plugins.toml(600). Every lateraddandlnk upgradeof it verifies against the pinned key alone: a release signed by another key is refused, and a key is only replaced byremove, thenaddagain.- It must answer the harness contract's version before it's installed, so only a harness adapter is added this way, and its name can't be one of Link's plugins'.
- An outside plugin runs as you, outside any sandbox, like any program
you install: the harness plugin asks it what to run, and
lnkasks its version. Link's sandbox confines the harness it installs and runs, not the adapter's own answers. - On a box,
lnk agent moveadds it with--key, the key this computer pinned, so the box trusts nothing the release offers. lnk upgrade, andaddagain (as a box does on every move), take its latest release and install it only when it is newer than the one installed; one whose version, or the installed one's, can't be compared is refused. A release whose signedVERSIONfile says it is no newer is neither downloaded nor run.- A
SHA256SUMSline naming anything but letters, digits,.,_and-is dropped, so the names a release not yet trusted lists reach the terminal only as plain text.
Uninstall
lnk uninstalldeletes only inside~/.config/lnkand~/Link. A path a plugin names outside them, with..or relative, is left out. A path whose folder leads out of them through a link is left out too.- Before asking, it lists every path each picked part deletes.
- Before asking, it runs only Link's own plugins. Another program named
lnk-*inlnk's folder is listed, then told to uninstall and removed once you say yes. It's never asked to delete a part. lnk plugin removekeeps a plugin that fails to stop what it runs, and shows what the plugin said, so its services never outlive the program that stops them.
Releases and updates
Signing
Every release's SHA256SUMS is signed with the release key
(ssh-keygen -Y sign, namespace link-release). The public key is in
src/shared/release-signers. It is built into lnk and the relay,
embedded in install.sh and the relay's bootstrap.sh, which installs
it on servers at /etc/link/release-signers; a test checks the copies
match. No release carries the key.
Who checks
The relay updater, the relay's bootstrap.sh, lnk upgrade and
install.sh install nothing without a valid signature, so someone with
the releases repo or its token can't push code.
lnk upgradeuses its own SSHSIG verifier,link_plugin::release.lnk upgrade --check, whichlnk upandlnk agent statusstart in the background at most once a day, reads the latest release'sVERSIONonly through its signedSHA256SUMS, so a notice can't name a version Link didn't release. It asks the releases host for three small files and sends nothing butlnk's version, as its user agent;LNK_UPDATE_NOTICE=offturns it off.install.shusesssh-keygen, and refuses to run without it unlessLNK_INSECURE=1.
The installer
curl https://local.link/install.sh | sh gets the script from the
relay's own signed binary, not from the releases repo, so the releases
repo can't swap it.
Downgrades
lnk upgraderefuses a release older than itself unless that version is requested, and refuses a version it can't parse.lnk upgrade <version>installs only that version: thelnkit downloads must report it, so an older signed release moved under the tag is refused, with or without aVERSIONfile.- Releases carry a
VERSIONfile inside the signedSHA256SUMS.lnk upgradechecks it against the version asked for and thelnkit holds;lnk plugin addchecks it against its own version. install.shchecks theVERSIONfile againstLNK_VERSION, and thelnkit installs must report that version. It installslatestonly from a release with aVERSIONfile, and, when the relay served the script, never one older than the relay (the relay fills in its own version). Right after a release, the relay can be up before its release is: the script then says to try again in a few minutes.lnk plugin addalso runs each plugin's--version, which must match, and must answer when a release has noVERSIONfile.- The relay updater,
link-relay-update, refuses a relay whose--version(inside the signed binary) is older than the installed one's, or that it can't read, and remembers it so later runs skip it. It also refuses one older than the floor bootstrap keeps in/etc/link/min-relay-version, the operator's checkout's version, whether or not a relay is installed yet.LINK_ALLOW_DOWNGRADE=1is the operator's way to roll back on purpose. - The relay updater still installs a same-version build, since
relay-latestis rebuilt on every merge tomainwithout a version bump. So an earlier build of the same version can still be put back; nothing older than the installed relay or the floor can. A server bootstrapped without--min-versionand with no relay yet has no floor, and takes any signed release.
Download limits
Downloads give up after 15 s connecting or 60 s without data. They stop
past 64 KiB for SHA256SUMS, its signature and VERSION, and past
200 MiB for an archive.
CI
- Workflow actions are pinned to commit hashes.
- Jobs that run third-party actions get no secrets.
- Signing and publishing run in separate jobs that use only
actions/*. - Default permissions are read-only.
- A publish fails if the signature doesn't verify against
src/shared/release-signers.
Key rotation
How to rotate it: Rotate the release signing key.
Gaps
-
The first
lnk plugin add <url>trusts whatever key the repository serves that day; nothing inlnkvouches for an outside plugin's key. -
An outside plugin can go back to an older release signed by the same key only if its maker publishes it as the latest;
lnk upgraderefuses a version older than the installed one. -
An outside plugin's release without a
VERSIONfile, or whoseVERSIONsays newer than its program does, has its program run (contract, then--version) before it's refused as older. It is signed by the pinned key, so this runs nothing its maker couldn't run already. -
The release key is a GitHub Actions secret; see Known limitations.
-
A copy of
install.shthe relay didn't serve, such as the one in each release or in a checkout, has no relay version to compare with. Solatestpointed at an older signed release that has aVERSIONfile is installed.
