161 lines
11 KiB
Markdown
161 lines
11 KiB
Markdown
# Security model
|
|
|
|
- **The bastion dials out.** Every lane is a reverse connection from this
|
|
machine to the host. No port is opened on an externally reachable interface;
|
|
the control ports and console bind loopback only, and the optional
|
|
memory-mirror forwarder binds loopback plus the replica container's private
|
|
gateway address, with every accept source-checked.
|
|
- **Inside the tunnel, only gated listeners.** The host can connect to up to
|
|
three inference forwards (local ollama, an OpenAI-compatible server, a PII
|
|
sterilizer), each a proxy to a service you run, on loopback by default and
|
|
elsewhere only if you point it there by environment variable; and to one
|
|
port per server in the service slot (18100 to 18199), joined to that
|
|
server's socket; and to one port per coder run (18200 to 18299), joined to
|
|
that run's control socket. Every connection is checked when it is accepted: a forward
|
|
needs the pairing to hold the `inference` capability family, a service
|
|
slot port needs the `services` family and a reach grant for that server
|
|
naming that host, and a coder run port needs the `harness` family, the run
|
|
to be that host's and within its deadline, and the connection to come from
|
|
that host's own tunnel address. Nothing else in the tunnel accepts a connection.
|
|
- **A Host on this machine gets the same walls.** Its lanes go over its lane
|
|
socket with mutual TLS against the CA pinned at pairing, the pairing's name
|
|
checked and the bearer on every lane; a pairing with no bearer opens none.
|
|
A coder run it owns is a socket in its own runs directory (0660, group
|
|
`privatae-host`, else root), checked at every accept as a run port is, and
|
|
removed at run-down and at the watchdog's stop. The runs directory must be
|
|
one only root can change (root's, writable by nobody else, under
|
|
directories that are root's and not writable by others unless sticky), or
|
|
no socket is made in it. On an anchored machine its run-up needs the
|
|
standing coder grant like any pairing's.
|
|
- **Scoped tunnel.** The WireGuard peer's only allowed address is the paired
|
|
host's single tunnel address, so the tunnel can reach nothing else. A
|
|
pairing that offers anything wider is refused, and a pairing file that
|
|
holds one brings no tunnel up.
|
|
- **No MCP endpoint.** Nothing on this machine drives the host.
|
|
- **Grants are verified here.** What the host may do arrives signed. The verify
|
|
key is pinned on first use, and a deny always beats an allow.
|
|
- **Fixed argv.** Every management op runs a fixed command line. File ops are
|
|
dedicated Go handlers with no shell. Text extraction runs a named local tool
|
|
from a fixed template.
|
|
- **Approval on the machine, once enrolled.** With a second factor enrolled
|
|
here, folder grants, move plans, model pulls and root sessions each need a
|
|
code from this machine, bound to that exact request, single use. Without
|
|
one, grants and move plans rest on the signed grant alone, and model pulls
|
|
and root sessions are refused.
|
|
- **On an anchored machine, a write needs an approval verified and spent
|
|
here.** It comes from the second factor enrolled here or from your pinned
|
|
approval device, is bound to the op and a digest of its arguments, and is
|
|
used once; with neither enrolled, those writes are refused. The exceptions
|
|
take no fresh approval: standing work under a place you set up at the
|
|
machine (backup fetch and backup push, the host's context store), a backup
|
|
restore request (approved on its own, below), runs under op terms you
|
|
approved earlier, a root command under the grant you gave here with the
|
|
code, the anchor host's signed pairing records, revokes, and a
|
|
self-update to a newer version we signed. Repeated refused writes close a pairing's
|
|
write lanes for a while.
|
|
- **Host-written programs run sandboxed first.** An op runs only under signed
|
|
terms you approved, as a dry run in a sandbox with no network, and never
|
|
changes anything outside its declared file effects. A one-time apply lands
|
|
only the change list you saw; a run under a standing grant is held to the
|
|
grant's bounds.
|
|
- **A server in the service slot has no network.** The host's tunnel is its
|
|
only way in, and it cannot reach this machine, your network or the
|
|
internet.
|
|
- **Egress for someone else reaches the public internet, never this
|
|
machine.** A coder run's proxy, always, and the egress lane on an anchored
|
|
machine refuse loopback, link-local (the cloud metadata address included),
|
|
private, CGNAT and reserved addresses, and any address this machine takes
|
|
as its own: one on its interfaces (a public one included), and on Linux any
|
|
address the kernel's routing table delivers here (a `local`, broadcast,
|
|
anycast or multicast route, such as an AnyIP range on `lo`), asked of the
|
|
kernel per connection. The IPv4 address inside an IPv4-mapped, NAT64 or
|
|
6to4 address is checked too, every address a name resolves to is checked,
|
|
and the address checked is the one dialled. When the machine cannot tell,
|
|
it refuses. A coder run reaches a LAN host only when you named it in
|
|
`coders set --allow-lan`; the anchored egress lane has no such exception.
|
|
The anchored lane's refusal of this machine's own non-private addresses is
|
|
new in 0.50.0; before it, that lane refused only the internal ranges. A
|
|
local (unanchored) machine's egress lane is unchanged and vets no
|
|
destination. The backup lanes (`backup_fetch` and `backup_push`, anchored
|
|
or not) apply the same check of this machine's own addresses, to every
|
|
address the URL's name resolves to, to each redirect a fetch follows and to
|
|
the address dialled; this too is new in 0.50.0, and before it they refused
|
|
only loopback, private, link-local, multicast, unspecified, CGNAT and ULA
|
|
addresses. Also since 0.50.0 they classify an address as a coder run's
|
|
proxy does: the IPv4 address inside a NAT64 `64:ff9b::/96` or 6to4
|
|
address is judged, and IPv4-compatible and IPv4-translated addresses, the
|
|
rest of `64:ff9b::/32`, site-local `fec0::/10`, `0.0.0.0/8`,
|
|
`198.18.0.0/15` and `240.0.0.0/4` are refused. Not covered: a public
|
|
address that reaches this machine only through a 1:1 NAT in front of it,
|
|
which is on no interface and in no local route. The bastion does not learn
|
|
that address.
|
|
- **A terminal is checked here and logged.** On a local (unanchored) machine,
|
|
a terminal opens when the host asks, with no approval. No signed grant is
|
|
needed, but one that is sent is verified, and a deny in it refuses. On an
|
|
anchored machine, a host other than the anchor needs the `session` family
|
|
in its pairing grant, and each terminal needs an approval from your pinned
|
|
approval device or the second factor enrolled here, bound to that one
|
|
session and used once. A root terminal, on either kind of machine, needs
|
|
the bastion to run as root, a second factor enrolled here, and a code
|
|
entered on this machine for that session; an approval device cannot approve
|
|
it. Each session, and each refusal of a well-formed request, is written to
|
|
the audit log on this machine when the pairing has a grant store; a
|
|
malformed request is refused with no audit line.
|
|
- **Root commands run only under a grant given here with the code.** Only
|
|
the anchor host can hold it. It names this machine, ends when you said, is
|
|
revoked from this console, and needs the bastion to run as root. A revoke
|
|
or an expiry also stops a command already running, within 2 seconds. Every
|
|
command and its output is written to a transcript here and to the audit
|
|
log; refusals, malformed commands and setup failures are audited too.
|
|
- **Root only where there is no one else to ask.** Claiming ownership or the
|
|
anchor requires root because those are machine-wide facts.
|
|
- **On macOS and Windows, a networked bastion is the system's service.** It
|
|
runs as root under launchd, or as LocalSystem under the Windows service
|
|
manager, starts at boot and is restarted when it stops, so the signed-in
|
|
user cannot stop it. It is registered only from a program that only root or
|
|
an administrator can replace. Beside it, `pair`, `run` and `status` from an
|
|
account that does not administer the machine are refused, so this binary
|
|
will not start a second bastion in that account. An older or modified copy
|
|
is not bound by that refusal; what keeps the service in place is that the
|
|
signed-in user does not administer the machine.
|
|
- **Observation changes nothing on the machine.** Observe ops read and report.
|
|
No input injection lives in the bastion. The observe ops that send traffic
|
|
are `observe:net`, an outbound probe to a host you name; `observe:k8s`, a
|
|
read-only query of a cluster API; and the three monitoring ops
|
|
(`observe:alerts`, `observe:metrics`, `observe:logs`), read-only HTTP
|
|
against an Alertmanager/Prometheus/Loki on the box's own network —
|
|
private addresses only, re-checked at the socket so neither a redirect nor
|
|
a DNS answer can walk a query out of that neighbourhood.
|
|
- **A backup's key goes to a host only when you approve it here.** A host's
|
|
request to restore files a request and gets nothing until you approve it
|
|
on this machine; the key sealed to the host then opens that one backup and
|
|
no other, and this bastion's key never leaves this machine. Thinning the backup
|
|
folder removes only backup files this bastion recorded fetching, still as
|
|
they arrived, judged by arrival here. A host sends from this machine only
|
|
backup files in its own folder or a folder you shared with it.
|
|
- **No delete op on shared stores.** The one write lane is a move plan you
|
|
approved, and it archives rather than deletes.
|
|
- **Verified releases.** The self-updater checks the release manifest's
|
|
signature against the release keys compiled into it, then checks the
|
|
binary's SHA-256 against that manifest before anything is swapped. The
|
|
agent download is checked the same way, and an installed agent is never
|
|
replaced with an older one. The release key never reaches the machine that
|
|
builds and publishes: each release waits until the key holder signs it on
|
|
their own computer. The installer checks the SHA-256; see
|
|
[SECURITY.md](../SECURITY.md) for what that does and does not prove.
|
|
- **No secrets cached for git.** The credential helper fetches a token from
|
|
the host on every auth and stores nothing itself, and `git-setup` resets
|
|
the forge's helper chain before adding it. With git 2.46 or later, git's
|
|
own store and cache helpers keep nothing, wherever they are configured; a
|
|
helper that records everything it is given still receives the token. A git older than 2.46 gets the username
|
|
and password, and for it only helpers configured before `git-setup`'s
|
|
lines are kept out.
|
|
|
|
## How each claim is tested
|
|
|
|
[claims.md](claims.md) names, for each point above, the test that fails if
|
|
it stops being true, and says where there is none.
|
|
|
|
To report a security issue, please do not open a public issue. See
|
|
[SECURITY.md](../SECURITY.md).
|