Skip to content

identity.ssh: sandbox ssh never laxer than host ssh for the pin (fixes #688) - #698

Merged
vyskocilm merged 4 commits into
mainfrom
fix/688-ssh-strict
Oct 25, 2026
Merged

vyskocilm merged 4 commits into
mainfrom
fix/688-ssh-strict

Conversation

@vyskocilm

@vyskocilm vyskocilm commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #688. Residual for unpinned hosts: #697.

Change

  • Pin block in generated ~/.ssh/config: StrictHostKeyChecking yes (was accept-new); whitelisted values of host ssh -G -l git <pin> (Host-block kex/ciphers/RequiredRSASize reach sandbox); RevokedHostKeys staged at ~/.ssh/revoked_host_keys.
  • knownHostsFor (now internal/cli/sshpin.go): matches hashed |1|, wildcards, ! negation, [host]:port; keeps @revoked, @cert-authority; reads files ssh -G names. No usable entry → refuse, exit 77: identity.ssh: this host has no known_hosts entry for github.com ....
  • probeSSHConfig failure/timeout/unwritable value → refuse (was -v-only aside + compiled defaults). Error carries ssh stderr, e.g. exit status 255: Bad owner or permissions on /usr/etc/ssh/ssh_config.d/50-suse.conf.
  • System ssh_config replacement: every whitelisted key, no delta vs -F /dev/null. Whitelist + RekeyLimit, StrictHostKeyChecking.

Redteam round (this branch, all fixed, named tests)

  • known_hosts path with space → relative fragment opened from project dir; repo-planted ace/kh became trusted key. Refuse non-absolute: TestProbeSSHPinRefusesASplitKnownHostsPath.
  • KnownHostsCommand / HostKeyAlias / HostName / Port for pin silently ignored; revoked key accepted inside. Refuse: TestProbeSSHPinRefusesWhatTheGeneratedBlockDoesNotCarry.
  • identity.ssh.host = "-E<file>" reached host ssh argv pre-validation. IsHostName + --: TestProbeSSHPinRefusesAnOptionShapedHost.
  • Unpinned host ask inside vs host yes. Carry StrictHostKeyChecking: TestStrictHostKeyCheckingCarriesButNeverLoosensThePin, rank check in TestSandboxSSHNeverLaxerThanHost.
  • Matcher divergences vs ssh-keygen -F all fail-closed (StrictHostKeyChecking yes); not changed.

Mutation-checked: each new cli test fails with its fix removed. TestSandboxSSHNeverLaxerThanHost, TestPinnedSSHWithNoKnownHostsEntryRefuses, TestSandboxSSHRefusesAHostKeyTheHostRevoked fail on main.

Cost

Pin whose host config sets HostName/Port (e.g. ssh.github.com:443) now refuses.

Docs

No docs/src change: book update batched in #700 (pinned-host text drafted here, reverted in 0ee5fa8, plus the two book tests that held it).

Harness

newAuthoredAccount authors system ssh_config like a snug sandbox; host's root-owned Include read as 65534 under raw bwrap → probe refused → 5 tests. Suite pins github.com via fixture known_hosts + ssh wrapper on PATH.

Checks

  • make gate green.
  • make integration: integration-sandbox green (299 pass). integration-signals: TestASignalWhileThePayloadIsParkedNeverReleasesIt/net fails PRECONDITION: no bwrap ever appeared under snug — same failure on origin/main (648950c) on this host, not this change.
  • make verify: 5 passed, 4 skipped, 0 failed.

🤖 Generated with Claude Code

vyskocilm and others added 3 commits October 25, 2026 20:20
…t ssh (fixes #688)

- Generated ~/.ssh/config: StrictHostKeyChecking yes (was accept-new), plus
  the whitelisted values of host `ssh -G -l git <pin>`, so a `Host <pin>`
  block reaches the sandbox (F4). RevokedHostKeys staged at
  ~/.ssh/revoked_host_keys (F5).
- known_hosts filter matches like ssh: hashed |1| entries, wildcards,
  negation, [host]:port; keeps @Revoked and @cert-authority; reads the
  User/GlobalKnownHostsFile ssh names. No usable entry for the pin refuses
  (F1).
- Probe failure, timeout or an unwritable value refuses (exitPolicy) instead
  of falling back to compiled-in defaults under -v only (F2, F3). The
  refusal carries ssh's own stderr reason.
- System ssh_config replacement writes every whitelisted key, not the delta
  from `ssh -F /dev/null` (F6). RekeyLimit and StrictHostKeyChecking added
  to the whitelist.

Redteam round on this branch, all fixed with named tests:
- known_hosts path with a space split into a relative fragment opened
  against the project dir: refuse non-absolute fragments.
- KnownHostsCommand, HostKeyAlias, HostName, Port for the pin: refuse.
- identity.ssh.host reached ssh argv before validation: IsHostName check
  and `--` before the host.
- Unpinned host got `ask` inside against host `yes`: carry
  StrictHostKeyChecking. Rest of the unpinned residual is #697.

Integration harness newAuthoredAccount authors a system ssh_config like a
snug sandbox does; the host's root-owned Include read as 65534 and host
ssh refused it. Suite pins github.com in a fixture known_hosts through an
ssh wrapper on PATH.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…efs #688)

CI run 40225572953: TestPinnedSSHWithNoKnownHostsEntryRefuses saw snug exit 0
on the GitHub runner. The wrapper only redirected UserKnownHostsFile, so the
runner's system-wide known_hosts could still supply github.com.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The maintainer batches book updates. Dropped with the prose: booksshconfig_test.go
and the refusal-quote half of TestProbeSSHPinRefusesAPinWithNoEntry, which
held it. Both are in #700, to restore with the docs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vyskocilm
vyskocilm merged commit 5e077bd into main Oct 25, 2026
5 checks passed
@vyskocilm
vyskocilm deleted the fix/688-ssh-strict branch October 25, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant