Skip to content

Security Maintenance

Review focus

Changes to SSH argv, lifecycle transitions, stderr handling, environment recovery, runtime paths, or remote supervision receive direct invariant tests. Review exact process arguments and state changes rather than only successful output.

OpenSSH configuration remains trusted input so host-key verification, proxy routing, identities, and system prompts keep their native behavior. Secondary channels retain explicit no-authentication and false-proxy barriers. A lost master object is terminal.

Diagnostics and environment

Public master failures stay path-free and never copy raw stderr. Drains remain bounded and continuous. Session probes use bounded output, short timeouts, new local process groups, an exact allowlist, owned absolute runtime paths, and no global environment mutation.

Remote program argv remains data encoded exactly once. Supervision owns a single new remote process group and cannot broaden cleanup to other groups or users.

Acceptance isolation

The Linux acceptance test keeps the native SSH client on the host and runs the server as an unprivileged user in a throwaway container. Host sshd is neither required nor installed. Make builds the test-only image from tests_acceptance/server/Containerfile, whose base digest and signed Debian snapshot pin its toolchain; the first build requires network access and later builds reuse the engine cache. No test image is published or packaged.

The image also installs tini from that signed snapshot and runs it as PID 1 to forward signals and reap orphaned children. The fixture explicitly uses --init=false, so Podman/Docker never needs to find or mount an init binary from the host, even if engine defaults enable init injection. In particular, lookup init binary: catatonit ... not found is a test-fixture dependency error, not a reason to install another host package or change the operator's PATH. Both live gates verify PID 1 and orphan reaping before client authentication.

The ordinary gate creates a fresh client key, writes an isolated SSH configuration and known-hosts entry, and disables agent use and unrelated identities. The server generates its own fresh host key inside the container. Only the client's public authorization key is passed in; there are no checkout, identity, agent-socket or hardware-device mounts. Its root filesystem is read-only, capabilities are dropped, and no-new-privileges is enabled. The engine publishes an unused port only on host 127.0.0.1, without a racy host-side reservation. Before creation, engine metadata selects direct per-container pasta for rootless Podman or the native Docker bridge. Podman never uses a forced shared bridge; rootful Podman is rejected with instructions to run without sudo. There is no network fallback or change to host AppArmor policy. Startup and teardown errors remain blocking, including failures to remove the container. It exercises the real library, verifies one accepted authentication and one master, kills that master to prove no fallback, and checks that only the owned remote group is cleaned. It also exercises conflicting native background/stdin/session/tunnel settings, literal percent characters, and the maximum safe mux pathname length.

Both make test-acceptance and make test-fido call the existing shared live scenario and the same container fixture. Remote Python, PID-file reads, process-group inspection and signals all run inside that container: a container PID is never used in a host kill operation. The fixture removes only its unique owned container, including after a failed or cancelled startup. Startup is observed through engine logs, without adding an SSH connection or authentication.

GitHub CI calls the same make test-acceptance target without provisioning a host server. make ci includes this gate once. Engine subprocesses use the same D-Bus cleanup as workflow validation; the native client's session environment and prompt routing are unchanged. Missing required tools and server failures fail the requested gate instead of reporting a successful skip.

All subprocesses and temporary material are reaped on success, assertion failure, timeout, and cancellation. Ordinary tests remain network-blocked and independent of live Linux session services. The security contract is the normative source.

Operator-assisted FIDO check

Run ordinary checks and make test-acceptance first. Then coordinate with the operator before running in their normal graphical login session, without sudo:

make test-fido FIDO_IDENTITY=/absolute/path/to/id_ed25519_sk

Use an existing OpenSSH ed25519-sk or ecdsa-sk identity with its matching .pub file and an available physical authenticator. The test never generates, rewrites, or copies the hardware credential. It creates temporary symlinks to the selected identity on the host, and passes only its public key to the container server. Existing server configuration, authorized keys, and known hosts are untouched. The host needs CPython 3.13 or 3.14, the OpenSSH client tools and a working Podman or Docker engine, not an OpenSSH server. Authentication uses the native local agent or security-key provider and recovered initial session environment; no USB device or agent forwarding into the container is needed.

The operator enters any PIN/passphrase in the native system prompt and touches the key when requested, never in chat or a library API. The startup window is 120 seconds, starting only after the container server is ready. After one accepted authentication, the same acceptance sequence checks mux commands, owned cleanup and failure without fallback after master loss; no second authentication is permitted. This hardware gate is not part of make ci, ordinary tests, or automated acceptance and cannot be reported as passed without the actual operator-assisted run.