Saphan StudioDocs
Configuration

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

saphan config set fleet.ssh_host_key_policy refuse                       # default
saphan config set fleet.ssh_host_key_policy accept_rotation
saphan config set host.runner-1.ssh_host_key_policy accept_rotation      # per machine

In the file:

[fleet]
ssh_host_key_policy = "refuse"

[host.runner-1]
ssh_host_key_policy = "accept_rotation"

<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

PolicyKnown machine, key unchangedKnown machine, key changedUnknown machine
refuse (default)dialrefuse, fail loud, event recordedrefuse
accept_rotationdialre-pin, dial, event recordedrefuse — this is not a rotation

Four constraints:

  1. accept_rotation is 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.
  2. 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.
  3. 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_rotation would disarm the guard exactly where rotation is not normal.
  4. 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:

    UserKnownHostsFile ~/.saphan/ssh/known_hosts
    StrictHostKeyChecking yes

Two consequences worth stating plainly:

  • The fleet has its own pin store. It never inherits, and never edits, your personal ~/.ssh/known_hosts.
  • accept_rotation is 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_hosts has 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 admit and saphan 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).

On this page