Skip to content

0016 — A managed known_hosts file for SSH host CAs, distinct from sluisctl bao

Status: Accepted; refines 0011, distinguishes from 0013 Date: 2026-09-28

Context

0011 put host certificates on the secret store's own SSH CA, and docs/connect/ssh.md already documents the manual side of trusting it: fetch the CA's public key (bao read ssh/config/ca, or read it off any certificate with ssh-keygen -L), then add one @cert-authority <pattern> <key> line to known_hosts, by hand, once per environment, on every laptop. That chore repeats on every laptop an operator uses, and again on every CA rotation, and gets no easier for being small: forgetting it is not a failure, it is ssh's ordinary unknown-host prompt, which reads as "nothing is configured" rather than "one line was never added."

0013 drew a hard line against growing sluisctl with OpenBAO's own data plane: every new secrets engine, every new flag on sign, is bao's own surface to keep current, not a courier this repository re-implements one call at a time. A command that trusts an SSH CA looks, at a glance, like exactly that kind of courier — it is one more thing OpenBAO's SSH secrets engine answers.

Decision

sluisctl ssh known-hosts is a laptop-configuration command, not an OpenBAO client, and that is why it does not fall under 0013's line. 0013 refuses to reimplement OpenBAO's growing data plane — kv, sign, write, and whatever engine comes next — one recipe at a time. This command touches none of that surface: it reads exactly one permanently-unauthenticated, permanently-unchanging endpoint (an SSH secrets engine's public_key, the same one bao read ssh/config/ca already reads) that carries no secret and decides no authorization, and OpenBAO is only one of two sources it accepts — a plain url: does the identical job for an installation that fronts its CA another way. What this command grows with is nothing upstream; what it owns is a file on this laptop, the same category of thing kubeconfig and aws-config (cmd/sluisctl/config_writers.go) already own.

Input is entirely a config list this binary never gives a name to. An entry pairs one or more ssh_config-style host patterns with a CA source — url: or openbao: {namespace, mount}, read the same way sluisctl bao already resolves its own address (0013): --address, then $BAO_ADDR, then $VAULT_ADDR. The list lives in config.yaml's own sshKnownHosts: section, or $SLUISCTL_SSH_KNOWN_HOSTS when the file names none — the same "file, then one environment variable" shape every other setting in this tool already uses for a single value, applied here to a list. No estate name, domain or namespace is ever declared in this binary.

It owns exactly one file, and only that one: ~/.ssh/known_hosts.d/sluisctl by default (--file names another), fully rewritten on every run — never ~/.ssh/known_hosts, and never ~/.ssh/config. It checks whether ~/.ssh/config already points a UserKnownHostsFile line at that file (a plain substring check, tilde form or absolute, whichever a person wrote) and prints the one line to add when it does not; it never edits that file itself, for the same reason kubeconfig writes through kubectl config rather than rewriting a kubeconfig wholesale — it is the person's own file, with entries this tool knows nothing about.

A fetch failure for one entry never drops that entry's trust, and never fails the whole run. The previous run's line for that exact set of patterns is kept, with a warning, when there is one to keep; the entry is skipped, with a warning, when there is not. One environment's CA being briefly unreachable must not blank out every other environment already trusted, and must not be indistinguishable from "this CA is no longer trusted" — silently dropping a line would be exactly that.

Only ssh-ed25519 CA keys are ever written. OpenBAO's own SSH secrets engine defaults new CAs to ssh-ed25519, every host certificate this design targets is signed by one, and refusing anything else by name is what keeps a misconfigured entry (the wrong mount, a stray answer from a different service entirely) from writing a @cert-authority line for a key type nothing here can reason about.

sluisctl login refreshes this file automatically when sshKnownHosts (file or environment) names at least one entry — the same shape setup already gives kubeconfig and aws-config, run once by hand after signing in. Its failure is a warning, never a login failure: a stale, or still-absent, trust file is nothing like a failed sign-in, and most installations name nothing here at all.

Consequences

A laptop trusts a fleet's SSH host CAs the moment it signs in, with no separate step to remember, and stays current on every later login without anyone re-running a fetch-and-paste by hand.

Nothing here grows with OpenBAO. The openbao: source is one fixed call this design does not expect to ever need a second flag for; a url: entry needs no OpenBAO awareness in the caller's own installation at all.

The honest boundary: this command decides nothing about authorization — it fetches a public key that, by construction, proves nothing on its own; the CA's own signing role, and who may ask it to sign a host certificate, stay exactly where docs/connect/ssh.md already puts them, with the secret store's own owners. A url: entry handed something other than an OpenSSH public key, or a key whose own integrity depends on the URL's own TLS trust, gets exactly the trust that connection deserves — the same as the manual curl recipe it replaces, no more and no less.

Alternatives considered

A docs recipe only (the manual bao read / ssh-keygen -L steps docs/connect/ssh.md already has), matching 0013's general preference for documentation over a new command. Rejected: unlike a single bao kv get, trusting a CA safely needs a FILE this tool owns outright (never a blind append to known_hosts itself, which is the person's own), a fallback rule so one flaky environment does not erase every other, and a hook into login so it is current the moment someone signs in. That is exactly what already turned kubeconfig and aws-config from a recipe into a first-class command, and a shell one-liner does not keep that behaviour without reimplementing it worse, by hand, on every laptop.

Grow sluisctl bao itself, e.g. sluisctl bao ssh -mode=trust, reusing the passthrough 0013 already built. Rejected: the whole point of that passthrough is that everything after sluisctl bao is bao's own syntax, unchanged; teaching it to also rewrite a known_hosts file conflates authenticate, then run bao with laptop configuration, and would only ever work for an openbao: source, never a plain url: one.