Skip to content

fix(ssh): bound the systemd agent's default identity lifetime - #329

Open
toku345 wants to merge 1 commit into
mainfrom
fix/ssh-agent-bounded-lifetime
Open

fix(ssh): bound the systemd agent's default identity lifetime#329
toku345 wants to merge 1 commit into
mainfrom
fix/ssh-agent-bounded-lifetime

Conversation

@toku345

@toku345 toku345 commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary

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 — 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 28800 on the unit's ExecStart.

Why the agent and not the ssh config

The obvious alternative, AddKeysToAgent 8h in private_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 optionterminating, 1 bad configuration options, rc=255), with the abort happening before host matching, so neither the Include on line 1 nor a Match block can shield it. That file renders to ~/.ssh/config on every present and future machine, and recovery is a footgun: the file is chezmoi-managed, so a hand-edit is undone by the next chezmoi apply, and if the chezmoi source remote is SSH then chezmoi update is itself broken.

The agent default binds both add paths at once and carries no version floor. private_dot_ssh/config.tmpl is left untouched.

What this does not claim

-t is a default, not a cap — an explicit ssh-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 (docs is 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: a sed on the on-disk ExecStart, a grep to 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 yes makes 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). The sed in 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 28800 is present (idempotent).

Found by a claude-security scan; reviewed by an independent verifier and re-challenged by a separate reviewer of the bare diff.

🤖 Generated with Claude Code

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>
@toku345 toku345 self-assigned this Aug 8, 2026
@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown

Greptile Summary

The PR updates the headless Linux authentication runbook to give the systemd user ssh-agent an eight-hour default identity lifetime.

  • Adds -t 28800 to the documented agent unit.
  • Explains how the default applies to bare and automatic key additions while remaining overridable.
  • Adds a manual migration procedure for existing installations and documents the restart consequences.

Confidence Score: 5/5

The documentation change appears safe to merge.

The documented unit applies the intended eight-hour agent default, the migration matches the sole repository-supported prior unit form, and the restart and key-reload consequences are clearly stated.

Important Files Changed

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant