SSH host key policy
The problem, the two policies, how rotation is reconciled before the dial, and how a machine gets its first pin.
The problem
Some machines regenerate their SSH host keys on every boot — a cloud image with cloud-init is the usual case — while others never rotate at all. Under strict checking, every dial to such a machine fails after a reboot until someone re-pins it by hand, and re-pinning by hand is precisely the habit that makes a real key substitution invisible.
Blanket StrictHostKeyChecking=no is not the answer: it also admits a brand-new host
answering on a reused address, which is the one case the guard exists for.
Keys
In the file:
<id> is the machine id from the registry — the same string used as the Host stanza name
in ~/.saphan/ssh/config. Resolution order: per machine → [fleet] → built-in
refuse.
Behaviour
| Policy | Known machine, key unchanged | Known machine, key changed | Unknown machine |
|---|---|---|---|
refuse (default) | dial | refuse, fail loud, event recorded | refuse |
accept_rotation | dial | re-pin, dial, event recorded | refuse — this is not a rotation |
Four constraints:
accept_rotationis not blind trust-on-first-use. It applies only to a rotation on a machine already pinned. First contact with an unknown host is refused under both policies — the answer to an unknown host is to admit the machine.- Accepting always leaves a trace. The record receives host, old fingerprints, new fingerprints, timestamp, and the policy that allowed it. A silent acceptance belongs to the same class as a skipped test counted as passed.
- Per-machine scope is required, not decorative. Rotation is routine on some machines
in a fleet and unheard of on others; a single global
accept_rotationwould disarm the guard exactly where rotation is not normal. - The capability registry is pinned to the machine's device key, not to its SSH host key. Otherwise every reboot would invalidate that machine's measured confinement capability and probe history, which a host-key rotation does not change.
How it is enforced
ssh itself stays strict under both policies. The generated ~/.saphan/ssh/config
emits, for every machine stanza:
Two consequences worth stating plainly:
- The fleet has its own pin store. It never inherits, and never edits, your personal
~/.ssh/known_hosts. accept_rotationis never implemented by relaxing ssh. Rotation is reconciled before the dial: the live keys are compared against the pin, the policy is applied, a permitted re-pin is deliberate, and the outcome is typed —match·rotated-accepted·rotated-refused·unknown-host. Four distinct states of the world; a caller never has to infer one from another.
How a machine gets its first pin
Pins are written when the fleet's ssh configuration is written, for every live machine, and the pin of a retired machine is removed.
- A pin you already hold is adopted. If your own
~/.ssh/known_hostshas an entry for the machine, that key is copied into the fleet's store — your earlier trust decision, moved rather than re-made. Your file is read, never edited. - Otherwise, only admission scans.
saphan machine admitandsaphan machine renew— a human deciding now to trust a machine — scan the host's key over the network when neither store has one.saphan machine pin, which may run unattended, never scans: a machine with nothing to copy is reported as not found.
⚠ A key scanned at admission is trusted on first sight. When the network between the two machines
is not yours, compare the pinned key (ssh-keygen -l -f ~/.saphan/ssh/known_hosts) against the
machine's own (ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub, run on that machine).