Skip to content

Split View: OpenSSH 10.4 정독 — 실험적 ML-DSA 복합 서명, mlkem768 기본값의 다음 단계, 그리고 운영자를 무는 폐기들

|

OpenSSH 10.4 정독 — 실험적 ML-DSA 복합 서명, mlkem768 기본값의 다음 단계, 그리고 운영자를 무는 폐기들

들어가며 — 버그픽스 릴리스처럼 보이는 전환점

OpenSSH 10.4는 2026년 7월 6일에 나왔습니다. 릴리스 노트의 자기소개는 겸손합니다 — "a number of security fixes as well as general bugfixes and a couple of new features". 대부분의 운영자는 여기까지 읽고 패키지 업데이트 큐에 넣은 뒤 넘어갈 겁니다.

그런데 그 "a couple of new features" 중 하나가 SSH 프로토콜의 다음 5년을 예고합니다. ML-DSA-44와 Ed25519를 묶은 포스트양자 복합 서명이 실험적으로 들어왔습니다. 키 교환의 포스트양자 전환이 사실상 끝난 시점에서, 이제 서명 차례가 시작된 것입니다.

어제 올린 PQ 인증서가 아직 안 오는 이유 편에서 TLS/WebPKI 쪽 이야기를 다뤘습니다 — 그쪽은 ML-DSA 서명이 너무 커서 Merkle Tree Certificates라는 우회로를 만드는 중입니다. SSH 쪽은 사정이 다르고, 그래서 다른 길을 갑니다. 이 글은 OpenSSH 10.0부터 10.4까지의 릴리스 노트를 원문으로 대조하면서 그 길을 따라가고, 중간중간 운영자를 무는 비호환 변경들을 함께 정리합니다.

먼저 타임라인부터 박아두겠습니다. 전부 공식 릴리스 노트 기준입니다.

9.0   2022-04       sntrup761x25519-sha512 기본 KEX (포스트양자 기본값의 시작)
9.9   2024-09-19    mlkem768x25519-sha256 추가
10.0  2025-04-09    mlkem768x25519-sha256이 기본 KEX로. DSA 제거. 서버 기본값에서 modp DH 제거
10.1  2025-10-06    비-PQ 키 교환 경고 도입(WarnWeakCrypto). XMSS 제거. 에이전트 소켓 /tmp 탈출
10.2  2025-10-10    긴급 버그픽스 (ControlPersist가 세션을 못 쓰게 만드는 문제, bz3872)
10.3  2026-04-02    인증서 빈 principals 의미 반전. rekey 미지원 구현 호환 코드 제거
10.4  2026-07-06    실험적 복합 서명 ssh-mldsa44-ed25519@openssh.com. seccomp 실패 치명화

키 교환 쪽 이야기 — sntrup761에서 mlkem768 기본값까지

OpenSSH의 포스트양자 페이지에 따르면, 포스트양자 키 교환이 기본값이 된 건 2022년 4월의 9.0부터입니다. 당시에는 NIST 표준화가 끝나기 전이라 NTRU Prime 계열의 sntrup761x25519-sha512를 골랐습니다. 9.9에서 NIST 표준(FIPS 203) 기반의 mlkem768x25519-sha256이 추가됐고, 10.0에서 기본값이 됐습니다.

10.0 릴리스 노트가 기본값 교체의 근거로 든 것은 네 가지입니다 — 양자 컴퓨터 공격에 안전하다고 간주되고, 기존의 curve25519-sha256보다 약해지지 않음이 보장되며, NIST가 표준화했고, 이전 기본값(sntrup761)보다 상당히 빠르다는 것("considerably faster than the previous default" — 프로젝트 자체 설명이고, 릴리스 노트에 벤치마크 수치가 따로 제시되지는 않았습니다).

여기서 "약해지지 않음이 보장"이 하이브리드 구조의 핵심입니다. mlkem768x25519-sha256은 ML-KEM-768과 고전 x25519 ECDH를 둘 다 수행하고, 두 공유 비밀을 이어붙여 해시합니다. IETF 문서(draft-ietf-sshm-mlkem-hybrid-kex, 이 글을 쓰는 시점 기준 -10 리비전이 RFC 편집자 큐에 들어가 있습니다)의 정의로는 이렇습니다.

K = HASH(K_PQ || K_CL)
  K_PQ: ML-KEM-768 캡슐화로 얻은 공유 비밀 (FIPS 203)
  K_CL: x25519 ECDH로 얻은 공유 비밀 (RFC 8731)

