Saphan StudioDocs
Configuration

SSH host key policy

The measured problem, the two policies, and how rotation is reconciled before the dial.

The problem, measured

On our own fleet, one Linux runner regenerates /etc/ssh/ssh_host_*_key four seconds after every boot (cloud-init present) — while other machines in the same fleet never rotate at all. Every dial to the rebooted machine failed until someone re-pinned 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, each derived from measurement rather than taste:

  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 — and measurement after a real reboot showed those values are unaffected by rotation.

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.

Direction of travel

SSH host certificates signed by a fleet CA. With those, rotation stops being an event for ssh at all and ssh_host_key_policy remains only for machines outside the CA. That is the option that removes the problem rather than managing it, and it is separate, planned work.

On this page