SSH breaks down "remote operations on untrusted networks" into three problems: which host you are connecting to, which user the remote side accepts, and what features the authenticated user is allowed to open. Understanding these three boundaries is more helpful for distinguishing between Host key verification failed, Permission denied, and forwarded connections being refused than memorizing a key exchange flow.
Layer
Work Completed
What Is Still Not Guaranteed After Success
Transport
Algorithm negotiation, host authentication, encryption and integrity, key exchange
The user can log in
User Authentication
Proves user identity and satisfies authentication policies
All session features are authorized
Connection
Multiplexed channels, flow control, session and forwarding requests
Application operations are necessarily completed or persisted
Key Exchange: Shared Secret, Exchange Hash, and Host Key
SSH typically establishes a TCP connection first, then sends SSH-2.0-SoftwareIdentifier followed by a CRLF terminator. The version line is not encrypted content; subsequently, KEXINIT is exchanged via binary packets to negotiate key exchange, host keys, and transport algorithms for both directions. The intersection and order of algorithm lists affect the result; you cannot treat the default values of a single OpenSSH version as the only algorithms for the SSH protocol.
Using ECDH-based exchange as an example: the client and server each generate temporary keys and jointly compute the shared secret K; the exchange hash H covers specified fields such as version information, both parties' KEXINIT, the server's host public key, temporary exchange values, and K. The server signs H with its host private key, and the client verifies the signature, thereby binding the negotiation process to this host key. H is not simply a rename for the shared secret, nor is it used directly as the traffic encryption key.
Only the H from the first key exchange is fixed as the session ID. Subsequent rekeys produce new K, H, and transport keys, while the session ID remains unchanged. Both parties send NEWKEYS, switching keys in their respective send and receive directions; do not interpret this as a global switch that changes both directions simultaneously. RFC 4253 §7–9
Modern implementations also support hybrid post-quantum key exchange. For example, OpenSSH 10.0 sets mlkem768x25519-sha256 as the default preference; this is a change in key exchange and does not mean host authentication automatically becomes the same algorithm. On the other hand, disabling ssh-rsa signatures usually refers to disabling RSA/SHA-1, not that all RSA public keys become invalid; the same RSA key can use RSA/SHA-2 signatures, provided both sides support and allow it. OpenSSH Release Notes
Why Check known_hosts After Signature Verification Passes?
An attacker can also generate their own keys and correctly sign their own exchange. Signature verification only proves that the other party holds the private key corresponding to the provided public key; to confirm "this is the host I want to connect to," you still need to bind the hostname to a trusted key or trusted issuer.
Trust Source
How Binding Is Established
Content to Maintain
Pre-installed Host Public Key
Write public key to known_hosts via trusted channel
Host changes, key rotation, revocation
Trust on First Use (TOFU)
Save after first verification or acceptance, compare subsequently
Credibility of the first accepted key
OpenSSH Host Certificates
Trust CA for matching host ranges, verify certificate constraints
CA, principal, validity period, and revocation
DNS SSHFP
Use fingerprints in DNS when configuration and verification conditions are met
DNSSEC validation chain and record maintenance
Therefore, initial trust can be completed automatically via configuration distribution or host CA; it is not "the only step in SSH that cannot be automated." ssh-keyscan only collects public keys obtained from the network; using it alone does not constitute trusted verification; clearing old records also does not explain a host key change. ssh Host Authentication, VerifyHostKeyDNS
OpenSSH certificates are SSH's own certificate format, not TLS X.509 certificates. Host certificates answer the server identity question, while user certificates answer the login identity question; trusting a host CA does not mean that any user signed by this CA gains login permissions.
User Authentication: Signature Requests and "Partial Success"
Data signed by publickey authentication includes the session ID and specified user authentication request fields, binding the proof to the current connection and requested identity. The client can first ask the server if it accepts a certain public key, then send the signature; receiving SSH_MSG_USERAUTH_PK_OK only acknowledges the candidate, not that login is complete. Complete success is indicated by SSH_MSG_USERAUTH_SUCCESS. RFC 4252 §5, 7
Method or Mechanism
Actual Function
publickey
Verifies control over the corresponding private key, while checking authorization rules for that user
password
Submits password within the protected SSH transport; the server still accesses the password
keyboard-interactive
General challenge/response, can carry OTP, PAM, etc.
User Certificates
Adds CA signature, principal, validity period, and constraints to the public key, still requiring private key proof
TOTP is not a directly named method in the base SSH user authentication protocol. The order of authentication attempts is also not "publickey, OTP, password" as specified by the protocol; client preferences, server-allowed methods, and multi-factor policies jointly determine the process. The server can report that a method has partially succeeded but another method is still needed; this is different from a complete failure caused by an incorrect private key.
OpenSSH's AuthenticationMethods publickey,keyboard-interactive can express a two-step policy, but whether the second step is TOTP depends on the server's interactive authentication configuration. Keys in authorized_keys can also come with command or forwarding restrictions; possessing an accepted key does not equate to gaining an unrestricted shell. sshd_config
Binary Packets: Boundaries First, Then Messages
The base transport packet includes 4 B packet_length, 1 B padding_length, payload, random padding, and integrity data processed according to the algorithm definition. packet_length does not include its own 4 B, nor does it include the MAC at the end of the traditional format; padding is at least 4 B. Do not mechanically apply this base description to the encryption scope of all AEAD or Encrypt-then-MAC algorithms.
Below is a pedagogical SSH_MSG_IGNORE packet constructed for the initial unencrypted phase: message type 1 B, string length 4 B, hello 5 B, totaling 10 B payload. Aligned to 8 B, the 5 B header plus 10 B payload requires 9 B padding; the complete packet is 24 B, with packet_length being 20.
importstructpayload=bytes([2])+struct.pack('>I',5)+b'hello'padding=4while(5+len(payload)+padding)%8:padding+=1# Fixed bytes are only for reproducible encoding demonstration; real sends use required random padding.
packet=struct.pack('>IB',1+len(payload)+padding,padding)packet+=payload+bytes(range(padding))assertlen(payload)==10andpadding==9assertlen(packet)==24assertstruct.unpack('>I',packet[:4])[0]==20assertpacket[5]==2andpacket[10:15]==b'hello'print('Base SSH packet: payload 10 B, padding 9 B, total length 24 B')
A single TCP read may only capture part of a packet, or it may contain multiple packets. The receiver must accumulate and verify based on length, rather than judging the number of SSH messages by the number of socket reads. RFC 4253 §6
Channels: Independent Numbers and Windows, Shared TCP
A session channel can request a shell, exec, or subsystem; SFTP is an application protocol within a subsystem. Port forwarding commonly uses direct-tcpip or forwarded-tcpip. This is not a fixed numbering table where "channel 0 is always shell, channel 1 is always forwarding."
Both sides allocate local channel IDs independently. If the client opens using 7, and the server confirms and allocates 42, then subsequent data from the client to the server uses recipient 42, and data from the server back to the client uses recipient 7. Numbers are only meaningful within the respective connection and endpoint.
Each direction has a receive window and a maximum channel packet size; the amount sent is limited by both. After the receiver processes and frees the buffer, it uses WINDOW_ADJUST to increase the other party's send quota; stdout and extended data like stderr must also comply with the channel window and cannot accumulate infinitely.
window, max_packet, queued=65536, 32768, 90000sent=[]whilewindowandqueued:n=min(window,max_packet,queued)sent.append(n)window-=nqueued-=nassertsent==[32768,32768]andqueued==24464assertwindow==0# At this point, you must wait for the window in this direction to replenish.
window+=16384n=min(window,max_packet,queued)assertn==16384andqueued-n==8080print('Window exhausted, paused; WINDOW_ADJUST only restores the corresponding quota')
Independent windows prevent a slow channel from eating up the application buffers of other channels, but they do not eliminate head-of-line blocking caused by underlying TCP packet loss. CHANNEL_EOF indicates that no more data will be sent in that direction, while the reverse direction can continue; only CHANNEL_CLOSE participates in closing the entire channel. The command's exit-status is a different type of channel request and cannot be replaced by "normal TCP closure" for business exit codes. RFC 4254 §5–6
Sent to 42, Why Does the Reply Write 7?
When the direction arrow changes, the recipient takes the local number saved by the peer. Increasing the window does not break the 32768 single-packet limit; reducing the queued data does not require filling the window. These three quantities have different managers and lifecycles.
Preparing the visual
Sent to 42, Why Does the Reply Write 7?
Channel numbers are allocated independently by both ends; sending uses the peer's number. Single data volume is limited by direction window, max packet, and queued amount.
{"id":"ssh-channel-direction","title":"Sent to 42, Why Does the Reply Write 7?","summary":"Channel numbers are allocated independently by both ends; sending uses the peer's number. Single data volume is limited by direction window, max packet, and queued amount.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"mode\"><button data-mode=\"cs\" data-i18n=\"cs\"></button><button data-mode=\"sc\" data-i18n=\"sc\"></button></div><div class=\"control\"><label for=\"window\" data-i18n=\"window\"></label><output for=\"window\" id=\"window-value\"></output><input id=\"window\" type=\"range\" min=\"0\" max=\"65536\" value=\"65536\" step=\"1024\"></div><div class=\"control\"><label for=\"queued\" data-i18n=\"queued\"></label><output for=\"queued\" id=\"queued-value\"></output><input id=\"queued\" type=\"range\" min=\"0\" max=\"98304\" value=\"90112\" step=\"1024\"></div><svg id=\"direction\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\" data-i18n-label=\"heading\"></svg><div class=\"metrics\"><div><span data-i18n=\"recipient\"></span><strong id=\"recipient\"></strong></div><div><span data-i18n=\"size\"></span><strong id=\"size\"></strong></div></div><p id=\"equation\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let mode='cs';function draw(){const w=+$('#window').value,q=+$('#queued').value,n=Math.min(w,q,32768);set('window-value',w);set('queued-value',q);graph('direction',[{name:'C',x:70,y:75,sub:'local 7'},{name:'S',x:290,y:75,sub:'local 42'}],mode==='cs'?[[0,1,true]]:[[1,0,true]]);set('recipient',mode==='cs'?42:7);set('size',n);set('equation','send = min('+w+',32768,'+q+') = '+n);$('#size').dataset.value=n;pressed(mode);}document.querySelectorAll('input').forEach(e=>e.oninput=draw);document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;draw()});draw();","audio":false,"strings":{"cs":"Client → Server","heading":"Sent to 42, Why Does the Reply Write 7?","intro":"Select send direction, queued data, and recipient quota to view the current recipient and maximum sendable segment.","mode":"Send Direction","queued":"Queued Data (B)","recipient":"Message Recipient Number","reset":"Reset","sc":"Server → Client","scope":"Single channel, client local 7, server local 42; max packet 32768, window slider represents receive quota for the currently selected send direction. Independent snapshot, does not simulate TCP and authentication.","scopeLabel":"Model Scope and Conventions","size":"Sendable / B","window":"Remaining Window in This Direction (B)"}}
Forwarding: Who Listens, Who Resolves the Target, Where Encryption Ends
The following two are command examples for on-demand use; gateway.example and internal.example need to be replaced with your own hosts.
# Access from local 8080, connect to internal.example:80 via gateway.ssh -N -o ExitOnForwardFailure=yes \
-L 127.0.0.1:8080:internal.example:80 user@gateway.example# Access from gateway's loopback 3000, connect to local 22 via SSH client.ssh -N -o ExitOnForwardFailure=yes \
-R 127.0.0.1:3000:localhost:22 user@gateway.example
The target name in -L is usually resolved and connected by the SSH server; localhost in the above -R is on the SSH client side. The listen address limits who can connect, but does not determine which side the target is on. Whether remote listening can bind to non-loopback addresses is also affected by server policies such as GatewayPorts.
SSH encryption terminates at both ends of SSH; the segment where the server then connects to internal.example:80 does not automatically gain SSH encryption. If the target runs HTTPS, application TLS can continue to protect traffic to the target service. -N does not request remote commands; ExitOnForwardFailure can detect failures in establishing listening or forwarding, but does not guarantee success for every future connection to the target. ssh Forwarding Options
Requirement
Mechanism
Boundary
Fixed TCP Service
-L / -R
Maps connections, not a full IP VPN
Application Selects Multiple TCP Targets
-D Dynamic SOCKS
Application must use proxy; not general UDP forwarding
Login to Target Host via Jump Host
-J ProxyJump
Still requires verifying target host identity
Remote Calls Local Signing Agent
Agent Forwarding
Private keys are not directly copied, but the remote side can request signatures within authorized scope
ProxyJump usually does not require forwarding the agent to the jump host: the client can establish its own SSH connection to the final target via the TCP channel forwarded by the jump host. Agent forwarding serves a different purpose and cannot be treated as "private key files didn't leave the local machine, so the remote side cannot use the identity." ssh Agent and Jump Host
Locate by Phase, Instead of Uniformly Attributing "Wrong Key"
Phenomenon
Priority Check
Cannot connect TCP
Address, port, routing, service listening, and connection policies
No common algorithms
Error belongs to KEX, host signature, or transport algorithm category
Host verification failed
Target name, port mapping, trusted records, and planned key rotation
User authentication failed
Login username, candidate identities, server methods, and authorization constraints
Features denied after login
Subsystem, command restrictions, forwarding policies, and target connections
Transmission stuck or connection interrupted
Channel backpressure, TCP packet loss, idle state, and timeouts
When you only want to check command parsing, you can use ssh -G -F /dev/null to output effective options without reading personal configuration; -G does not establish an SSH session. For actual connection debugging, use -v or -vvv to compare against the failure phase. Keep-alives are used to discover unresponsive connections, not as proof that business processing is complete; after reconnecting, do not assume that commands started previously were not executed.