cheat sheet
SSH Tunnels & Port Forwarding
Local, remote, and dynamic SSH tunnels — port forwarding, SOCKS proxies, jump hosts, ssh_config directives, agent forwarding, autossh persistence, post-quantum key exchange, and operational recipes.
SSH Tunnels & Port Forwarding
What it is
An SSH tunnel uses an already-authenticated SSH connection as an encrypted transport for arbitrary TCP traffic, letting you reach services that would otherwise be unreachable, unencrypted, or firewalled. The three core forwarding modes are local (-L — pull a remote port to your laptop), remote (-R — push a local port out to a server), and dynamic (-D — turn the SSH connection into a SOCKS5 proxy). On top of those primitives, ProxyJump/-J chains tunnels through bastion hosts, ssh_config makes them declarative, autossh keeps them alive, and agent forwarding (-A) lets the far end use your local keys without copying them. Reach for SSH tunnels whenever you need encrypted access to an internal database, a VPN-like SOCKS proxy from a coffee-shop network, or a stable bastion path into a private VPC.
Configuration
SSH on a modern Linux/macOS box reads four files at every invocation, in this precedence order. Knowing which file owns which behaviour is the difference between a 5-second fix and a half-hour bisect of "why does this host work from my laptop but not from CI".
| File | Owner | Purpose |
|---|---|---|
~/.ssh/config | the user | Per-host overrides — aliases, IdentityFile, ProxyJump, LocalForward. The file you edit 95% of the time. |
/etc/ssh/ssh_config | root | System-wide client defaults applied to every user. Distros often ship Include /etc/ssh/ssh_config.d/*.conf here. |
/etc/ssh/ssh_config.d/*.conf | root | Drop-in fragments merged into the system config (e.g. 50-redhat.conf setting ServerAliveInterval). |
~/.ssh/known_hosts | the user | Pinned host-key fingerprints. Editable, but normally maintained by ssh-keygen -R <host> and -F <host>. |
~/.ssh/authorized_keys | the user on the server | Public keys allowed to log into the user's account on the remote machine. Lines may carry from=, command=, restrict, permitlisten= options. |
The most-edited file is ~/.ssh/config. It is matched top-to-bottom, first-match-wins per option, so put specific Host blocks before catch-all Host * blocks. Glob patterns (Host *.internal), negation (Host !staging-*), and Match predicates (Match host db-* user alicedev) compose well; Include ~/.ssh/config.d/* pulls in fragments the same way sshd_config does on the server side.
# ~/.ssh/config — typical layout
# 1. Per-host blocks first, most specific to least specific
Host db-tunnel
HostName bastion.example.com
User alicedev
LocalForward 5432 db.internal:5432
ExitOnForwardFailure yes
Host *.internal
ProxyJump bastion
User alicedev
Host bastion
HostName bastion.example.com
User alicedev
IdentityFile ~/.ssh/id_ed25519_bastion
IdentitiesOnly yes
# 2. Pull in extra fragments
Include ~/.ssh/config.d/work.conf
# 3. Last, the catch-all defaults
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
# Check the effective config for a given host (resolves Match, Include, globs)
ssh -G db-tunnel
# Verify ownership/permissions — sshd refuses keys if these are too loose
ls -la ~/.ssh
# Required: ~/.ssh = 700, config = 600, authorized_keys = 600, *.pub = 644
chmod 700 ~/.ssh
chmod 600 ~/.ssh/config ~/.ssh/authorized_keys ~/.ssh/id_ed25519
# Lookup the recorded fingerprint for a host (without printing all hosts)
ssh-keygen -F bastion.example.com -l
# Remove a stale host key after a server is re-provisioned
ssh-keygen -R bastion.example.com
Local port forwarding (-L)
Forward a local port to a remote destination via an SSH host.
# Access remote DB on localhost:5432
ssh -L 5432:localhost:5432 user@jumphost.example.com
# Access a host only reachable from the jump host
ssh -L 8080:internal-server:80 user@jumphost.example.com
# Keep alive, no shell, background
ssh -fNL 5432:db.internal:5432 user@jumphost.example.com
Remote port forwarding (-R)
Expose a local service on a port of the remote host.
# Make local :3000 reachable as remote :9000
ssh -R 9000:localhost:3000 user@remote.example.com
# Bind to all interfaces on remote (needs GatewayPorts yes in sshd_config)
ssh -R 0.0.0.0:9000:localhost:3000 user@remote.example.com
Dynamic (SOCKS5 proxy) (-D)
# Start SOCKS5 proxy on local :1080, route through jump host
ssh -D 1080 -fN user@jumphost.example.com
# Then configure browser/curl to use SOCKS5 localhost:1080
curl --socks5-hostname localhost:1080 http://internal-site/
Jump hosts (-J / ProxyJump)
# Single jump
ssh -J user@jump.example.com user@target.internal
# Multi-hop
ssh -J user@jump1,user@jump2 user@final.internal
~/.ssh/config patterns
Host jump
HostName jump.example.com
User myuser
IdentityFile ~/.ssh/id_ed25519
Host internal
HostName target.internal
User myuser
ProxyJump jump
Host db-tunnel
HostName jump.example.com
User myuser
LocalForward 5432 db.internal:5432
ServerAliveInterval 60
ExitOnForwardFailure yes
Then just: ssh -fN db-tunnel
Persistent tunnels with systemd
[Unit]
Description=SSH tunnel to production DB
After=network.target
[Service]
User=tunnel
ExecStart=/usr/bin/ssh -NT -o ServerAliveInterval=60 \
-o ExitOnForwardFailure=yes \
-L 5432:db.prod.internal:5432 \
user@jumphost.example.com
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target
Forwarding flag anatomy
Every forwarding flag follows the pattern [bind_address:]port:host:hostport, but the meaning of "host" depends on direction. For -L, the host/hostport is resolved on the remote SSH server after the connection is established; for -R, the host/hostport is resolved on the local machine that initiates the SSH session. Confusing the two is the classic newbie mistake — it produces tunnels that connect to the wrong machine entirely.
# -L: local listener; remote does the lookup of "db.internal"
ssh -L 5432:db.internal:5432 alicedev@bastion.example.com
# -R: remote listener; LOCAL machine resolves "10.0.0.5"
ssh -R 8080:10.0.0.5:80 alicedev@remote.example.com
# Bind to a specific local interface instead of 127.0.0.1
ssh -L 192.168.1.10:5432:db.internal:5432 alicedev@bastion.example.com
# Bind to all interfaces (DANGER — exposes the tunnel on your LAN)
ssh -L 0.0.0.0:5432:db.internal:5432 alicedev@bastion.example.com
By default -L binds only to 127.0.0.1 and -R binds only to the remote's loopback. To listen on all interfaces, prefix the bind with 0.0.0.0: (or *:) and ensure the right server-side knob is set — for -R that means GatewayPorts yes in the remote sshd_config. Without it, the remote sshd silently rebinds to localhost no matter what you specify.
GatewayPorts and bind-address pitfalls
GatewayPorts is a sshd_config setting on the far end that controls whether -R tunnels may bind to non-loopback addresses on the server. The default no means ssh -R 0.0.0.0:9000:... will still only bind to 127.0.0.1 on the remote, regardless of what the client asks for. Set GatewayPorts yes (or clientspecified for finer control) to allow public exposure.
# /etc/ssh/sshd_config on the remote
# yes -> remote -R binds to 0.0.0.0 if client asks
# clientspecified -> honour the client's bind address verbatim
# no (default) -> always bind to loopback only
GatewayPorts clientspecified
# Reload sshd after editing
sudo systemctl reload sshd
The mirror-image setting on the client is AllowTcpForwarding, which controls whether -L and -R are permitted at all. If a tunnel command returns immediately with channel 0: open failed: administratively prohibited, the remote sshd_config has AllowTcpForwarding no or PermitOpen is restricting which targets you may reach.
Dynamic SOCKS proxy in depth
-D port starts a SOCKS4/SOCKS5 server on the local machine that proxies every connection through the SSH tunnel and resolves DNS on the remote side. This is the simplest VPN substitute — point a browser at localhost:<port> and every site you visit appears to originate from the SSH server's IP.
# Background SOCKS proxy on :1080
ssh -fN -D 1080 alicedev@bastion.example.com
# Use it with curl
curl --socks5-hostname localhost:1080 https://internal-wiki.example.com/
# Use it with git over HTTPS
git -c http.proxy=socks5h://localhost:1080 clone https://github.com/owner/repo
# Whole-system SOCKS via tsocks / proxychains
proxychains4 -q apt update
Always pass
--socks5-hostnameto curl (not--socks5). The-hostnamevariant resolves the target DNS name on the remote end. Plain--socks5does the DNS lookup locally, which defeats the proxy and may leak split-horizon hostnames (db.internal) to your local resolver.
For browser use, point Firefox or Chrome at SOCKS5 host=127.0.0.1 port=1080 and enable "Proxy DNS when using SOCKS v5" (Firefox) or run Chrome with --proxy-server="socks5://127.0.0.1:1080" --host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE 127.0.0.1".
SSH agent and key forwarding (-A)
ssh-agent holds your decrypted private keys in memory so you don't re-enter passphrases for every connection. Agent forwarding (-A on the command line or ForwardAgent yes in ssh_config) makes the agent socket available on the remote host, letting a process there authenticate to a third host using your local keys without ever copying the key material.
# Start an agent if one isn't running
eval "$(ssh-agent -s)"
# Add a key (prompts for passphrase once)
ssh-add ~/.ssh/id_ed25519
# List loaded identities
ssh-add -l
# Remove all loaded keys
ssh-add -D
# Connect with agent forwarding (then "ssh other-host" from the remote
# uses YOUR local key, not a key stored on the remote)
ssh -A alicedev@bastion.example.com
Agent forwarding to an untrusted host is a privilege-escalation risk: any root user on that host can hijack the agent socket and use your keys for as long as the connection is open. Prefer
ProxyJump(which never exposes the agent on intermediate hops) overForwardAgent yesfor routine bastion access.
ProxyJump and ProxyCommand
ProxyJump (the -J flag and ssh_config directive) is the modern, declarative way to route through one or more bastion hosts. The traffic is multiplexed through the bastion's SSH channel without exposing the agent or running shell commands. ProxyCommand is the older, more powerful escape hatch — it pipes the SSH connection through an arbitrary command, useful for jumping through HTTP proxies, nc, or cloudflared access.
# Modern: ProxyJump with -J
ssh -J alicedev@bastion.example.com alicedev@target.internal
# Multi-hop ProxyJump
ssh -J alicedev@bastion1,alicedev@bastion2 alicedev@target.internal
# Legacy / advanced: ProxyCommand through netcat on the bastion
ssh -o ProxyCommand="ssh -W %h:%p alicedev@bastion.example.com" \
alicedev@target.internal
# ProxyCommand for a Cloudflare-tunnelled service
ssh -o ProxyCommand="cloudflared access ssh --hostname %h" alicedev@target.example.com
In ssh_config:
Host target
HostName target.internal
User alicedev
ProxyJump bastion
Host bastion
HostName bastion.example.com
User alicedev
# ProxyCommand alternative for legacy clients
Host target-legacy
HostName target.internal
ProxyCommand ssh -W %h:%p bastion
ControlMaster connection multiplexing
ControlMaster reuses a single SSH connection for multiple sessions, drastically reducing the latency of repeated ssh, scp, rsync, and git calls to the same host. The first invocation opens a master channel; subsequent invocations share it through a control socket and avoid the full TCP handshake and key exchange.
Host *
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
ControlMaster auto— open a master if none exists, share otherwise.ControlPath— socket file;%ruser,%hhost,%pport (use a dedicated dir like~/.ssh/cm/and pre-create it).ControlPersist 10m— keep the master alive 10 minutes after the last session closes.
# After first ssh, subsequent commands are near-instant
ssh alicedev@bastion.example.com # opens master
scp file.tar alicedev@bastion.example.com:/tmp/ # reuses master
# Manage the master from the client
ssh -O check alicedev@bastion.example.com # status
ssh -O stop alicedev@bastion.example.com # graceful shutdown
ssh -O exit alicedev@bastion.example.com # force kill
A stale or orphaned control socket causes "mux_client_request_session: read from master failed" errors. Delete the socket file and retry —
rm ~/.ssh/cm-*is safe when no SSH sessions are open.
Persistence with autossh
autossh wraps ssh and re-launches the connection automatically if it dies, using a pair of monitoring ports to detect frozen sessions that haven't dropped TCP. It's the right tool for long-lived tunnels on laptops that suspend, or for any "always-on" forward that must survive transient network failures.
# Install on Debian/Ubuntu
sudo apt install autossh
# Background tunnel that auto-reconnects
autossh -M 0 -fN -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" \
-L 5432:db.internal:5432 alicedev@bastion.example.com
-M 0 disables autossh's own monitoring ports and relies on ServerAliveInterval/ServerAliveCountMax (built into ssh) instead, which is the recommended modern configuration. The -f flag forks to background; -N means "no remote command".
For supervised persistence on a server, prefer systemd (see the unit file above) — it gives you systemctl status, structured logs, and dependency ordering that autossh alone cannot.
Keep-alive and ExitOnForwardFailure
Idle SSH connections often die silently when a NAT or firewall drops the conntrack entry. ServerAliveInterval sends an encrypted keep-alive every N seconds; ServerAliveCountMax is how many missed replies are tolerated before the client gives up. Setting both prevents zombie tunnels that look connected but no longer pass traffic.
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
TCPKeepAlive yes
ExitOnForwardFailure yes
ExitOnForwardFailure yes makes ssh return non-zero immediately if a -L, -R, or -D cannot be set up (port already in use, remote refuses the forward). Without it, ssh -fN silently drops into a tunnel-less interactive session and your script proceeds thinking the tunnel exists.
Post-quantum key exchange
OpenSSH has shipped hybrid post-quantum key agreement by default since 9.0 (April 2022, originally sntrup761x25519-sha512). OpenSSH 9.9 (September 2024) added the NIST-standardised mlkem768x25519-sha256 (ML-KEM from FIPS 203, derived from CRYSTALS-Kyber, hybridised with X25519). OpenSSH 10.0 (April 2025) promoted mlkem768x25519-sha256 to the first-preference KEX and removed DSA entirely. OpenSSH 10.1 (October 2025) prints a warning when a non-post-quantum key agreement is negotiated, and 10.2 (also October 2025, the bugfix release that superseded 10.1 for general distribution) extended the warning to additional cases. The push behind this is the "harvest now, decrypt later" threat — sessions captured today could be retroactively decrypted once a cryptographically relevant quantum computer exists, so the long tail of legacy NIST P-256 / classic X25519 sessions needs to be rotated out.
# Check what your local ssh client offers (most-preferred first)
ssh -Q kex | head -5
# Typical OpenSSH 10.x output:
# mlkem768x25519-sha256
# sntrup761x25519-sha512@openssh.com
# curve25519-sha256
# curve25519-sha256@libssh.org
# ecdh-sha2-nistp256
# See which KEX the server actually negotiated for a real connection
ssh -v alicedev@bastion.example.com 2>&1 | grep -i "kex: algorithm"
# debug1: kex: algorithm: mlkem768x25519-sha256
# Force a specific algorithm (for testing or to silence the 10.1+ warning)
ssh -o KexAlgorithms=mlkem768x25519-sha256 alicedev@bastion.example.com
# Or list a preference order in ssh_config — note the leading PQ entries
# ~/.ssh/config — explicit PQ-first preference
Host *
KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
PubkeyAcceptedAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
If you see
ssh: kex_exchange_identification: post-quantum algorithm not used(or similar) since upgrading to OpenSSH 10.1+, the server is the one that doesn't supportmlkem768x25519-sha256orsntrup761x25519-sha512@openssh.com. The fix is to upgradesshdon the far end — GitHub, GitLab, and most major hosting providers added support during 2025. To suppress the warning on a per-host basis while you wait for the server to be upgraded, setKexAlgorithmsexplicitly in the relevantHostblock; the warning fires when the negotiated KEX is not post-quantum, not on the mere presence of legacy algorithms in the preference list.
Recent OpenSSH features worth knowing (9.6 → 10.x)
A handful of ssh_config / sshd_config directives added since OpenSSH 9.6 (December 2023) materially change tunnel and bastion workflows. They are worth folding into hardened configs.
%jtoken (9.6) — expands to the configuredProxyJumphostname (empty if none). Useful inIdentityFile,ControlPath, and other%-tokenised directives to differentiate sessions by jump host.Includewith environment expansion (9.9) —~/.ssh/configcan nowInclude ~/.ssh/config.d/${ENV:-default}.confand expand the same%-tokens asMatch Exec, making per-environment configs much simpler.Match invalid-user(sshd, 9.9) — match on connections that target nonexistent usernames, useful for fail2ban-style policies without touching authentication logs.RefuseConnectionandrefuseconnectionpenalty (sshd, 9.9) — terminate the connection at the first authentication attempt and feed that intoPerSourcePenalties, the rate-limiter introduced in 9.8.PerSourcePenalties(sshd, 9.8) — first-class IP penalty system that throttles or blocks clients failing authentication, malformed handshakes, or hittingRefuseConnection. Reduces the value of installing fail2ban for the SSH service alone.- Core-dump scrubbing (9.9) — private keys held in
ssh/sshd/ssh-agentmemory are excluded from core dumps on OpenBSD, Linux, and FreeBSD. - Username metachar ban (10.1 security fix) — control characters in usernames passed via the command line or
%-expanded from config are rejected; older versions could be tricked into shell injection through a craftedProxyCommand. - Agent socket relocation (10.1) —
ssh-agentandsshdnow place the agent forwarding socket under~/.ssh/agent/instead of/tmp, so processes running in tighter filesystem sandboxes no longer ambiently have access to the agent. ControlPersistfix (10.2) — 10.1 introduced a regression that broke interactive sessions whenControlPersistwas set; upgrade to 10.2 (or skip 10.1 entirely) if you use connection multiplexing, which most heavy SSH users should.
# Confirm the OpenSSH version you have (client + server)
ssh -V
ssh alicedev@bastion.example.com 'ssh -V'
# 9.6+ — use %j in a per-jump-host ControlPath to keep sockets separated
Host *
ControlMaster auto
ControlPath ~/.ssh/cm/%C-%j # %C is a hash of %l%h%p%r; %j is the ProxyJump
ControlPersist 10m
Hardening ssh_config
Project- or host-level overrides in ~/.ssh/config keep command lines short and make tunnel setup reproducible. Use the same file for tunnels you maintain regularly; named entries also work with scp, rsync, git, and most other SSH-aware tools.
# Per-host pinned key + reduced cipher list
Host bastion
HostName bastion.example.com
User alicedev
IdentityFile ~/.ssh/id_ed25519_bastion
IdentitiesOnly yes
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
# PQ-first KEX (OpenSSH 9.9+ for mlkem768x25519-sha256; 9.0+ for sntrup761)
KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512@openssh.com,curve25519-sha256
# Database tunnel — one shot
Host db-tunnel
HostName bastion.example.com
User alicedev
LocalForward 5432 db.internal:5432
ExitOnForwardFailure yes
ServerAliveInterval 30
RequestTTY no
RemoteCommand none
# Lab network — SOCKS proxy
Host lab-socks
HostName lab-gw.example.com
User alicedev
DynamicForward 1080
ServerAliveInterval 30
IdentitiesOnly yes tells ssh to use only the listed IdentityFile, not every key loaded in the agent — this prevents "too many authentication failures" when the agent has many keys for many hosts.
Common recipes
Reach a private Postgres through a bastion
# One-shot tunnel + psql in one command
ssh -fNL 5432:db.internal:5432 alicedev@bastion.example.com
psql -h 127.0.0.1 -U appuser appdb
# Tear down
pkill -f "ssh -fNL 5432:db.internal"
Browse an internal site through a SOCKS proxy
ssh -fND 1080 alicedev@bastion.example.com
# Firefox: about:preferences#general -> Network Settings -> Manual SOCKS5 host=127.0.0.1 port=1080
# Or via curl:
curl --socks5-hostname 127.0.0.1:1080 https://internal-wiki.example.com/
Expose a local dev server through a public host
# Make local :3000 reachable as https://myhost.example.com (port 9000)
# Requires GatewayPorts clientspecified on the remote sshd_config
ssh -fNR 0.0.0.0:9000:localhost:3000 alicedev@myhost.example.com
Tunnel into a Kubernetes API through a jump host
ssh -fNL 6443:kube-api.internal:6443 -J alicedev@bastion.example.com alicedev@control-plane.internal
kubectl --insecure-skip-tls-verify --server=https://127.0.0.1:6443 get pods
rsync through a bastion without copying keys
# Uses ProxyJump under the hood — bastion never sees plaintext data
rsync -avP -e "ssh -J alicedev@bastion.example.com" \
./build/ alicedev@target.internal:/srv/app/
Chain a SOCKS tunnel through two hops
ssh -J alicedev@bastion1,alicedev@bastion2 -D 1080 -fN alicedev@target.internal
Diagnosing tunnel problems
When a tunnel "works but doesn't", the failure is almost always in one of: bind address, server-side sshd_config, the resolver picking the wrong target, or a firewall closing idle conntrack. Use -v (or -vv/-vvv) on the client and check journalctl -u ssh on the server.
# Verbose client output, no shell
ssh -vN -L 5432:db.internal:5432 alicedev@bastion.example.com
# Check what's listening locally
ss -tlnp | grep 5432
lsof -iTCP:5432 -sTCP:LISTEN
# Test the tunnel from a separate shell
nc -zv 127.0.0.1 5432
# Check the remote sshd config without editing
ssh alicedev@bastion.example.com 'sshd -T | grep -Ei "gatewayports|allowtcp|permitopen"'
# Inspect the active control-master socket
ssh -O check alicedev@bastion.example.com
Common error → cause map:
| Symptom | Likely cause |
|---|---|
bind: Address already in use | A previous ssh -fN is still running — pkill ssh or pick a different local port. |
channel 2: open failed: administratively prohibited | sshd_config has AllowTcpForwarding no or PermitOpen restricts the target. |
-R binds only to 127.0.0.1 on remote | GatewayPorts is no or unset on the remote sshd_config. |
| Tunnel "freezes" after sleeping the laptop | Add ServerAliveInterval 30 and consider autossh. |
Permission denied (publickey) | IdentityFile not loaded in agent, or IdentitiesOnly yes missing and agent offered the wrong key first. |
mux_client_request_session: read from master failed | Stale ControlPath socket — delete and retry. |
Could not request local forwarding | Mistyped -L syntax (host on wrong side) or local port already bound. |
ssh vs alternatives
| Need | Best tool |
|---|---|
| One-shot encrypted port forward | ssh -L / -R |
| VPN-like full traffic routing | WireGuard, Tailscale, or ssh -D SOCKS |
| Long-lived multi-port site-to-site link | WireGuard or Tailscale |
| Expose dev server to the public internet | ssh -R with GatewayPorts, cloudflared tunnel, or ngrok |
| Multi-tenant bastion with audit | Teleport, Cloudflare Access, or AWS SSM Session Manager |
| Reverse tunnel that survives reboots | autossh + systemd unit |
For long-lived production tunnels, prefer purpose-built tools (Tailscale, WireGuard, Cloudflare Tunnel) over
autossh. SSH tunnels are excellent for ad-hoc and developer use but have no built-in metrics, no failover, and a single user identity per connection.
Sources
- OpenSSH Release Notes — canonical list of every release and its feature/security changes.
- OpenSSH 9.9 release notes — introduces
mlkem768x25519-sha256,RefuseConnection,Match invalid-user, core-dump scrubbing. - OpenSSH 10.0 release notes — promotes
mlkem768x25519-sha256to default KEX, removes DSA. - OpenSSH 10.1 release notes — non-PQ KEX warning, username metachar fix, agent-socket move under
~/.ssh/. - OpenSSH: Post-Quantum Cryptography — upstream rationale for
sntrup761x25519-sha512andmlkem768x25519-sha256. - OpenSSH 10.2 released (undeadly.org) — bugfix for
ControlPersistregression in 10.1. - OpenSSH 9.6 release notes — adds
%jProxyJump token, multiplexing fixes.