SSH (Secure Shell) is a protocol for secure remote login and other services over an untrusted network. It stacks three protocols on one TCP connection, usually port 22: a transport layer that authenticates the server and encrypts traffic, a user authentication layer, and a connection layer that multiplexes shells, file transfer and forwarding into channels.
The core standards, RFC 4250 to RFC 4254, date from January 2006, but almost every algorithm inside them has since been replaced. This post walks through each layer on the wire, follows that history to OpenSSH 10.6 (6 October 2026), explains the weakness classes as concepts, and gives the commands a defender uses to check their own servers. Protocol facts come from the RFCs, the IANA registries and the OpenSSH project's own pages and manuals.
Key takeaways
- SSH is defined by RFC 4250 to RFC 4254 (January 2006), updated by RFC 8308, RFC 8332, RFC 8709 and RFC 9142, among others.
- The transport runs a negotiated key exchange signed by the server's host key and derives separate keys per direction. The first exchange hash becomes the session identifier, and user signatures are bound to it, so they cannot be replayed elsewhere.
- SSH is only as strong as the client's host key check. RFC 4251 allows unverified first connections and says they are open to an active man in the middle. Pin keys with a managed known hosts file, certificates or DNSSEC-signed SSHFP records (RFC 4255).
- Port and agent forwarding are channel types that need their own policy; RFC 9987 (May 2026) says agents should not be forwarded by default.
- OpenSSH has offered a post-quantum hybrid key exchange by default since 9.0 (April 2022), switched the default to mlkem768x25519-sha256 in 10.0 (April 2025), and since 10.1 warns when a connection falls back to a non post-quantum exchange.
- Check what you offer with
ssh -Q,ssh -Gandsshd -T, and keep the SSH libraries inside your software and images patched.
What SSH is for: the layer and the standard
SSH replaced cleartext remote login tools. RFC 4251 describes it as "a protocol for secure remote login and other secure network services over an insecure network" and contrasts it with telnet and rlogin, which sent passwords in the clear. Today the same protocol carries interactive shells, remote command execution for automation and CI, file transfer through the SFTP subsystem, Git over SSH, and tunnels for other TCP services.
SSH is an application layer protocol that runs over a reliable byte stream, which in practice means TCP. RFC 4253, section 4.1, says that when used over TCP/IP the server normally listens on port 22 and that the port is registered with IANA. The IANA service name registry lists ssh on port 22 for TCP, UDP and SCTP, referencing RFC 4251, but real deployments use TCP.
The five core RFCs
| RFC | Title | What it defines |
|---|---|---|
| RFC 4250 | SSH Protocol Assigned Numbers | Message numbers, disconnect codes and the IANA registries for algorithm and method names |
| RFC 4251 | SSH Protocol Architecture | The three layer design, data types, host key trust models, naming and the main Security Considerations |
| RFC 4253 | SSH Transport Layer Protocol | Version exchange, binary packet format, algorithm negotiation, Diffie-Hellman key exchange, key derivation and rekeying |
| RFC 4252 | SSH Authentication Protocol | The authentication framework and the publickey, password and hostbased methods |
| RFC 4254 | SSH Connection Protocol | Channels, flow control, sessions, X11 forwarding, environment passing and TCP/IP port forwarding |
All five were published in January 2006 as Standards Track RFCs by the IETF secsh working group. They did not obsolete earlier RFCs, because the first version of the protocol, SSH-1, was never standardised. RFC 4253, section 5, notes that earlier versions "have not been formally documented" and that the only documentation of 1.x was the README files shipped with the original source code.
The RFCs that update them
| RFC | Date | Updates | What changed |
|---|---|---|---|
| RFC 4255 | January 2006 | (new record type) | SSHFP DNS record for publishing host key fingerprints |
| RFC 5656 | December 2009 | (new algorithms) | Elliptic curve key exchange and ECDSA host keys |
| RFC 8308 | March 2018 | RFC 4251, 4252, 4253, 4254 | Extension negotiation (SSH_MSG_EXT_INFO) |
| RFC 8332 | March 2018 | RFC 4252, RFC 4253 | RSA signatures with SHA-256 and SHA-512 |
| RFC 8709 | February 2020 | RFC 4253 | Ed25519 and Ed448 public key algorithms |
| RFC 8731 | February 2020 | (new method) | curve25519-sha256 and curve448-sha512 key exchange |
| RFC 9142 | January 2022 | RFC 4250, 4253, 4432, 4462 | Updated MUST, SHOULD and MUST NOT list for key exchange methods |
| RFC 9941 | April 2026 | (Informational) | sntrup761x25519-sha512 hybrid key exchange |
| RFC 9987 | May 2026 | (Standards Track) | The SSH agent protocol, including agent forwarding |
| RFC 10042 | August 2026 | (Informational) | ML-KEM hybrid key exchange, including mlkem768x25519-sha256 |
The IANA "Secure Shell (SSH) Protocol Parameters" registry, last updated 8 September 2026 when fetched for this post, is the authoritative list of algorithm names and their implementation recommendations.
The three layers: transport, user authentication and connection
Almost every SSH security property follows from the order of the layers in RFC 4251.
- Transport layer (RFC 4253). Server authentication, confidentiality, integrity and optional compression, directly on TCP.
- User authentication layer (RFC 4252). Authenticates the user. It runs over the transport, so credentials are encrypted and the server is already authenticated when they are sent.
- Connection layer (RFC 4254). Multiplexes the tunnel into channels. Nothing in it is available until the user is authenticated.
After key exchange the client sends a service request for ssh-userauth, and each authentication request names ssh-connection as the service to start next. Message numbers are split by layer, which makes captures easy to read: 1 to 49 belong to the transport (KEXINIT is 20, NEWKEYS 21, EXT_INFO 7), 50 to 79 to user authentication, 80 to 127 to the connection protocol (CHANNEL_OPEN is 90), and 192 to 255 are for local extensions.
How SSH works on the wire
The full connection sequence
The diagram below follows a typical OpenSSH connection from TCP setup to an interactive shell. Message names are from RFC 4253, RFC 4252, RFC 4254 and RFC 8308.
Client Server
|------------------- TCP SYN to port 22 -------------->|
|<------------------ TCP handshake completes --------->|
|<----- "SSH-2.0-<software> <comments>" CR LF ---------| identification strings
|------ "SSH-2.0-<software>" CR LF ------------------->| (sent in clear)
|------ SSH_MSG_KEXINIT (cookie, algorithm lists) ---->| algorithm negotiation
|<----- SSH_MSG_KEXINIT (cookie, algorithm lists) -----| (still in clear)
|------ key exchange init (client ephemeral) -------->| messages 30 to 49
|<----- key exchange reply (host key, server ---|
| ephemeral, signature over exchange hash H) |
| client verifies host key against known_hosts, |
| SSHFP or a CA, then checks the signature on H |
|------ SSH_MSG_NEWKEYS ----------------------------->|
|<----- SSH_MSG_NEWKEYS ------------------------------| encryption starts here
|<----- SSH_MSG_EXT_INFO (server-sig-algs, ...) ------| RFC 8308, optional
|------ SERVICE_REQUEST "ssh-userauth" -------------->|
|<----- SERVICE_ACCEPT -------------------------------|
|------ USERAUTH_REQUEST user, "ssh-connection", --->|
| "publickey", signature over session id |
|<----- USERAUTH_SUCCESS -----------------------------|
|------ CHANNEL_OPEN "session", window, max packet -->|
|<----- CHANNEL_OPEN_CONFIRMATION --------------------|
|------ CHANNEL_REQUEST "pty-req" ------------------->|
|------ CHANNEL_REQUEST "shell" --------------------->|
|<====> CHANNEL_DATA / WINDOW_ADJUST (both ways) <===>|
|------ CHANNEL_EOF, CHANNEL_CLOSE ------------------>|
|<----- CHANNEL_CLOSE --------------------------------|
|------ SSH_MSG_DISCONNECT -------------------------->|
Step 1: the version exchange
RFC 4253, section 4.2, requires both sides to send an identification string of the form SSH-protoversion-softwareversion SP comments CR LF, at most 255 characters. For this protocol the version must be 2.0. A server may send other lines first, which must not begin with SSH-; that allowance exists so TCP wrappers can show an error before disconnecting.
The identification strings are sent before any encryption, so anyone on the path can read the software name and version. They are also included in the key exchange hash, so a man in the middle who edits them breaks the exchange. A server that still announces the 1.99 compatibility version is running very old software.
Step 2: the binary packet protocol
Every message after the identification strings uses the binary packet format from RFC 4253, section 6:
| Field | Size | Meaning |
|---|---|---|
| packet_length | uint32 | Length of the packet in bytes, excluding the MAC and this field |
| padding_length | byte | Length of the random padding |
| payload | packet_length minus padding_length minus 1 | The message, compressed if compression was negotiated |
| random padding | 4 to 255 bytes | Random bytes so the total is a multiple of the cipher block size or 8; at least 4 bytes |
| mac | depends on algorithm | Message authentication code, sent last and not encrypted |
The MAC covers an implicit sequence number plus the unencrypted packet. The number starts at zero, rises with every packet and is never sent, so a deleted, replayed or reordered packet fails the check. RFC 4253 never resets it, even on rekeying, a detail that mattered in 2023 (see Terrapin below).
With encrypt-then-MAC modes (the -etm@openssh.com names) the MAC covers the ciphertext instead, and the OpenSSH sshd_config(5) manual says these "are considered safer and their use recommended".
Step 3: algorithm negotiation with KEXINIT
Each side sends SSH_MSG_KEXINIT. Its fields, from RFC 4253, section 7.1, are:
| KEXINIT field | Purpose |
|---|---|
| cookie (16 random bytes) | Ensures neither side alone controls the keys and session identifier |
| kex_algorithms | Key exchange methods in preference order; also carries the ext-info and strict kex markers |
| server_host_key_algorithms | Host key signature algorithms the client accepts or the server holds keys for |
| encryption_algorithms, client to server and server to client | Ciphers for each direction |
| mac_algorithms, both directions | Integrity algorithms for each direction |
| compression_algorithms, both directions | none or zlib variants |
| languages, both directions | Normally empty |
| first_kex_packet_follows | Whether a guessed key exchange packet is sent immediately |
| reserved uint32 | Zero, for future extension |
Choice is client driven: in each category the winner is the first algorithm in the client's list that the server also supports. If any category has no match, both sides disconnect. The server's list order does not matter, as the sshd_config(5) manual notes for KexAlgorithms.
Both KEXINIT messages are included in the exchange hash, so an attacker who strips a strong algorithm from one list changes the hash and the server's signature fails. What remains is honest negotiation of weak algorithms that both sides still list, so the fix is to stop offering them.
Step 4: key exchange, host key verification and the exchange hash
In the Diffie-Hellman exchange of RFC 4253, section 8, the client sends its public value e, and the server replies with its host public key K_S, its value f and a signature over the exchange hash H. Modern methods, including curve25519-sha256 and the post-quantum hybrids, keep that shape. H binds everything both sides said:
H = HASH( V_C || V_S || I_C || I_S || K_S || e || f || K )
V_C, V_S client and server identification strings
I_C, I_S client and server KEXINIT payloads
K_S server public host key
e, f client and server key exchange values
K shared secret
The client makes two checks. The signature on H proves the server holds the private key for K_S. Whether K_S belongs to the intended host is a separate question the protocol cannot answer alone. RFC 4251 offers two trust models, a local name to key database (known_hosts) or a certification authority, and warns that skipping the check on first connection leaves the session open to an active man in the middle.
Step 5: key derivation, NEWKEYS and rekeying
The exchange produces the shared secret K and the hash H. The H from the first exchange becomes the session identifier and never changes for the life of the connection. Six values are derived with the negotiated hash, each with a different single letter label, so every direction gets its own key and IV:
Initial IV, client to server HASH(K || H || "A" || session_id)
Initial IV, server to client HASH(K || H || "B" || session_id)
Encryption key, client to server HASH(K || H || "C" || session_id)
Encryption key, server to client HASH(K || H || "D" || session_id)
Integrity key, client to server HASH(K || H || "E" || session_id)
Integrity key, server to client HASH(K || H || "F" || session_id)
Each side then sends SSH_MSG_NEWKEYS under the old keys, and every later message uses the new ones. Either side can start a new key exchange at any time by sending another KEXINIT. RFC 4253 recommends rekeying after each gigabyte of data or each hour of connection time, whichever comes first, and RFC 4251 adds that peers must rekey before the 32-bit sequence number wraps.
Step 6: extension negotiation (RFC 8308)
RFC 8308 lets each side announce capabilities after keys are in place. The client adds the marker ext-info-c and the server ext-info-s to their kex_algorithms lists; the markers never match, so they cannot be selected as a method. A peer that saw the marker may send SSH_MSG_EXT_INFO (message 7) after NEWKEYS. Its key extension, server-sig-algs, tells the client which signature algorithms the server accepts, so a client can use rsa-sha2-256 with an existing RSA key. OpenSSH supports ext-info-c since 7.2 and ext-info-s since 9.6.
The user authentication layer (RFC 4252)
Authentication is server driven. The client sends SSH_MSG_USERAUTH_REQUEST with a user name, the service to start (ssh-connection) and a method. The server answers USERAUTH_FAILURE, listing the methods that can continue and a partial success flag, or USERAUTH_SUCCESS once every required method is done.
The defined methods are:
| Method | RFC | Status in RFC 4252 | How it proves identity |
|---|---|---|---|
| publickey | RFC 4252 | REQUIRED | Signature with the user's private key over data that includes the session identifier |
| password | RFC 4252 | OPTIONAL | Cleartext password sent inside the encrypted transport |
| hostbased | RFC 4252 | OPTIONAL | The client host signs on behalf of the user; trusts the client machine |
| keyboard-interactive | RFC 4256 | (separate RFC) | Server driven prompts, often used for one time codes |
| GSS-API methods | RFC 4462 | (separate RFC) | Kerberos through GSS-API; OpenSSH implements only the authentication part |
| none | RFC 4252 | NOT RECOMMENDED | Used to query methods; accepted only if no authentication is required |
The public key signature covers the session identifier, user name, service, algorithm and key, so RFC 4251 notes it "cannot be successfully replayed in other sessions". A password has no such binding: a compromised server, or an undetected man in the middle, learns a reusable credential.
RFC 4252 recommends a 10 minute authentication timeout and at most 20 failed attempts per connection, and lets a server answer for a non existent user with a plausible method list so it does not reveal which accounts exist. OpenSSH is stricter: LoginGraceTime defaults to 120 seconds and MaxAuthTries to 6.
The connection layer (RFC 4254): channels
Once authenticated, everything happens in channels. Either side opens one with SSH_MSG_CHANNEL_OPEN:
| CHANNEL_OPEN field | Meaning |
|---|---|
| channel type | session, x11, forwarded-tcpip, direct-tcpip, or an extension name |
| sender channel | The sender's local identifier for this channel |
| initial window size | How many bytes the peer may send before waiting for a window adjust |
| maximum packet size | The largest data packet the sender will accept |
| type specific data | For example the host and port to connect to |
Flow control is per channel: the receiver grants space with CHANNEL_WINDOW_ADJUST, so a bulk copy cannot starve a shell on the same connection. Data travels in CHANNEL_DATA, stderr in CHANNEL_EXTENDED_DATA, and a channel ends with CHANNEL_EOF and CHANNEL_CLOSE.
A session channel is shaped by channel requests: pty-req, env, x11-req, then one of shell, exec or subsystem (for example sftp). RFC 4254 flags env as a hazard in privileged processes and recommends an allow list, which OpenSSH implements as AcceptEnv.
How port forwarding works
Port forwarding is two channel types and one global request:
- Local forwarding (
ssh -L). The client listens locally and, per connection, opens adirect-tcpipchannel naming a destination; the server connects to it from its own network position. - Remote forwarding (
ssh -R). Atcpip-forwardglobal request asks the server to listen; each arriving connection becomes aforwarded-tcpipchannel back to the client, which must reject ports it never asked for. - Dynamic forwarding (
ssh -D). The client runs a SOCKS proxy locally and opens adirect-tcpipchannel per destination.
RFC 4254 says clients SHOULD reject direct-tcpip opens sent by a server. The bind address matters: localhost means loopback only, 0.0.0.0 or :: mean every interface, and OpenSSH's GatewayPorts default of no keeps remote forwardings on loopback.
How agent forwarding works
Agent forwarding, long an OpenSSH extension, was standardised as RFC 9987 in May 2026. The client sends an auth-agent-req@openssh.com (or advertised agent-req) request on a session; the server creates a user private socket and sets SSH_AUTH_SOCK; signature requests travel back over a channel to the client's agent.
The key cannot be copied this way, but it can be used. RFC 9987 says forwarding "inherently creates a transitive trust relationship" and should not be on by default. The ssh(1) manual warns that users who can bypass file permissions on the remote host can "perform operations on the keys", and suggests a jump host (-J) instead. Our post on whether SSH agent forwarding is safe covers the options in more depth.
Versions and history: from SSH-1 to post-quantum key exchange
SSH-1 and the move to SSH-2
SSH-1 was never written up as an RFC, and SSH-2 is a separate, incompatible protocol, which is why RFC 4253 defines the 1.99 compatibility version for the transition. OpenSSH's release notes record that 7.0 (11 August 2015) disabled SSH-1 at compile time, the 1024-bit diffie-hellman-group1-sha1 exchange and ssh-dss (DSA) keys by default; 7.6 (3 October 2017) deleted SSH-1 from the client; and 10.0 (9 April 2025) removed DSA entirely.
Algorithm updates that matter
RSA with SHA-2 (RFC 8332, March 2018). The original ssh-rsa signature uses SHA-1. RFC 8332 defines rsa-sha2-256 (RECOMMENDED) and rsa-sha2-512 (OPTIONAL) for the same keys, supported in OpenSSH since 7.2 (29 February 2016). OpenSSH 8.8 (26 September 2021) disabled SHA-1 RSA signatures by default because chosen-prefix collisions had become cheap. Existing RSA keys keep working with SHA-2: an "ssh-rsa" key is fine, an "ssh-rsa" signature is not.
Ed25519 (RFC 8709, February 2020). RFC 8709 formalises ssh-ed25519 and ssh-ed448. OpenSSH added Ed25519 in 6.5 and, per its specifications page, implements only the Ed25519 half. For key choice guidance see Ed25519 vs RSA SSH keys for developers.
Key exchange recommendations (RFC 9142, January 2022). RFC 9142 replaced the 2006 list: diffie-hellman-group1-sha1 drops from MUST to SHOULD NOT, diffie-hellman-group14-sha1 to MAY, diffie-hellman-group14-sha256 becomes the MUST, curve25519-sha256 and NIST curve ECDH are SHOULD, diffie-hellman-group-exchange-sha1 is SHOULD NOT and rsa1024-sha1 is MUST NOT.
Hybrid post-quantum key exchange. The OpenSSH post-quantum page warns that sessions recorded today could be decrypted later by a large enough quantum computer, which it calls "store now, decrypt later". The history:
| Version | Date | Change |
|---|---|---|
| OpenSSH 8.9 | 23 Feb 2022 | Adds sntrup761x25519-sha512@openssh.com (Streamlined NTRU Prime plus X25519) to the default list, after the ECDH methods |
| OpenSSH 9.0 | 8 Apr 2022 | Makes sntrup761x25519-sha512@openssh.com the default key exchange |
| OpenSSH 9.9 | 19 Sep 2024 | Adds mlkem768x25519-sha256 (FIPS 203 ML-KEM plus X25519), available by default; adds sntrup761x25519-sha512 without the vendor suffix |
| OpenSSH 10.0 | 9 Apr 2025 | Makes mlkem768x25519-sha256 the default; prefers AES-GCM over AES-CTR |
| OpenSSH 10.1 | 6 Oct 2025 | Client warns when a connection uses a non post-quantum key exchange; WarnWeakCrypto option |
| OpenSSH 10.4 | 6 Jul 2026 | Experimental ML-DSA 44 plus Ed25519 composite signature scheme, off by default |
| OpenSSH 10.6 | 6 Oct 2026 | Enables the ssh-mldsa44-ed25519 signature algorithm under its new name; adds WarnWeakCrypto to sshd |
Both hybrids pair a post-quantum scheme with X25519, so the result is "no worse than the previous best classical algorithm", in the OpenSSH page's words. The specifications are now Informational RFCs: RFC 9941 (April 2026) and RFC 10042 (August 2026). IANA lists the ML-KEM hybrids and sntrup761x25519-sha512 as SHOULD implement.
Signatures carry no "store now, decrypt later" risk, the OpenSSH page notes; classical host and user keys just need retiring before a capable quantum computer exists, which the ML-DSA composite work in 10.4 and 10.6 starts.
Other changes in recent OpenSSH releases
OpenSSH 9.8 (1 July 2024) split out a per session sshd-session binary and turned on PerSourcePenalties, which temporarily refuses sources that cause failed logins, invalid user attempts or crashes. OpenSSH 10.0 moved authentication into a separate sshd-auth binary. OpenSSH 10.1 moved agent sockets from /tmp to ~/.ssh/agent and announced that a future release will ignore SHA-1 SSHFP records. OpenSSH 10.6 disabled the LZ77 dictionary coder in compression after researchers showed that a shared compression dictionary across channels can leak plaintext from one channel to an attacker who controls another; the release notes say this makes the Compression option less effective and recommend compressing at the application layer instead.
The 10.6 notes also promise more frequent releases, because many security reports now arrive with AI assistance.
Security properties and weakness classes
What SSH guarantees, and what it does not
| Property | Provided? | Notes from the RFCs |
|---|---|---|
| Confidentiality | Yes, after NEWKEYS | Version strings and algorithm lists are visible |
| Integrity | Yes, unless the none MAC is used | Sequence numbered MAC detects changes, deletion and reordering |
| Server authentication | Only if the client verifies the host key | RFC 4251 calls unverified use "inherently insecure" against active attackers |
| Client authentication | Yes | Public key signatures are bound to the session identifier |
| Replay protection | Yes | Sequence numbers within, session identifier across sessions |
| Forward secrecy | Yes with Diffie-Hellman style exchanges | Past sessions survive a later host key leak |
| Availability | No | Key exchange is costly and precedes authentication |
| Traffic analysis resistance | Partial | Sizes and timing can reveal, for example, password length |
| Endpoint security | No | A compromised server exposes every session through it |
Trust on first use and host key substitution
The largest practical weakness is not cryptographic. On a first connection the client has no stored key to compare. RFC 4251 describes the cases: an attacker on the path presents its own key and relays everything if the user accepts it; an attacker persuades users to trust a wrong key through a side channel; or an attacker tampers with an established session, which the MAC prevents unless the MAC algorithm is broken.
OpenSSH's StrictHostKeyChecking default, ask, prompts for new keys and refuses changed ones; accept-new adds new keys silently; no accepts changed keys too. The real fix is a trustworthy key before the first connection: a managed ssh_known_hosts file, host certificates or DNSSEC-signed SSHFP records. RFC 4254 adds that agent, X11 and TCP forwarding should be disabled if a host key changes without explanation.
Downgrade and prefix truncation
KEXINIT lists are signed, but weak algorithms both sides still list can be chosen honestly. A subtler gap surfaced in December 2023: researchers showed that an attacker on the path could insert messages before encryption started and delete the same number of messages just after, because the unencrypted handshake and the sequence numbers were not fully protected. CVE-2023-48795 describes it as letting "some packets" be omitted so that "some security features" are downgraded. OpenSSH's 9.6 release notes call the impact "very limited": the main identified effect was deleting the EXT_INFO message, which could disable some keystroke timing protections. The fix is the "strict KEX" extension, which ends the connection on any unexpected message during key exchange and resets the sequence numbers after each key exchange. It only applies when both sides support it, so old peers keep the gap. Our Terrapin explainer covers the mechanism and the affected cipher modes.
Agent forwarding abuse
A forwarded agent lets anyone with enough access on the remote host sign as you while you are connected. RFC 9987 defines key lifetime and confirmation constraints that narrow this, but the structural fix is ProxyJump instead of forwarding. Server side, AllowAgentForwarding defaults to yes, and the manual says disabling it "does not improve security unless users are also denied shell access".
Port forwarding as a policy bypass
RFC 4251 warns that forwarding can tunnel other protocols "undetectably" through a firewall, in either direction, so an SSH account is also network access from the server's position. PermitOpen, PermitListen and DisableForwarding narrow it.
Weak and legacy algorithms
Retire SHA-1 key exchange and signatures, 1024-bit Diffie-Hellman, DSA keys, CBC mode and 3DES ciphers, and MD5 MACs. RFC 4251 already flagged CBC mode's theoretical exposure to chosen ciphertext attacks in 2006. Every algorithm you offer is one negotiation might pick with an old peer.
Denial of service before authentication
RFC 4251 notes that key exchange is expensive and happens before authentication. OpenSSH's MaxStartups (default 10:30:100) drops unauthenticated connections as they pile up, and PerSourcePenalties refuses misbehaving sources, but limiting exposure is the real control.
Implementation bugs
Most SSH incidents are code bugs, not protocol flaws. The OpenSSH 9.8 release notes describe a critical sshd race condition in Portable OpenSSH 8.5p1 to 9.7p1 that could allow code execution as root. SSH libraries in applications and devices carry the same class of risk.
Secure configuration and how to check it
Only run the commands below against systems you own or are authorised to test. Every command and option comes from the OpenSSH manual pages linked in the sources.
Check what your client and server support
ssh -Q lists the algorithms a build supports. The ssh(1) manual lists the query terms, including cipher, cipher-auth, mac, kex, key, key-sig, sig and protocol-version:
ssh -Q kex
ssh -Q cipher
ssh -Q mac
ssh -Q key-sig
ssh -Q protocol-version
That is capability, not configuration. For the effective settings use ssh -G on the client and sshd -T on the server; sshd -T -C takes connection parameters (user, host, addr and others) to show what a Match block does:
ssh -G host.example.com | grep -Ei '^(kexalgorithms|ciphers|macs|hostkeyalgorithms|stricthostkeychecking|forwardagent)'
sudo sshd -T | grep -Ei '^(kexalgorithms|ciphers|macs|passwordauthentication|permitrootlogin|allowagentforwarding|allowtcpforwarding|gatewayports|x11forwarding|maxauthtries|logingracetime)'
sudo sshd -T -C user=deploy,host=ci.example.com,addr=192.0.2.10
Algorithm settings
The current sshd_config(5) manual gives this default for KexAlgorithms, which already puts the post-quantum hybrids first:
mlkem768x25519-sha256,
sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,
curve25519-sha256,curve25519-sha256@libssh.org,
ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521
and this default for Ciphers:
chacha20-poly1305@openssh.com,
aes128-gcm@openssh.com,aes256-gcm@openssh.com,
aes128-ctr,aes192-ctr,aes256-ctr
On a current OpenSSH, leaving these defaults alone is usually safest. Trouble comes from old hand written lists that silently opt out of the hybrids. To restrict, remove items from the default with a leading - instead of replacing the list:
# /etc/ssh/sshd_config (illustrative; check each name with ssh -Q first)
KexAlgorithms -diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com
Then confirm with sshd -T. If an OpenSSH 10.1 or later client warns that the key exchange is not post-quantum, the server is old or has a restrictive KexAlgorithms list.
Authentication and access
| Setting (sshd_config) | Default per the manual | Hardened value | Why |
|---|---|---|---|
| PasswordAuthentication | yes | no, once keys or certificates work | Removes guessable, reusable credentials |
| PermitRootLogin | prohibit-password | no | Forces named accounts and an audit trail |
| AuthenticationMethods | any | publickey, or publickey,keyboard-interactive | Requires specific methods |
| MaxAuthTries | 6 | 3 to 6 | Limits guesses per connection |
| LoginGraceTime | 120 seconds | 30 to 60 seconds | Frees unauthenticated slots sooner |
| AllowUsers or AllowGroups | all users | named groups | Narrows who can log in at all |
For hardening specific to internet exposed servers, see sshd settings for remote access from anywhere.
Forwarding
| Setting | Default | Suggested |
|---|---|---|
| AllowAgentForwarding | yes | no where users only run commands; pair with no shell for real effect |
| AllowTcpForwarding | yes | no, or local, plus PermitOpen for specific targets |
| GatewayPorts | no | no |
| X11Forwarding | no | no |
| DisableForwarding | (not set) | yes for service accounts, with Match blocks for exceptions |
On the client, ForwardAgent defaults to no. Keep it that way and use ProxyJump (ssh -J) for multi hop access, as shown in how to set up a bastion host for secure SSH access.
Pinning host keys
Get the real fingerprint on the server itself, as the ssh(1) manual shows:
ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub
On a client, list accepted fingerprints or look up one host:
ssh-keygen -lv -f ~/.ssh/known_hosts
ssh-keygen -F host.example.com
Use StrictHostKeyChecking yes with a managed /etc/ssh/ssh_known_hosts for automation, and host certificates (RFC 4251's CA model) for larger fleets.
Publishing SSHFP records (RFC 4255)
The SSHFP record (DNS type 44) holds an algorithm number (IANA: 1 RSA, 2 DSA, 3 ECDSA, 4 Ed25519, 6 Ed448), a fingerprint type (1 SHA-1, 2 SHA-256) and the fingerprint. RFC 4255 says it MUST NOT be trusted unless authenticated with DNSSEC. The ssh(1) manual's workflow:
# on the server: print SSHFP records to add to the zone
ssh-keygen -r host.example.com.
# check the zone answers
dig -t SSHFP host.example.com
# connect, showing whether DNS matched
ssh -o "VerifyHostKeyDNS ask" host.example.com
VerifyHostKeyDNS defaults to no; yes trusts DNSSEC validated matches without prompting. Publish SHA-256 records only, since OpenSSH 10.1 announced that SHA-1 SSHFP records will be ignored in future. Build records from the server's key files rather than ssh-keyscan -D, whose manual warns that unverified network collection is open to a man in the middle.
Finally, keep sshd's LogLevel at its default INFO; the manual warns that DEBUG levels violate user privacy.
Which SSH implementations are inside your software
SSH code also lives where nobody edits sshd_config: base images that ship a Git client, CI runners, deployment tools, network appliances, and applications that open SSH or SFTP connections through libraries such as libssh, libssh2, Paramiko, Go's golang.org/x/crypto/ssh or Apache MINA SSHD. Each has its own release history and its own timing for fixes such as strict KEX, so a fully patched server can still run an application linked to an older SSH library.
That is where Safeguard fits. Its software composition analysis, generally available, inventories transitive dependencies in repositories and container images, so an SSH library three levels deep still shows up. SBOM generation and analysis in SPDX and CycloneDX, vulnerability management with EPSS and CISA KEV enrichment, reachability analysis and container scanning are also generally available and help you find and rank SSH library findings when an advisory lands. Safeguard does not inspect SSH traffic or replace host configuration review; the sshd -T checks above remain your job.
When to use SSH and when to use something else
| Need | SSH | RDP | Bastion (SSH jump host) | Identity-aware access proxy |
|---|---|---|---|---|
| Typical use | Shell, commands, file transfer, Git, tunnels | Graphical Windows desktop | One controlled entry point to private SSH hosts | Per request access based on user and device identity |
| Authentication | Keys, certificates, passwords, GSS-API | Windows account credentials | SSH keys or certificates at bastion and targets | Single sign on, device posture, short lived credentials |
| Main risks | Unverified host keys, forwarding, password guessing on exposed port 22 | Exposed listeners, credential relay, drive and clipboard redirection | Bastion is a high value target; agent forwarding through it | Proxy configuration; trust in the identity provider |
| Operational cost | Low: one daemon, one port | Medium: Windows hosts plus a gateway | Medium: one more host to patch and log | Higher: identity integration and policy design |
| Good default when | You manage Linux, BSD, network devices or Git | You need a Windows desktop | You need SSH into private networks today | You want to remove standing network access |
They combine well: SSH to every server, only a bastion or proxy exposed, short lived SSH certificates instead of static keys, and no agent forwarding.
Common mistakes
- Treating "ssh-rsa" as a key problem. OpenSSH 8.8 disabled SHA-1 RSA signatures, not RSA keys. Re-enabling
ssh-rsaglobally to fix one old device weakens every connection; scope it with aHostblock if you must. - Hand written algorithm lists that never get updated. A
KexAlgorithmsline copied years ago can silently exclude the post-quantum defaults. Prefer the defaults or remove items with the-syntax. - Accepting host keys by habit. Typing "yes" to an unknown fingerprint is the exact moment RFC 4251 says the protocol cannot protect you. Distribute keys or use certificates.
- Forwarding the agent to shared hosts. The key stays home but can be used by anyone with enough access on the remote host. Use ProxyJump.
- Believing AllowTcpForwarding no isolates a shell user. The manual says users with shell access can install their own forwarders. Restrict the shell too.
- Publishing SSHFP without DNSSEC. RFC 4255 says unsigned SSHFP records must not be trusted. Also move to SHA-256 records.
- Turning on compression for mixed trust traffic. OpenSSH documentation already advised against it, and 10.6 reduced its effectiveness to close a cross channel leak. Compress in the application.
- Patching sshd but not the libraries. Strict KEX has to exist on both ends, and applications and images often carry their own SSH code.
FAQ
What port does SSH use?
TCP port 22, registered with IANA for ssh and named in RFC 4253. Another port cuts scan noise but adds no cryptographic protection.
Is SSH encrypted?
Yes, from the first SSH_MSG_NEWKEYS onward, including user names and passwords. The version strings and algorithm lists sent before that are visible to anyone on the path.
What is the difference between SSH-1 and SSH-2?
SSH-2 is the layered protocol standardised in RFC 4250 to 4254, with negotiated algorithms and public key authentication bound to the session. SSH-1 was an earlier protocol that was never standardised, and the two are incompatible. OpenSSH disabled SSH-1 by default in 7.0 (2015) and removed it from the client in 7.6 (2017).
Is SSH quantum safe?
Key agreement can be. OpenSSH has used a hybrid post-quantum key exchange by default since 9.0 and uses mlkem768x25519-sha256 by default since 10.0, which protects recorded sessions against future decryption. Host and user key signatures are still mostly classical; OpenSSH added an ML-DSA 44 plus Ed25519 composite scheme in 10.4 and enabled it in 10.6.
How do I check which algorithms my SSH server supports?
Run sudo sshd -T on the server for the effective kexalgorithms, ciphers and macs; ssh -Q kex and friends show what the build supports. Only check servers you own or are authorised to test.
What is an SSHFP record?
An SSHFP record (RFC 4255, DNS type 44) publishes a hash of a server's SSH host key in DNS. A client with VerifyHostKeyDNS enabled compares it with the key the server presents. It is only trustworthy when the zone is signed with DNSSEC and the client validates it.
Is SSH agent forwarding safe?
Only to hosts you fully trust. The key is never copied, but while you are connected anyone with enough access there can use it to sign as you. ProxyJump avoids the need.
What was the Terrapin attack?
Terrapin (CVE-2023-48795, published 18 December 2023) let an attacker on the path delete a few messages at the start of an encrypted SSH session without detection, mainly the extension information message. OpenSSH 9.6 added a "strict KEX" extension that blocks it when both client and server support it.
Fix it without waiting for upstream
SSH weaknesses that reach production usually live in an implementation, not in RFC 4253: an sshd in a base image, an SSH or SFTP library in an application, or a deployment tool that bundles its own client. Those are dependencies you can find, upgrade and gate like any other.
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
These demos show dependency, image and SBOM workflows. They do not monitor SSH sessions or check sshd_config. You can download Safeguard and follow the CLI docs to try them on your own projects.
This demo sets up the Safeguard runner on your own machine, approves it with its device code and then scans from the terminal. Pointed at a container image for a CI runner or deployment tool, the same scan lists the SSH packages and libraries inside it, such as an OpenSSH client or libssh2, with their versions and any known vulnerabilities.
Click through it at your own pace, or open it full screen.
This demo is a real CLI session: install, open the interactive terminal, ask it to fix the lodash CVE in a repository, and watch it read the finding and advisory, ask before each command, move lodash to 4.17.21, run npm install and npm test with approval, and print a summary without pushing anything. The same approve each step flow applies when the package to bump is an SSH library such as Paramiko or the Go SSH module pinned in your manifest.
Click through it at your own pace, or open it full screen.
This demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. Once connected, you can ask in plain language which of your projects include a particular SSH library version after an advisory, and get the answer from your own scan data rather than from memory.
Click through it at your own pace, or open it full screen.
Quick check
An attacker records your SSH session and later steals the server's private host key. Can they decrypt the recording?
No, if the session used a Diffie-Hellman style key exchange, which all modern SSH key exchanges are. RFC 4251 explains that the session keys come from ephemeral values that are thrown away, so a later leak of the long term host key does not reveal past sessions. The host key only proves the server's identity during the handshake. A future quantum computer is a different threat, which is why hybrid post-quantum key exchange is now the OpenSSH default.
You set AllowTcpForwarding no on a server where users still have a normal shell. Have you stopped them tunnelling?
Not really. The sshd_config(5) manual says disabling TCP forwarding does not improve security unless users are also denied shell access, because they can install their own forwarders. Pair the setting with restricted shells, ForceCommand or DisableForwarding for accounts that do not need interactive access.
Sources
- RFC 4250: The Secure Shell (SSH) Protocol Assigned Numbers, January 2006
- RFC 4251: The Secure Shell (SSH) Protocol Architecture, January 2006
- RFC 4252: The Secure Shell (SSH) Authentication Protocol, January 2006
- RFC 4253: The Secure Shell (SSH) Transport Layer Protocol, January 2006
- RFC 4254: The Secure Shell (SSH) Connection Protocol, January 2006
- RFC 4255: Using DNS to Securely Publish SSH Key Fingerprints, January 2006
- RFC 4256: Generic Message Exchange Authentication for SSH
- RFC 4344: SSH Transport Layer Encryption Modes
- RFC 4419: Diffie-Hellman Group Exchange for SSH
- RFC 5656: Elliptic Curve Algorithm Integration in SSH
- RFC 6668: SHA-2 Data Integrity Verification for SSH
- RFC 8268: More MODP Diffie-Hellman Groups for SSH
- RFC 8308: Extension Negotiation in SSH, March 2018
- RFC 8332: Use of RSA Keys with SHA-256 and SHA-512 in SSH, March 2018
- RFC 8709: Ed25519 and Ed448 Public Key Algorithms for SSH, February 2020
- RFC 8731: SSH Key Exchange Method Using Curve25519 and Curve448
- RFC 9142: Key Exchange Method Updates and Recommendations for SSH, January 2022
- RFC 9941: SSH Key Exchange Method Using Hybrid sntrup761 and X25519 with SHA-512, April 2026
- RFC 9987: Secure Shell (SSH) Agent Protocol, May 2026
- RFC 10042: Post-Quantum/Traditional Hybrid Key Exchange with ML-KEM for SSH, August 2026
- IANA: Secure Shell (SSH) Protocol Parameters
- IANA: DNS SSHFP Resource Record Parameters
- IANA: Service Name and Transport Protocol Port Number Registry
- OpenSSH: Specifications
- OpenSSH: Release Notes
- OpenSSH: Post-Quantum Cryptography
- OpenBSD manual: sshd_config(5)
- OpenBSD manual: sshd(8)
- OpenBSD manual: ssh(1)
- OpenBSD manual: ssh_config(5)
- OpenBSD manual: ssh-keygen(1)
- OpenBSD manual: ssh-keyscan(1)
- CVE-2023-48795 record (cve.org)
Last reviewed: 23 October 2026. OpenSSH defaults change between releases; confirm against the manual pages for the version you run.