Repository navigation
fix(ssh): close host-certificate and Unix-socket hardening gaps - #332
Conversation
KnownHostsCommand output now feeds host-certificate validation. For a certificate and any method other than NoCheck or a pinned key, the HOSTNAME invocation runs once with the certificate as %K and %t, and its @cert-authority and @Revoked lines get the same checks as file lines. A certificate from a command-trusted CA that fails a check is refused, not downgraded. The output is reused for the plain-key fallback instead of running the command again. The marker scan and CA lookup are split into a file wrapper and a text core so both sources share one matcher. RevokedHostKeys accepts several files, as OpenSSH 10.3 does. A key listed in any file is refused; an unreadable or KRL file, or an unexpandable path, anywhere in the list refuses every host key. none or /dev/null is valid only as the sole value. bssh -G prints the expanded paths on one line. On macOS and the BSDs the Unix listener is bound and narrowed to 0600 inside a private mkdtemp directory beside the target, then moved into place with renamex_np(RENAME_EXCL) or link and unlink, so the configured path never holds a wider mode and no existing node is replaced. The macOS test runs in the macOS leg of openssh-regress.yml. Validated with the narrow lib suites for tokio_client, ssh_config and streamlocal, the ssh_config_dump_test integration test, clippy and fmt. The Apple code path was compile-checked for aarch64-apple-ios in a scratch crate and exercised on Linux with the link and unlink move. Closes #330
Implementation Review SummaryIntent
Findings AddressedNo CRITICAL or HIGH findings, so no code was changed. All four items meet their acceptance criteria. Each departure is sound: Remaining Items
Verification
|
The @Revoked marker scan split fields on spaces only and matched host fields only as globs, while @cert-authority lookup splits on any whitespace and matches hashed |1| fields. A revoked CA or certified key whose @Revoked line used a hashed host field or tab separators was therefore ignored next to a matching @cert-authority line, and the certificate was accepted. This applied to known_hosts files and to the KnownHostsCommand output that #330 now loads for certificates; OpenSSH refuses in both cases. The scan now splits on ASCII whitespace and matches each non-negated pattern with host_pattern_matches, keeping the lenient bare-host retry. Negated patterns still never match and never veto, so revocation stays fail-safe. Plain-key marker checks share the same scan and gain the same coverage. Validated with cargo test --lib for host_certificate, host_verification, connection, ssh_config, and forwarding::streamlocal, the ssh_config_dump_test integration test, clippy --lib --tests -D warnings, and fmt --check. Refs #330
Security and Performance ReviewFixed
Checked, no finding
Remaining (LOW, not fixed)
Verification on 62fc1e6: |
On non-Apple BSDs the owner-only socket is hard-linked into place and the temporary name is then removed. If that removal failed after the link succeeded, the bind reported an error while a socket node stayed at the configured path. The link now counts as the completed move, and the private-directory drop guard removes the temporary name, so the configured path never holds an orphaned node after an error. Also corrects the DNSSEC ordering comment in check_server_key (the certificate HOSTNAME invocation runs first), documents KnownHostsCommand @cert-authority and @Revoked lines and the RevokedHostKeys file list in docs/architecture/ssh-client.md, and rewrites the ARCHITECTURE.md host certificate section, which still said bssh never advertises certificates. No Korean docs exist in the repository. Validated with cargo test --lib forwarding::streamlocal, clippy --lib --tests -D warnings, and cargo fmt. The BSD fallback and the macOS-only test cannot run on this host. Refs #330
…link probe_known_hosts_path called metadata, got NotFound, and then treated any successful symlink_metadata as a dangling symlink. When a concurrent first connection created known_hosts between the two calls, the probe refused the host with "exists but cannot be resolved" (ServerCheckFailed). This made test_accept_new_concurrent_first_connections_record_one_entry flaky under load: it failed CI on this PR and reproduced on main 5b20ce5 at 2/720 runs with 12 parallel loops. A path that is not a symlink on the second look is now probed once more; a real dangling symlink is still refused. With the fix the same stress run gives 0/2400 failures. A new test pins the fail-closed dangling-symlink case, and it fails if a path present on the second look is treated as missing. The racer assertion now prints the result it got. Refs #330
Summary
KnownHostsCommandoutput is a certificate-authority source. For a host certificate under any method exceptNoCheckand pinned keys, theHOSTNAMEinvocation runs once with the certificate as%K/%t(%fis the certified key's fingerprint, as in OpenSSH). Its@cert-authorityand@revokedlines go through the same text core as file lines (host scope,!negation, hashed hosts, port retry), followed by the usual principal, validity, critical-option,CASignatureAlgorithmsandRevokedHostKeyschecks. A certificate from a command-trusted CA that fails any check is refused, not downgraded. The output is reused for the plain-key fallback. A command failure is still a hard error.RevokedHostKeystakes a list of files. A key in any file is refused. An unreadable or KRL file, or an unexpandable path, anywhere in the list refuses every host key.noneor/dev/nullmust be the only value.-Gprints the expanded paths on one line.sinside a privatemkdtempdirectory (.bsshXXXXXX, 0700) next to the target, narrowed to 0600 and verified there, then moved into place without replacing anything:renamex_np(RENAME_EXCL)on Apple,link(2)on other BSDs (viastd::fs::hard_link, no extraunsafe). A completed link is the move; the private directory, including the temporary name, is removed on every path, so the configured path never holds an orphaned node after an error. A temporary path oversun_pathfails closed with an error that names the limit. The umask is not touched.ARCHITECTURE.mdanddocs/architecture/ssh-client.mddescribe the current host-certificate behavior (the "never advertises certificates" claim was stale since fix(openssh): close residual regress compatibility gaps #314).Changes during review
@revokedmarker lines were split on spaces only and matched without hashed|1|host fields, so a CA or certified key revoked by a hashed or tab-separated line was ignored next to a matching@cert-authorityline. This predates the PR for known_hosts files and also reached the newKnownHostsCommandsource. The scan now splits on any whitespace and matches hashed patterns; negated patterns never match and never veto, so revocation errs toward refusing. Covered byhashed_and_tab_separated_revoked_lines_refuse_the_certificate(files and command output, ports 22 and 2222).probe_known_hosts_pathmistook a known_hosts file created by a concurrent first connection (betweenmetadataandsymlink_metadata) for a dangling symlink and refused the host. It reproduced on main at 2/720 under 12 parallel loops; after the fix 0/2400. A path that is not a symlink on the second look is probed again, and a real dangling symlink is still refused (test_accept_new_dangling_symlink_known_hosts_fails_closed).Test plan
cargo fmt --check,cargo clippy -- -D warnings,cargo test --lib(1801 passed), andcargo test --tests -- --skip integration_testpass locally on the final head. Threetests/integration_test.rscases that need a live localhost sshd fail identically on main 5b20ce5 on this host and are skipped in CI.keytype,limit-keytype,agentpass;hostkey-agentfails only the twosk-*certificate phases tracked in fix(openssh): decide FIDO host-key coverage for the hostkey-agent sk certificate phases #328.openssh-regress.ymlstep rancargo test --lib forwarding::streamlocalonmacos-latest(run 39699945497): 5 passed, includingmacos_socket_node_is_owner_only_before_it_appears.Closes #330