ML-KEM이 미래의 암호분석으로 무너져도 x25519가 남고, 양자 컴퓨터가 x25519를 부수면 ML-KEM이 막습니다. 왜 이런 보험까지 들어가며 지금 서두르냐면, "store now, decrypt later" — 지금 오가는 트래픽을 저장해 뒀다가 나중에 양자 컴퓨터로 푸는 공격 때문입니다. 키 교환이 뚫리면 세션 전체가 복호화되므로, 암호학적으로 유의미한 양자 컴퓨터(CRQC)가 없는 오늘도 키 교환만큼은 미리 바꿔야 한다는 논리입니다. OpenSSH의 PQ 페이지는 CRQC 도래 전망을 5~20년 범위로, 다수 관측자의 기대를 2030년대 중반으로 적고 있습니다.

프로토콜 수준의 배경 — KEX가 어디에 끼어들고 무엇을 서명하는지 — 은 SSH 프로토콜 딥다이브 편을 참고하세요.

10.1의 경고 — WarnWeakCrypto가 만든 운영 현실

기본값을 바꾸는 것과 생태계를 움직이는 것은 다른 문제입니다. 10.1(2025-10-06)은 더 직접적인 수단을 꺼냈습니다. 클라이언트가 포스트양자가 아닌 키 교환으로 협상을 마치면 이런 경고를 띄웁니다.

** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html

이 경고는 기본으로 켜져 있고, 새 ssh_config 옵션 WarnWeakCrypto로 제어합니다. 운영 관점에서 이 경고의 의미는 단순합니다 — 클라이언트를 10.1 이상으로 올린 순간, 조직 안의 오래된 SSH 서버 전부가 사용자 눈앞에 드러납니다. 오래된 네트워크 장비, 벤더가 방치한 어플라이언스, KexAlgorithms를 옛날 값으로 고정해 둔 골든 이미지가 전부 경고를 냅니다.

올바른 해법은 서버를 올리는 것입니다. sntrup761은 9.0부터, mlkem768은 9.9부터 지원하니 서버가 이미 그 버전대라면 설정이 KexAlgorithms로 PQ 알고리즘을 꺼 버리고 있지 않은지부터 확인하면 됩니다. 도저히 서버를 못 올리는 경우를 위해 PQ 페이지가 권하는 방식은 호스트 단위의 선별적 침묵입니다.

Match host unsafe.example.com
    WarnWeakCrypto no-pq-kex

전역으로 WarnWeakCrypto no를 박고 싶은 유혹이 있겠지만, 릴리스 노트는 이 옵션이 앞으로 다른 약한 암호 경고에도 쓰일 것이라고 예고하고 있습니다. 전역으로 끄면 미래의 경고까지 미리 끄는 셈입니다. 경고를 무시하도록 사용자를 훈련시키는 것이 최악의 결과라는 점을 생각하면, 침묵은 좁게 거는 편이 맞습니다.

10.4 — 서명 전선이 열렸다

키 교환과 달리 서명은 "store now, decrypt later"의 대상이 아닙니다. 서명 위조는 연결이 살아 있는 그 순간에 해내야 하는 일이라, 오늘 저장한 트래픽의 인증을 10년 뒤에 소급해서 깰 수는 없습니다. OpenSSH PQ 페이지도 이 점을 명시하면서, 서명 쪽의 유일한 시급성은 CRQC가 현실이 되기 전에 고전 서명 키들을 은퇴시키는 것뿐이라고 적습니다. 그래서 서명 전환은 키 교환보다 몇 년 늦게 출발해도 되는 경주였고, 실제로 그렇게 진행되고 있습니다.

전조는 10.1에 있었습니다. 실험적으로 들어 있던 해시 기반 서명 XMSS를 제거하면서 릴리스 노트에 이렇게 적었습니다 — "We expect to implement a new post-quantum signature scheme in the near future." 그 near future가 10.4입니다.

10.4draft-miller-sshm-mldsa44-ed25519-composite-sigs에 명세된 복합 서명 ssh-mldsa44-ed25519@openssh.com을 실험 지원합니다. 드래프트 저자가 OpenSSH의 Damien Miller 본인입니다. 구조는 키 교환의 하이브리드와 같은 철학입니다 — 포스트양자 ML-DSA-44와 고전 Ed25519로 같은 메시지를 각각 서명하고, 검증은 둘 다 통과해야 유효합니다. 드래프트의 보안 고려사항 그대로, 위조하려면 두 알고리즘을 모두 깨야 합니다.

M' = Prefix || Label || len(ctx) || ctx || SHA512(M)
  Prefix = "CompositeAlgorithmSignatures2025"
  Label  = "COMPSIG-MLDSA44-Ed25519-SHA512"

