Relationships
#3581 Explicit pinned host-key policy, argv exec, atomic copy, and agent-based identityContent
Opened by randybias · 10/17/2026· Shipped 10/20/2026
Problem
A few hardening requests from using @swamp/ssh (checked against 2026.10.16.1) to manage hosts that are rebuilt often:
- Host-key policy is implicit.
strictHostKeyCheckingis optional; when unset, no-o StrictHostKeyCheckingis passed and behaviour falls back to whatever the runner'sssh_configsays. The only pinning mechanism isknownHostsFile, a path to a file that must already exist on the runner. There is no way to supply the expected host key(s) as model data. exectakes one shell string.commandis a single string passed to the remote shell, so callers that build commands from data have to shell-quote every token themselves.copyis a plainscp/rsync. The destination is written in place, so a reader on the remote side can see a partially written file, there is no way to set the final mode/owner as part of the copy, and nothing verifies what landed.identityContentis written to a temp file.materializeTempKeyswrites the private key toDeno.makeTempFile()(mode 0600) for the duration of the call. It is cleaned up afterwards, but the key does touch disk, and a crash can leave it behind.
Proposed solution
- Add an explicit host-key policy, e.g.
hostKeyPolicy: "pinned" | "accept-new" | "insecure"plus ahostKeys: string[]input (OpenSSH public-key lines; public data, so no sensitive marking needed). Forpinned, write the keys to a per-call known_hosts file and passStrictHostKeyChecking=yesandUserKnownHostsFile=<that file>. Making the policy required (or defaulting topinnedoncehostKeysis set) would make the security posture visible in the definition rather than inherited from the runner.collect-host-public-keyalready produces the data this would consume. - Add an argv form, e.g.
argv: string[]as an alternative tocommand, with each element POSIX single-quoted before it is joined for the remote shell. - An
atomic: trueoption forcopy ... direction: to: upload to a temp path next todst, optionallychmod/chown(mode,ownerinputs), optionally verify a sha256, thenmvinto place. - When an agent is available or can be started, load
identityContentwithssh-add -over stdin into a short-lived per-call agent (ssh-agent -a <socket>, killed afterwards), and point ssh at it viaIdentityAgent. Keep the temp-file path as a fallback.
Happy to split these into separate issues if that is easier to track.
Alternatives
Today callers can work around 1 by generating a known_hosts file before each run and passing knownHostsFile, 2 by quoting manually, and 3 by following copy with an exec that does mv/chmod/sha256sum. They work but every caller has to reimplement them.
Upstream repository: https://github.com/swamp-club/swamp-extensions
Environment
- Extension:
@swamp/ssh@2026.10.16.1 - swamp:
20261013.025835.0-sha.1b6f0c59 - OS:
linux(aarch64) - Deno:
2.9.7 - Shell:
/usr/bin/zsh
Shipped
Click a lifecycle step above to view its details.
system commented 10/17/2026, 1:50:50 PM
Classified automatically when this issue was filed.
- Source: Extensions
If you feel this classification is incorrect, add a ripple to tell us so.
randybias commented 10/17/2026, 5:08:54 PM
One addition to item 1 (re-checked against 2026.10.16.1): a TOFU pin record for hosts whose keys are not known in advance.
Explicit hostKeys covers hosts whose key is known up front. For freshly built hosts, though, the realistic policy is trust-on-first-use, and today that means strictHostKeyChecking: accept-new against a runner-local known_hosts file. The pin then lives outside swamp: it is not visible in model data, it is lost when the runner changes, and a key change surfaces only as a raw ssh error.
Suggestion: a hostKeyPolicy: "tofu" mode that:
- on first contact (nothing pinned yet), uses
accept-newagainst a per-call temp known_hosts file, then reads back the accepted key and writes a pin resource keyed byhost:port(key type, base64 key, SHA256 fingerprint, first-seen time); - on later calls, builds the per-call known_hosts file from that pin and connects with
StrictHostKeyChecking=yes, so a changed key fails closed with a typed "host key changed" error that names the pinned and presented fingerprints; - re-pins only through an explicit action (e.g. a
repinmethod, or a flag oncollect-host-public-key), never silently.
Keying by host:port matters because several sshd instances on one address (or port-forwarded hosts) have different keys. This would also make the pinned keys auditable and portable across runners, since they live in swamp data instead of ~/.ssh/known_hosts.
stack72 commented 10/20/2026, 1:02:52 AM
Thanks @randybias for reporting this! We shipped: Make the @swamp/ssh host-key posture explicit and enforceable, and add three hardening options. (1) hostKeyPolicy (pinned, tofu, accept-new, insecure) plus hostKeys. Pinned and tofu hosts use per-call known_hosts files keyed by a deterministic HostKeyAlias. Tofu stores an address:port-keyed hostKeyPin resource, mismatches raise a typed host-key-changed error, and an explicit repin method re-pins. (2) Fix the open method, which today ignores StrictHostKeyChecking, UserKnownHostsFile, ConnectTimeout and extraOptions, so the ControlMaster verifies host keys under runner defaults. Also fold the configured policy into the ControlPath digest. (3) exec argv form with POSIX quoting and direct sudo. (4) Atomic copy: staging file next to dst, optional chmod, chown and sha256 verification, then mv. (5) identityContent loaded into a short-lived per-call ssh-agent over stdin, falling back to the temp file.. The fix has been merged and a release is on its way. We appreciate your contribution to swamp.
Sign in to post a ripple.