fix(ssh): bound the systemd agent's default identity lifetime - #329
Open
toku345 wants to merge 1 commit into
Open
fix(ssh): bound the systemd agent's default identity lifetime#329toku345 wants to merge 1 commit into
toku345 wants to merge 1 commit into
Conversation
The runbook created the systemd user ssh-agent as `ssh-agent -D -a $SSH_AUTH_SOCK`, with no `-t`. From man ssh-agent: "Without this option the default maximum lifetime is forever." So on the box ADR 0027 governs, every add path produced an identity that never expired: - the chezmoi-managed `Host *` `AddKeysToAgent yes`, which man ssh_config describes as adding the key "with the default lifetime, as if by ssh-add(1)", i.e. unbounded; and - a bare `ssh-add <key>` with no `-t`, which step 7 forbids but nothing enforced. The 8h window ADR 0027 calls load-bearing therefore held only when every load remembered `-t`. A post-compromise attacker inheriting SSH_AUTH_SOCK could sign GitHub challenges for as long as a login session stayed open, without ever capturing the passphrase. Set `-t 28800` on the unit's ExecStart. The agent default binds both add paths at once and carries no ssh_config version floor — the alternative, `AddKeysToAgent 8h` in the managed ssh config, would impose an OpenSSH >= 8.4 requirement on a file that renders to ~/.ssh/config on every present and future machine, and a sub-floor host loses all ssh because the parse abort precedes host matching (neither the Include nor a Match block can shield it). This is a default, not a cap: an explicit `ssh-add -t <life>` still overrides it in either direction, so the commit does not claim that no add path can create an unbounded oracle. Step 7's "Never load with no `-t`" is reworded rather than dropped — the flag would have made the absolute form false while the imperative is still the tighter contract. The runbook is not chezmoi-managed, so this changes what a future setup produces and leaves a running box alone; the manual procedure for an existing box is included, along with the fact that the restart empties the agent so step 7 must be re-run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
| Filename | Overview |
|---|---|
| docs/headless-linux-auth-setup.md | The new agent lifetime, migration instructions, and operational explanation are internally consistent with the existing bounded-credential design. |
Reviews (1): Last reviewed commit: "fix(ssh): bound the systemd agent's defa..." | Re-trigger Greptile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The runbook created the systemd user ssh-agent as
ssh-agent -D -a $SSH_AUTH_SOCK, with no-t. Fromman ssh-agent: "Without this option the default maximum lifetime is forever."So on the box ADR 0027 governs, every add path produced an identity that never expired:
Host *AddKeysToAgent yes, whichman ssh_configdescribes as adding the key "with the default lifetime, as if by ssh-add(1)" — i.e. unbounded; andssh-add <key>with no-t, which step 7 forbids but nothing enforced.The 8h window ADR 0027 calls load-bearing therefore held only when every load remembered
-t. A post-compromise attacker inheritingSSH_AUTH_SOCK— the explicit threat model of ADR 0027 — could sign GitHub challenges for as long as a login session stayed open, without ever capturing the passphrase.This sets
-t 28800on the unit'sExecStart.Why the agent and not the ssh config
The obvious alternative,
AddKeysToAgent 8hinprivate_dot_ssh/config.tmpl, was tried first and rejected in review. The time-interval form requires a newer OpenSSH, and below that floor ssh does not ignore the line — it fails closed for every connection (unsupported option→terminating, 1 bad configuration options, rc=255), with the abort happening before host matching, so neither theIncludeon line 1 nor aMatchblock can shield it. That file renders to~/.ssh/configon every present and future machine, and recovery is a footgun: the file is chezmoi-managed, so a hand-edit is undone by the nextchezmoi apply, and if the chezmoi source remote is SSH thenchezmoi updateis itself broken.The agent default binds both add paths at once and carries no version floor.
private_dot_ssh/config.tmplis left untouched.What this does not claim
-tis a default, not a cap — an explicitssh-add -t <life>still overrides it in either direction (man ssh-agent: "A lifetime specified for an identity with ssh-add(1) overrides this value"). So this does not claim that no add path can create an unbounded oracle; it closes the forgotten and automatic ones.Step 7's "Never load with no
-t" is reworded rather than dropped: the new flag would have made the absolute form false, while the imperative is still the tighter contract.Rollout — read this part
The runbook is not chezmoi-managed (
docsis in.chezmoiignore), so this edit changes what a future setup produces and leaves an already-running box alone. The manual procedure for an existing box is included in the doc: asedon the on-diskExecStart, agrepto confirm,daemon-reload,restart. Note the restart empties the agent, so step 7 must be re-run; the socket path is unchanged, so existing shells keep working.Scope
macOS is deliberately untouched: ADR 0027 states it "governs only the headless Linux box", and there
UseKeychain yesmakes expiry a rolling silent re-add rather than a bounded window. Flagged as scope, not as fixed.Verification
Documentation-only change; no code path is exercised by the repository's test suite (
grep -rn "headless" tests/returns nothing). Thesedin the rollout procedure was checked against the real pre-change line —git log -S 'ExecStart=/usr/bin/ssh-agent'shows a single commit and one historical form, so there is no older variant the anchored pattern would silently miss, and the pattern no longer matches once-t 28800is present (idempotent).Found by a
claude-securityscan; reviewed by an independent verifier and re-challenged by a separate reviewer of the bare diff.🤖 Generated with Claude Code