mldsa_sig   = ML-DSA-44.Sign(mldsa_sk, M', ctx=Label)
ed25519_sig = Ed25519.Sign(ed25519_sk, M')
composite_signature = mldsa_sig || ed25519_sig

기본값이 아니므로 쓰려면 양쪽 끝에서 HostKeyAlgorithms, PubkeyAcceptedAlgorithms 등에 직접 추가해야 하고, 키 생성은 이렇게 합니다.

ssh-keygen -t mldsa44-ed25519

크기 감각을 잡아 보면 — Ed25519는 공개키 32바이트에 서명 64바이트입니다. FIPS 204 기준 ML-DSA-44는 공개키 1,312바이트에 서명 2,420바이트입니다. 복합 공개키는 두 공개키를 이어붙인 1,344바이트이고, 복합 서명은 2,484바이트가 됩니다. 어제 다룬 TLS 쪽에서는 핸드셰이크 하나가 서명 여러 개와 공개키 여러 개를 나르기 때문에 이 크기가 감당이 안 돼 Merkle Tree Certificates라는 우회로를 파는 중인데, SSH 핸드셰이크는 호스트 키 하나와 서명 하나만 나릅니다(RFC 4253의 KEX 구조). 연결당 4KB 남짓의 추가 비용이라, SSH는 우회로 없이 알고리즘을 그대로 갈아 끼우는 정공법이 가능한 것입니다.

드래프트 -00의 현재 상태 — 숫자가 안 맞는다

여기서 "실험적"이라는 딱지를 진지하게 받아들여야 할 이유를 하나 보여드리겠습니다. 드래프트 -00(2026년 6월 2일자) 본문은 복합 서명의 크기를 이렇게 적고 있습니다.

byte[3309] mldsa_sig
byte[64]   ed25519_sig

This 3373-byte combination is henceforth referred to as
"composite_signature".

그런데 FIPS 204가 정한 ML-DSA-44 서명은 2,420바이트입니다. 3,309바이트는 한 단계 위인 ML-DSA-65의 서명 크기입니다. 실제 OpenSSH 10.4 소스(태그 V_10_4_P1의 crypto_api.h)는 MLDSA44_SIGBYTES 2420으로 FIPS 204와 일치하게 구현했으니, 드래프트 본문 쪽이 오기(誤記)로 보입니다. 같은 문서의 서명 컨텍스트 절에는 "TODO TODO TODO"가 그대로 남아 있기도 합니다.

이건 드래프트를 조롱하려는 게 아닙니다 — -00 개인 제출(individual submission) 문서란 원래 이런 상태고, 그래서 아직 워킹그룹 채택도 안 된 문서라는 사실 자체가 정보입니다. 와이어 포맷과 키 포맷이 리비전마다 바뀔 수 있는 단계라는 뜻이고, OpenSSH가 "experimental"이라고 못 박은 것과 정확히 같은 이야기를 문서 상태가 하고 있는 것입니다.

운영자를 무는 변경들 — 10.0부터 10.4까지

포스트양자 이야기에 가려 잘 안 보이지만, 이 릴리스 트레인은 폐기(deprecation)의 연속이기도 했습니다. 실패가 드러나는 방식별로 묶어 보겠습니다.

연결 시점에 실패하는 것들. 10.0이 DSA 서명 알고리즘을 완전히 제거했습니다(2015년에 기본 비활성화로 시작한 폐기 절차의 완결입니다). 같은 릴리스에서 서버 기본값의 finite field DH — diffie-hellman-group*diffie-hellman-group-exchange-* — 가 빠졌습니다. ECDH도 PQ도 못 하는 오래된 클라이언트는 10.0 이상 서버의 기본 설정에 연결하지 못합니다(클라이언트 쪽 기본값은 유지됐으므로, 새 클라이언트가 옛 서버에 붙는 방향은 계속 됩니다). 또 moduli 파일이 존재하는데 요청 범위의 값이 없을 때 컴파일된 그룹으로 조용히 폴백하던 동작도 사라져서, 손으로 편집한 moduli 파일이 이제는 연결 실패로 이어질 수 있습니다.

세션 도중에 죽는 것들. 이쪽이 디버깅하기 훨씬 고약합니다. 10.3(2026-04-02)은 rekey를 지원하지 않는 구현과의 버그 호환 코드를 제거했습니다 — 연결은 정상으로 맺어지고, 전송량이 쌓여 재키잉이 필요해지는 시점에 가서야 실패합니다. 10.4는 인증 후 재키잉 중에 KEX가 아닌 메시지를 보내는 피어를 즉시 끊도록 프로토콜을 엄격화했습니다(RFC 4253 7.1절을 안 지키는 구현이 대상입니다). 둘 다 물리는 건 최신 OpenSSH가 아니라 반대편의 레거시 임베디드 장비들입니다. "붙긴 붙는데 큰 파일만 보내면 끊겨요"라는 티켓이 올라오면 이걸 의심하세요.

자동화와 툴링이 깨지는 것들. 10.1이 에이전트 소켓을 /tmp에서 ~/.ssh/agent 밑으로 옮겼습니다. /tmp 접근이 제한된 프로세스가 에이전트 키를 못 집게 하려는 보안 개선인데, /tmp/ssh- 경로를 글롭하던 모니터링·정리 스크립트는 전부 무효가 됩니다. 부수 효과도 있습니다 — OS의 /tmp 청소가 더는 낡은 소켓을 치워 주지 않아 ssh-agent가 자체 정리 기능과 플래그들(-U, -u, -uu, 그리고 /tmp로 되돌리는 -T)을 새로 얻었고, NFS 홈을 위해 소켓 경로에 호스트명 해시가 들어갑니다. 10.4에서는 sshd -G 설정 덤프가 소문자 일색에서 대소문자 혼합("PubkeyAuthentication")으로 바뀌었습니다 — 소문자를 전제로 grep하던 컴플라이언스 스크립트가 조용히 빈 결과를 내기 시작합니다. 10.1의 IPQoS 개편으로 lowdelay, throughput, reliability 같은 ToS 키워드는 이제 무시되고 시스템 기본 QoS로 떨어집니다.

인증 동작이 바뀐 것들. 10.3의 변경 중 가장 음미할 만한 것 — 이전에는 principals 섹션이 비어 있는 인증서가 authorized_keys의 principals= 옵션 경로에서 와일드카드로, 즉 아무 principal에나 매칭되는 것으로 취급됐습니다. 의도된 동작이었지만, CA가 실수로 principals가 빈 인증서를 발급하면 쓸모없는 인증서가 되기는커녕 그 CA를 신뢰하는 모든 계정으로 로그인 가능한 만능 키가 되는 함정이었습니다. 10.3부터 빈 principals는 아무것에도 매칭되지 않습니다(TrustedUserCAKeys 경로는 원래 이 문제가 없었습니다). 반대로 이 와일드카드 동작에 기대던 환경이 있다면 업그레이드 직후 인증이 깨집니다 — 그 의존이 애초에 위험했다는 뜻이지만, 깨지는 건 깨지는 겁니다. 10.1의 ssh-add는 인증서를 에이전트에 넣을 때 만료를 인증서 만료 시각 + 5분 유예로 자동 설정하게 됐고(-N으로 비활성화), 장수 에이전트에 단명 인증서를 넣어 두고 "계속 있겠지" 하던 파이프라인은 이제 만료 후 실패합니다.

빌드와 배포 환경. 10.4부터 리눅스에서 seccomp 샌드박스나 NO_NEW_PRIVS 활성화 실패가 치명적 오류가 됩니다. 예전에는 로그만 남기고 계속 돌았지만, 이제 그런 시스템은 configure 시점에 샌드박스를 명시적으로 꺼야 합니다. 아주 오래된 커널이나 특이한 컨테이너 런타임 위에 sshd를 올리는 경우가 대상입니다.

예고된 다음 타자. 10.1과 10.2 릴리스 노트는 SHA1 SSHFP DNS 레코드의 폐기를 예고하고 있습니다 — 미래 릴리스에서 SHA1 SSHFP는 무시되고 ssh-keygen -r은 SHA256 레코드만 생성하게 됩니다. SSHFP를 DNS에 심어 둔 조직이라면 지금 재생성해 두는 게 쌉니다(SHA256 SSHFP는 2012년의 6.1부터 지원됐습니다).

덧붙여 10.4에는 업그레이드 자체의 동기가 되는 보안 수정도 여럿 있습니다 — 악의적 서버가 sftp host:/path . 다운로드를 엉뚱한 위치에 쓰게 만들 수 있던 문제, 원격 간 scp 복사에서 대상 디렉터리의 부모에 파일을 쓸 수 있던 문제, internal-sftp의 커맨드라인이 9번째 인자 이후로 조용히 잘려 보안 옵션이 버려질 수 있던 문제, GSSAPI 인증 활성화 시의 사전 인증 DoS, 재키잉 중 서버가 호스트 키를 바꿀 때의 클라이언트 use-after-free 같은 것들입니다.

하지 말아야 할 것들

복합 서명을 프로덕션에 켜지 마세요. 위에서 본 대로 명세는 -00 개인 제출 드래프트이고, 본문에 TODO와 크기 오기가 남아 있으며, OpenSSH 스스로 experimental이라고 부릅니다. 와이어/키 포맷이 바뀌면 지금 만든 키와 배포 설정이 부채가 됩니다. 랩에서 키를 만들어 보고 상호운용을 관찰하는 것까지가 지금 단계에 맞는 행동입니다.

서명 때문에 호스트 키를 지금 갈아엎지 마세요. 서명에는 소급 위협이 없다는 게 OpenSSH 자신의 설명입니다. 지금 필요한 건 PQ 호스트 키 배포가 아니라, CRQC가 가시화됐을 때 조직 전체의 호스트 키와 CA 키를 얼마나 빨리 갈 수 있는지 — 그 회전 근육입니다. 인벤토리 없이는 회전도 없습니다.

경고를 전역으로 끄지 마세요. WarnWeakCrypto는 앞으로 다른 약한 암호 경고까지 관장할 예정인 옵션입니다. Match 블록으로 호스트 단위로만 침묵시키고, 침묵시킨 호스트 목록 자체를 기술 부채 목록으로 관리하세요.

KexAlgorithms를 골든 이미지에 고정해 두고 잊지 마세요. 옛날 장비 호환을 위해 한 번 고정한 알고리즘 목록은 PQ 기본값도, 미래의 제거도 전부 비껴가게 만듭니다. 고정이 정말 필요하면 만료일을 함께 적어 두는 편이 낫습니다.

실무 체크리스트

# 1) 클라이언트가 실제 협상할 KEX 확인 — 첫 항목이 mlkem768x25519-sha256인지
ssh -G host.example.com | grep -i kexalgorithms

# 2) 설정 어딘가에서 알고리즘을 고정해 둔 곳 찾기
grep -riE 'kexalgorithms|hostkeyalgorithms|pubkeyacceptedalgorithms' /etc/ssh/

# 3) /tmp 밑 에이전트 소켓을 가정한 스크립트 찾기 (10.1에서 ~/.ssh/agent로 이동)
grep -rn 'tmp/ssh-' /opt/scripts/

# 4) sshd -G 출력을 소문자로 grep하던 곳 찾기 (10.4에서 대소문자 혼합으로 변경)
grep -rn 'sshd -G' /opt/scripts/ | xargs -r grep -l 'grep [a-z]'

# 5) 레거시 장비의 rekey 생존 테스트 — 임계값을 낮춰 강제로 재키잉 유발
ssh -o RekeyLimit=1M legacy-appliance 'dd if=/dev/zero bs=1M count=16' > /dev/null

여기에 더해, SSH 인증서를 쓰는 조직이라면 CA 발급 파이프라인이 principals가 빈 인증서를 만들 수 있는지(10.3 이전 서버가 남아 있다면 그게 만능 키가 됩니다), authorized_keys의 principals= 옵션을 쓰는 곳이 어디인지 지금 목록화해 두세요. 포스트양자 전환의 전체 그림 — SSH 바깥의 TLS, VPN, 코드 서명까지 — 은 포스트양자 암호 마이그레이션 편과 겹쳐 보면 좋습니다.

마치며

OpenSSH의 포스트양자 전환은 교과서적인 순서로 진행되고 있습니다. 소급 위협이 있는 키 교환부터 — 표준화 전에 sntrup761로 시작(9.0), 표준이 나오자 mlkem768로 교체(10.0), 경고로 생태계를 밀어붙이기(10.1) — 그리고 소급 위협이 없는 서명은 표준 문서와 실험 구현으로 이제 막 출발(10.4). 급한 것부터 강하게, 덜 급한 것은 신중하게. 이 순서 감각은 어제 본 TLS 쪽 이야기와 정확히 같은 원리로 움직이면서도, SSH의 가벼운 핸드셰이크 덕에 훨씬 단순한 경로를 탑니다.

동시에 이 릴리스 트레인은 오래된 것들을 꾸준히 치워 왔습니다. DSA, modp DH 서버 기본값, /tmp의 에이전트 소켓, rekey 못 하는 구현과의 인내심, 빈 principals의 와일드카드. 각각은 합리적인 제거지만, 물리는 시점이 연결 시, 재키잉 시, 업그레이드 직후, 스크립트 실행 시로 제각각이라 인벤토리 없이 맞으면 아픕니다. 릴리스 노트의 "Potentially-incompatible changes" 섹션을 릴리스마다 정독하는 것 — 결국 그게 이 글 전체의 요약입니다.

참고 자료

Reading OpenSSH 10.4 Closely — Experimental ML-DSA Composite Signatures, the Next Step After the mlkem768 Default, and the Deprecations That Bite Operators

Introduction — A Turning Point Dressed as a Bugfix Release

OpenSSH 10.4 shipped on July 6, 2026. The release notes introduce themselves modestly — "a number of security fixes as well as general bugfixes and a couple of new features". Most operators will read that far, drop the package into the update queue, and move on.

But one of those "a couple of new features" foreshadows the next five years of the SSH protocol. A post-quantum composite signature bundling ML-DSA-44 with Ed25519 landed, experimentally. With the post-quantum transition of key exchange effectively finished, the signature's turn has now begun.

Yesterday's post, Why PQ Certificates Aren't Here Yet, covered the TLS/WebPKI side of this story — over there, ML-DSA signatures are so large that a detour called Merkle Tree Certificates is being built. SSH's circumstances differ, and so it takes a different road. This post follows that road by working through the original release notes from OpenSSH 10.0 to 10.4, collecting along the way the incompatible changes that bite operators.

First, let me nail down the timeline. All of it comes from the official release notes.

9.0   2022-04       sntrup761x25519-sha512 becomes the default KEX (the start of post-quantum defaults)
9.9   2024-09-19    mlkem768x25519-sha256 added
10.0  2025-04-09    mlkem768x25519-sha256 becomes the default KEX. DSA removed. modp DH removed from server defaults
10.1  2025-10-06    Warning for non-PQ key exchange introduced (WarnWeakCrypto). XMSS removed. Agent socket escapes /tmp
10.2  2025-10-10    Emergency bugfix (ControlPersist making sessions unusable, bz3872)
10.3  2026-04-02    Meaning of empty certificate principals inverted. Compat code for implementations that cannot rekey removed
10.4  2026-07-06    Experimental composite signature ssh-mldsa44-ed25519@openssh.com. seccomp failure made fatal

The Key Exchange Story — From sntrup761 to the mlkem768 Default

According to OpenSSH's post-quantum page, post-quantum key exchange became the default back in 9.0, in April 2022. NIST standardization had not finished at the time, so the project picked sntrup761x25519-sha512 from the NTRU Prime family. 9.9 added mlkem768x25519-sha256, based on the NIST standard (FIPS 203), and it became the default in 10.0.

The 10.0 release notes give four grounds for the swap — it is considered safe against quantum computer attack, it is guaranteed to be no weaker than the previous curve25519-sha256, NIST standardized it, and it is considerably faster than the previous default, sntrup761 ("considerably faster than the previous default" — the project's own wording; the release notes do not present separate benchmark figures).

That "guaranteed to be no weaker" is the heart of the hybrid construction. mlkem768x25519-sha256 performs both ML-KEM-768 and classical x25519 ECDH, then concatenates and hashes the two shared secrets. The IETF document (draft-ietf-sshm-mlkem-hybrid-kex — as of this writing, revision -10 sits in the RFC editor queue) defines it like this.

K = HASH(K_PQ || K_CL)
  K_PQ: shared secret from ML-KEM-768 encapsulation (FIPS 203)
  K_CL: shared secret from x25519 ECDH (RFC 8731)

If ML-KEM collapses under future cryptanalysis, x25519 remains; if a quantum computer breaks x25519, ML-KEM holds the line. Why take out this much insurance and hurry now? Because of "store now, decrypt later" — the attack of stashing today's traffic and cracking it with a quantum computer later. Once key exchange falls, the entire session is decrypted, so the logic is that key exchange at least must be changed ahead of time, even today, with no cryptographically relevant quantum computer (CRQC) in existence. OpenSSH's PQ page puts the CRQC arrival forecast in a 5-to-20-year range, and writes that many observers expect the mid-2030s.

For protocol-level background — where KEX fits in and what it signs — see SSH Protocol Deep Dive.

The 10.1 Warning — The Operational Reality WarnWeakCrypto Created

Changing a default and moving an ecosystem are different problems. 10.1 (2025-10-06) reached for a more direct instrument. When a client finishes negotiating a non-post-quantum key exchange, it prints this warning.

** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html

The warning is on by default and controlled by a new ssh_config option, WarnWeakCrypto. Operationally, its meaning is simple — the moment you move clients to 10.1 or later, every old SSH server in your organization surfaces right in front of your users. Old network gear, appliances the vendor abandoned, golden images that pinned KexAlgorithms to some ancient value — all of them start warning.

The correct fix is to upgrade the servers. sntrup761 is supported from 9.0 and mlkem768 from 9.9, so if a server is already in that version range, start by checking whether its configuration is turning PQ algorithms off via KexAlgorithms. For servers you truly cannot upgrade, the approach the PQ page recommends is selective, per-host silence.

Match host unsafe.example.com
    WarnWeakCrypto no-pq-kex

You will be tempted to slam WarnWeakCrypto no in globally, but the release notes foretell that this option will also govern other weak-crypto warnings in the future. Turning it off globally means pre-emptively turning off future warnings too. Given that training your users to ignore warnings is the worst possible outcome, it is right to scope the silence narrowly.

10.4 — The Signature Front Opens

Unlike key exchange, signatures are not a target for "store now, decrypt later". Forging a signature is something you must pull off in the moment the connection is live, so you cannot retroactively break the authentication of traffic captured today ten years from now. The OpenSSH PQ page states this explicitly, and writes that the only urgency on the signature side is retiring classical signature keys before a CRQC becomes real. So signature migration was a race that could start a few years behind key exchange, and that is exactly how it is playing out.

The omen was in 10.1. Removing XMSS, the experimental hash-based signature scheme, the release notes said this — "We expect to implement a new post-quantum signature scheme in the near future." That near future is 10.4.

10.4 adds experimental support for ssh-mldsa44-ed25519@openssh.com, the composite signature specified in draft-miller-sshm-mldsa44-ed25519-composite-sigs. The draft's author is OpenSSH's own Damien Miller. The construction follows the same philosophy as the key exchange hybrid — post-quantum ML-DSA-44 and classical Ed25519 each sign the same message, and verification is valid only if both pass. As the draft's security considerations put it, forging requires breaking both algorithms.

M' = Prefix || Label || len(ctx) || ctx || SHA512(M)
  Prefix = "CompositeAlgorithmSignatures2025"
  Label  = "COMPSIG-MLDSA44-Ed25519-SHA512"

mldsa_sig   = ML-DSA-44.Sign(mldsa_sk, M', ctx=Label)
ed25519_sig = Ed25519.Sign(ed25519_sk, M')
composite_signature = mldsa_sig || ed25519_sig

It is not a default, so using it means adding it by hand to HostKeyAlgorithms, PubkeyAcceptedAlgorithms and friends on both ends. Key generation goes like this.

ssh-keygen -t mldsa44-ed25519

To get a feel for the sizes — Ed25519 has a 32-byte public key and a 64-byte signature. Per FIPS 204, ML-DSA-44 has a 1,312-byte public key and a 2,420-byte signature. The composite public key is the two public keys concatenated, 1,344 bytes, and the composite signature comes to 2,484 bytes. On the TLS side covered yesterday, a single handshake carries several signatures and several public keys, so this size becomes unbearable and a detour called Merkle Tree Certificates is being dug — but an SSH handshake carries just one host key and one signature (the KEX structure of RFC 4253). At roughly 4KB of extra cost per connection, SSH can take the frontal approach of swapping the algorithm in directly, no detour needed.

The Current State of Draft -00 — The Numbers Don't Add Up

Here is one reason to take the "experimental" label seriously. The body of draft -00 (dated June 2, 2026) states the composite signature's size like this.

byte[3309] mldsa_sig
byte[64]   ed25519_sig

This 3373-byte combination is henceforth referred to as
"composite_signature".

But the ML-DSA-44 signature that FIPS 204 specifies is 2,420 bytes. 3,309 bytes is the signature size of ML-DSA-65, one level up. The actual OpenSSH 10.4 source (crypto_api.h at tag V_10_4_P1) implements MLDSA44_SIGBYTES 2420, matching FIPS 204 — so the draft's body appears to be the typo. The same document's signature-context section still has a bare "TODO TODO TODO" sitting in it.

This is not meant to mock the draft — a -00 individual submission is normally in exactly this state, and the fact that the document has not even been adopted by a working group yet is itself information. It means the wire format and key format can still change from revision to revision, and the document's status is telling you precisely the same thing OpenSSH pinned down with the word "experimental".

The Changes That Bite Operators — 10.0 Through 10.4

Obscured by the post-quantum story, this release train has also been a run of deprecations. Let me group them by how the failure reveals itself.

The ones that fail at connection time. 10.0 removed the DSA signature algorithm entirely (completing a deprecation process that began with disabling it by default in 2015). The same release dropped finite field DH — diffie-hellman-group* and diffie-hellman-group-exchange-* — from the server defaults. An old client that can do neither ECDH nor PQ cannot connect to the default configuration of a 10.0-or-later server (the client-side defaults were kept, so a new client reaching an old server still works). Also, the behavior of silently falling back to compiled-in groups when a moduli file exists but holds no value in the requested range is gone, so a hand-edited moduli file can now lead to connection failure.

The ones that die mid-session. These are far nastier to debug. 10.3 (2026-04-02) removed bug-compatibility code for implementations that do not support rekeying — the connection establishes normally, and only fails once enough traffic has accumulated to require a rekey. 10.4 tightened the protocol to immediately disconnect a peer that sends non-KEX messages during a post-authentication rekey (the target being implementations that do not follow RFC 4253 section 7.1). What both bite is not the modern OpenSSH but the legacy embedded gear on the other end. When a ticket comes in reading "it connects fine, but it drops whenever I send a big file", suspect this.

The ones that break automation and tooling. 10.1 moved the agent socket from /tmp to under ~/.ssh/agent. It is a security improvement meant to stop processes with restricted /tmp access from grabbing agent keys, but every monitoring and cleanup script globbing the /tmp/ssh- path is invalidated. There are side effects too — the OS's /tmp cleaning no longer sweeps away stale sockets, so ssh-agent gained its own cleanup feature and flags (-U, -u, -uu, and -T to revert to /tmp), and a hostname hash goes into the socket path for NFS homes. In 10.4, the sshd -G config dump changed from all-lowercase to mixed case ("PubkeyAuthentication") — compliance scripts that grep assuming lowercase quietly start returning empty results. The 10.1 IPQoS rework means ToS keywords like lowdelay, throughput, and reliability are now ignored and fall back to the system default QoS.

The ones where authentication behavior changed. The most savorable of 10.3's changes — previously, a certificate with an empty principals section was treated as a wildcard along the principals= option path in authorized_keys, that is, as matching any principal. It was the intended behavior, but it was a trap: if a CA accidentally issued a certificate with empty principals, far from being a useless certificate, it became a skeleton key able to log into every account trusting that CA. From 10.3, empty principals match nothing (the TrustedUserCAKeys path never had this problem). Conversely, if you have an environment leaning on that wildcard behavior, authentication breaks right after the upgrade — which means the dependency was dangerous to begin with, but broken is broken. 10.1's ssh-add now automatically sets an expiry when adding a certificate to the agent, at the certificate's expiry time plus a 5-minute grace period (disable with -N), and pipelines that parked short-lived certificates in a long-lived agent assuming "it'll still be there" now fail after expiry.

Build and deployment environments. From 10.4, failure to enable the seccomp sandbox or NO_NEW_PRIVS on Linux is a fatal error. It used to just log and carry on; now such systems must explicitly disable the sandbox at configure time. The targets here are cases of running sshd on very old kernels or unusual container runtimes.

The next one up, already announced. The 10.1 and 10.2 release notes foretell the deprecation of SHA1 SSHFP DNS records — in a future release SHA1 SSHFP will be ignored, and ssh-keygen -r will generate only SHA256 records. If your organization has planted SSHFP in DNS, regenerating now is cheap (SHA256 SSHFP has been supported since 6.1, in 2012).

On top of that, 10.4 has several security fixes that are themselves a motive to upgrade — an issue where a malicious server could make an sftp host:/path . download write to the wrong location, an issue where a remote-to-remote scp copy could write files into the parent of the destination directory, an issue where internal-sftp's command line was silently truncated after the ninth argument so security options could be discarded, a pre-authentication DoS when GSSAPI authentication is enabled, and a client use-after-free when a server changes host keys during a rekey.

What Not to Do

Don't turn composite signatures on in production. As seen above, the specification is a -00 individual submission draft, its body still has a TODO and a size typo, and OpenSSH itself calls it experimental. If the wire or key format changes, the keys and deployment configuration you build today become debt. Generating a key in a lab and observing interoperability — that is the behavior that fits this stage.

Don't tear up your host keys now over signatures. That signatures carry no retroactive threat is OpenSSH's own explanation. What you need now is not a PQ host key rollout but the rotation muscle — how fast you could turn over the host keys and CA keys of the whole organization once a CRQC comes into view. Without an inventory there is no rotation.

Don't turn the warnings off globally. WarnWeakCrypto is an option slated to govern other weak-crypto warnings going forward. Silence it only per host with a Match block, and manage the list of silenced hosts as a technical debt list in its own right.

Don't pin KexAlgorithms into a golden image and forget it. An algorithm list pinned once for compatibility with old gear makes PQ defaults — and future removals — bypass you entirely. If pinning is truly necessary, it is better to write an expiry date alongside it.

A Practical Checklist

# 1) Check the KEX the client will actually negotiate — is the first entry mlkem768x25519-sha256?
ssh -G host.example.com | grep -i kexalgorithms

# 2) Find anywhere in the config that pins algorithms
grep -riE 'kexalgorithms|hostkeyalgorithms|pubkeyacceptedalgorithms' /etc/ssh/

# 3) Find scripts assuming an agent socket under /tmp (moved to ~/.ssh/agent in 10.1)
grep -rn 'tmp/ssh-' /opt/scripts/

# 4) Find places grepping sshd -G output in lowercase (changed to mixed case in 10.4)
grep -rn 'sshd -G' /opt/scripts/ | xargs -r grep -l 'grep [a-z]'

# 5) Rekey survival test for legacy gear — lower the threshold to force a rekey
ssh -o RekeyLimit=1M legacy-appliance 'dd if=/dev/zero bs=1M count=16' > /dev/null

Beyond this, if your organization uses SSH certificates, inventory now whether your CA issuance pipeline can produce certificates with empty principals (if any pre-10.3 servers remain, that becomes a skeleton key) and where the principals= option in authorized_keys is used. For the whole picture of the post-quantum transition — TLS, VPN, and code signing beyond SSH — it is worth overlaying Post-Quantum Cryptography Migration: The USD 7B Race Against Q-Day.

Closing

OpenSSH's post-quantum transition is proceeding in textbook order. Key exchange first, where the retroactive threat lives — start with sntrup761 before standardization (9.0), swap to mlkem768 once the standard arrives (10.0), push the ecosystem with warnings (10.1) — and then signatures, which carry no retroactive threat, only now setting off with a standards document and an experimental implementation (10.4). Hard on the urgent, careful with the less urgent. This sense of ordering runs on exactly the same principle as the TLS story from yesterday, while taking a far simpler path thanks to SSH's light handshake.

At the same time, this release train has steadily cleared away old things. DSA, modp DH in server defaults, the agent socket in /tmp, patience with implementations that cannot rekey, the wildcard of empty principals. Each removal is reasonable, but the moment each one bites is different — at connect time, at rekey time, right after an upgrade, at script runtime — so being hit without an inventory hurts. Reading the "Potentially-incompatible changes" section of the release notes closely, every release — in the end, that is the summary of this entire post.

References