Skip to content

hardening: offer post-quantum hybrid key exchange in sshd - #35

Merged
TakGN merged 1 commit into
mainfrom
claude/sshd-pq-kex
Nov 11, 2026
Merged

TakGN merged 1 commit into
mainfrom
claude/sshd-pq-kex

Conversation

@TakGN

@TakGN TakGN commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator

Why

Operators on OpenSSH 10 get connection is not using a post-quantum key exchange algorithm when they ssh to the boxes. The cause is the config: roles/hardening writes /etc/ssh/sshd_config.d/99-hardening.conf with KexAlgorithms pinned to curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512, which has no post-quantum hybrid in it.

What changes

Only KexAlgorithms. The post-quantum hybrids come first and the existing three stay after them, so no current client loses access. Ciphers, MACs and everything else are unchanged.

  • New default hardening_pq_kex_algorithms: mlkem768x25519-sha256, then sntrup761x25519-sha512@openssh.com.
  • sshd refuses to start on a KexAlgorithms name it does not know, and mlkem768x25519-sha256 needs OpenSSH 9.9. Ubuntu 24.04 (the ipfs_nodes boxes) ships OpenSSH 9.6p1 (1:9.6p1-3ubuntu13.20 in noble-updates). So a new read-only task runs ssh -Q kex on the box, and only the PQ algorithms that list contains go into the config. validate: sshd -t still guards the file before it is written.

What each box ends up offering:

Hosts OS / OpenSSH PQ kex offered
sing-1, lith-1, chic-1 (ipfs_nodes) Ubuntu 24.04 / 9.6p1 sntrup761x25519-sha512@openssh.com (enough to stop the warning)
indexers-chic1 (indexers) Debian 13 / 10.0 mlkem768x25519-sha256, sntrup761x25519-sha512@openssh.com
sf, sf2 (infra_boxes) not affected cve.yml runs only tasks_from: cve_2026_98432, never main.yml

The SSH client picks the first algorithm in its list that the server supports, so an OpenSSH 10 client gets the PQ hybrid as soon as the server offers one.

mlkem on the Ubuntu boxes needs OpenSSH 9.9+, which means a release upgrade to Ubuntu 26.04 LTS (OpenSSH 10.2p1). This PR does not do that. Once a box runs a newer OpenSSH, ssh -Q kex lists mlkem and the next site.yml run adds it with no further change.

Rollout: read before approving

  • site.yml applies from CI. A merge gets a dry run on the run's summary page. The real run waits until a required reviewer approves it in the production environment. It then rolls one box at a time (production_rollout) and stops at the first box that fails or loses admin SSH access.
  • A bad sshd config can lock everyone out. The role validates with sshd -t before writing, then reloads sshd and proves a fresh SSH login before it moves to the next box. Read the dry run's 99-hardening.conf diff for each box before approving: the only line that should change is KexAlgorithms.
  • If access is lost anyway, use the break-glass path in AGENTS.md ("Break-glass access"). The break-glass key is in 1Password, Compass vault, item ID 4ghgjny7l4rawld4spedu7mxz4. Load it into a throwaway ssh-agent and ssh as ipni. The last resort is the provider's KVM console with vault_console_password. Fix the problem through the repo, not by hand.

Checks

  • python3 -m yamllint .: clean
  • python3 -m ansiblelint: Passed: 0 failure(s), 0 warning(s) ... 'production'
  • ansible-playbook --syntax-check on site, routing, bootstrap, content, k3s-upgrade, platform, cve: all ok
  • I rendered the KexAlgorithms expression locally against an OpenSSH 10.5 ssh -Q kex list (both PQ algorithms) and against a 9.6-like list (sntrup only).

Not run against the boxes: this PR has had no site.yml dry run or apply. That happens in CI after merge.

🤖 Generated with Claude Code

99-hardening.conf pinned KexAlgorithms to curve25519 and DH group16 only,
so OpenSSH 10 clients warn that the connection is not using a post-quantum
key exchange. Put mlkem768x25519-sha256 and
sntrup761x25519-sha512@openssh.com ahead of the existing list, keeping the
existing algorithms after them.

sshd refuses a KexAlgorithms name it does not know, and mlkem needs
OpenSSH 9.9 while Ubuntu 24.04 ships 9.6p1, so each PQ algorithm is offered
only where `ssh -Q kex` on the box lists it: sntrup761x25519 on the
ipfs_nodes boxes, both on indexers-chic1 (Debian 13, OpenSSH 10.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TakGN

TakGN commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

@claude review opus

@claude

claude Bot commented Nov 11, 2026

Copy link
Copy Markdown

Claude review

This change looks correct and is safe to merge. It only adds algorithms: the three classical KexAlgorithms entries stay, so no client that connects today loses access. The PQ names are filtered against ssh -Q kex on each box, so sshd never gets a name it does not know. The probe reads the client binary, but that is a reliable stand-in for sshd here: on Debian and Ubuntu, openssh-server depends on the exact same version of openssh-client, so both binaries always support the same algorithms. validate: sshd -t still runs before the file is written. The probe has check_mode: false, so the CI dry run shows the real 99-hardening.conf diff for each box. It has no when and sits in the same role and tag scope as the task that uses it, so ssh_supported_kex is always defined when the template renders, including on --tags runs. cve.yml uses tasks_from: cve_2026_98432 and does not reach this code. The version notes are accurate: sntrup761x25519 arrived in OpenSSH 8.5 and mlkem768x25519 in 9.9. Clients choose by their own preference order, so OpenSSH 10 clients will pick the PQ hybrid wherever the server offers one.

No issues found.

claude-opus-5-5 at high effort · requested by TakGN · re-run with @claude review [sonnet|opus|fable] [low|medium|high|xhigh|max]

@TakGN
TakGN merged commit 6906e36 into main Nov 11, 2026
11 checks passed
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