Skip to main content
← Back to list
01Issue
FeatureShippedExtensionsPublicTeam
Assigneesstack72

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:

  1. Host-key policy is implicit. strictHostKeyChecking is optional; when unset, no -o StrictHostKeyChecking is passed and behaviour falls back to whatever the runner's ssh_config says. The only pinning mechanism is knownHostsFile, 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.
  2. exec takes one shell string. command is a single string passed to the remote shell, so callers that build commands from data have to shell-quote every token themselves.
  3. copy is a plain scp/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.
  4. identityContent is written to a temp file. materializeTempKeys writes the private key to Deno.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

  1. Add an explicit host-key policy, e.g. hostKeyPolicy: "pinned" | "accept-new" | "insecure" plus a hostKeys: string[] input (OpenSSH public-key lines; public data, so no sensitive marking needed). For pinned, write the keys to a per-call known_hosts file and pass StrictHostKeyChecking=yes and UserKnownHostsFile=<that file>. Making the policy required (or defaulting to pinned once hostKeys is set) would make the security posture visible in the definition rather than inherited from the runner. collect-host-public-key already produces the data this would consume.
  2. Add an argv form, e.g. argv: string[] as an alternative to command, with each element POSIX single-quoted before it is joined for the remote shell.
  3. An atomic: true option for copy ... direction: to: upload to a temp path next to dst, optionally chmod/chown (mode, owner inputs), optionally verify a sha256, then mv into place.
  4. When an agent is available or can be started, load identityContent with ssh-add - over stdin into a short-lived per-call agent (ssh-agent -a <socket>, killed afterwards), and point ssh at it via IdentityAgent. 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
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 27 MOREPR_LINKED+ 2 MORESESSION_SUMMARIZED

Shipped

10/20/2026, 1:02:40 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack7210/19/2026, 11:17:41 PM
Editable. Press Enter to edit.

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-new against a per-call temp known_hosts file, then reads back the accepted key and writes a pin resource keyed by host: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 repin method, or a flag on collect-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.