Port Forwarding
SSH port forwarding (SSH tunneling) routes TCP traffic through an encrypted SSH connection. The core idea: if your machine cannot reach a service directly — because it sits behind a firewall or in a separate network segment — but an SSH server can, you can borrow that connectivity by tunneling through the SSH connection.
In an audit, this matters in two directions: understanding what traffic the intercepted client is tunneling, and using those tunnels yourself to reach internal services that are otherwise inaccessible.
Types of Port Forwarding
There are three types of port forwarding with SSH:
Local port forwarding: opens a port on your machine; connections to that port are forwarded through the SSH server to a destination. Useful for reaching services that are only reachable from the server side.
Remote port forwarding: opens a port on the SSH server; connections to that port are forwarded back through the tunnel to your machine. Useful for exposing a local service to anyone on the server side.
Dynamic port forwarding: turns the SSH client into a SOCKS proxy. Each application request is forwarded through the server to its own destination — no fixed target required.
The names refer to where the port is opened: local means on your own machine, remote means on the SSH server.
Warning
Traffic monitoring and filtering is usually in place for a reason. Only use port forwarding in audits that explicitly authorise it.
Local Port Forwarding
Local port forwarding opens a port on Alice’s laptop. Any connection to that
port travels through the SSH tunnel to dev-server and from there to the
destination — which only needs to be reachable from dev-server, not from
Alice’s laptop directly.
Scenario: Logfile Inc.’s internal employee portal (portal.logfile.internal)
runs on plain HTTP — it is only reachable from within the corporate network, so
no one bothered adding TLS. Alice opens a local tunnel so she can access it
from her laptop:
ssh -L 8080:portal.logfile.internal:80 alice@dev-server
The -L format is LOCAL_PORT:DESTINATION_HOST:DESTINATION_PORT:
8080— port opened on Alice’s laptopportal.logfile.internal— destination host, resolved bydev-server80— destination port on that host
For the duration of the session, http://localhost:8080/ in Alice’s browser
connects to the internal portal through the tunnel.
flowchart TD
classDef client fill:#dae8fc,stroke:#6c8ebf
classDef server fill:#d5e8d4,stroke:#82b366
classDef external fill:#fff2cc,stroke:#d6b656
classDef dest fill:#e1d5e7,stroke:#9673a6
subgraph alice["Alice's laptop"]
B(["Browser"])
L["SSH client"]
end
subgraph corp["Logfile Inc. network"]
D["dev-server"]
P(["portal.logfile.internal:80"])
end
B -.->|"request localhost:8080"| L
L -->|"SSH tunnel · -L 8080:portal.logfile.internal:80"| D
D -.->|"plain HTTP"| P
class L client
class D server
class B external
class P dest
Remote Port Forwarding
Remote port forwarding opens a port on dev-server. Connections to that
port travel back through the tunnel to Alice’s laptop and from there to the
destination.
Scenario: Alice is running a local development server on her laptop
(port 3000) and wants to show it to a colleague who is logged in on
dev-server. She opens a remote tunnel:
ssh -R 8000:localhost:3000 alice@dev-server
The -R format is REMOTE_PORT:DESTINATION_HOST:DESTINATION_PORT:
8000— port opened ondev-serverlocalhost— destination host, resolved by Alice’s laptop (herself)3000— destination port on that host
For the duration of the session, anyone on dev-server can reach
localhost:8000 and the request is forwarded to Alice’s laptop on port 3000.
flowchart LR
classDef client fill:#dae8fc,stroke:#6c8ebf
classDef server fill:#d5e8d4,stroke:#82b366
classDef external fill:#fff2cc,stroke:#d6b656
subgraph alice["Alice's laptop"]
L["SSH client<br/>(localhost:3000)"]
end
subgraph corp["Logfile Inc. network"]
direction TB
D["dev-server"]
C(["Colleague"])
end
L -->|"SSH tunnel<br/>-R 8000:localhost:3000"| D
C -.->|"request localhost:8000"| D
D -.->|"forwarded via tunnel<br/>to localhost:3000"| L
class L client
class D server
class C external
Dynamic Port Forwarding
Dynamic port forwarding turns the SSH connection into a SOCKS proxy. Unlike
local port forwarding, there is no fixed destination — each application
request is forwarded individually, so Alice can reach any host that
dev-server can reach.
Note
The SOCKS protocol never reaches the server. The OpenSSH client handles
it locally: it accepts SOCKS5 connections from applications and converts
each one into a standard direct-tcpip channel — the same mechanism
used for local port forwarding. dev-server sees individual connection
requests with explicit destinations; it never processes SOCKS.
Scenario: Alice needs to reach several internal services during an audit session. Instead of opening a separate local tunnel for each one, she opens a single SOCKS proxy:
ssh -D 1080 alice@dev-server
Any application that supports SOCKS5 (browser, curl, vulnerability scanner)
can now route traffic through dev-server to the internal network. The
tunnel closes when Alice closes the SSH session.
flowchart LR
classDef client fill:#dae8fc,stroke:#6c8ebf
classDef server fill:#d5e8d4,stroke:#82b366
classDef external fill:#fff2cc,stroke:#d6b656
classDef dest fill:#e1d5e7,stroke:#9673a6
subgraph alice["Alice's laptop (SOCKS proxy: localhost:1080)"]
direction TB
L["SSH client"]
B1(["Browser"])
B2(["curl / scanner"])
end
subgraph corp["Logfile Inc. network"]
direction TB
D["dev-server"]
P1(["portal.logfile.internal"])
P2(["any internal host"])
end
B1 -.->|"via SOCKS"| L
B2 -.->|"via SOCKS"| L
L -->|"SSH tunnel<br/>-D 1080"| D
D -.-> P1
D -.-> P2
class L client
class D server
class B1,B2 external
class P1,P2 dest
Bastion hosts
A bastion host is a hardened, publicly reachable machine that acts as the
entry point to a network. SSH’s ProxyJump (-J) chains the connection
through the bastion transparently.
Scenario: dev-server is the only machine at Logfile Inc. reachable
from outside. Alice needs to connect directly to an internal database server
(db-server.logfile.internal) that is firewalled from the internet.
ProxyJump lets her do it in one command:
ssh -J alice@dev-server alice@db-server.logfile.internal
SSH establishes the first connection to dev-server and uses it as a relay
to db-server.logfile.internal. Alice needs only one command; dev-server
sees an encrypted relay channel, not a shell session.
You can also specify different users and ports:
ssh -J alice@dev-server:22 alice@db-server.logfile.internal:2222
Note
ProxyJump uses a direct-tcpip channel on the bastion rather than
a full shell session. SSH-MITM intercepts and rewrites these channels, so
the forwarded connection remains visible at the proxy layer — but because
the payload is an independent SSH session, its contents are encrypted from
SSH-MITM’s point of view unless a second SSH-MITM instance is chained.
Port forwarding in SSH-MITM
SSH-MITM intercepts both local and remote port forwarding requests from the client and surfaces them as usable tunnels for the auditor. No extra configuration is needed — SSH-MITM handles this automatically.
Local port forwarding
When Alice opens a local port forward (-L), SSH-MITM receives the
direct-tcpip channel from her client. At the same time, SSH-MITM starts
a local SOCKS server that the auditor can use independently to reach the same
destination.
flowchart LR
classDef client fill:#dae8fc,stroke:#6c8ebf
classDef mitm fill:#f8cecc,stroke:#b85450
classDef server fill:#d5e8d4,stroke:#82b366
classDef external fill:#fff2cc,stroke:#d6b656
classDef dest fill:#e1d5e7,stroke:#9673a6
subgraph alice["Alice's laptop"]
direction TB
L["SSH client<br/>-L 8080:portal.logfile.internal:80"]
B(["Browser"])
end
subgraph intercept["Interception"]
direction TB
M["SSH-MITM"]
A(["Auditor"])
end
subgraph corp["Logfile Inc. network"]
direction TB
D["dev-server"]
P(["portal.logfile.internal:80"])
end
B -.->|"request localhost:8080"| L
L -->|"SSH tunnel"| M
M -->|"relay"| D
M -.->|"plain HTTP visible"| P
A -.->|"via SOCKS<br/>localhost:PORT"| M
class L client
class M mitm
class D server
class B,A external
class P dest
SSH-MITM prints the SOCKS details to its log as soon as the tunnel is opened:
The log shows a template — fill in the values for the intercepted tunnel:
LISTEN_PORT— any free local port, e.g.8080DESTINATION_ADDR— the tunneled destination, e.g.portal.logfile.internalDESTINATION_PORT— the destination port, e.g.80
For the portal scenario:
socat TCP-LISTEN:8080,fork socks5:127.0.0.1:portal.logfile.internal:80,socksport=39859
Because portal.logfile.internal uses plain HTTP, the traffic is
unencrypted — the auditor can read every request and response in full, even
though it arrived over an SSH tunnel.
Remote port forwarding
When Alice opens a remote port forward (-R), she is asking dev-server
to listen on a port and forward connections back to her. SSH-MITM sits in
the middle and also exposes that connection locally.
flowchart LR
classDef client fill:#dae8fc,stroke:#6c8ebf
classDef mitm fill:#f8cecc,stroke:#b85450
classDef server fill:#d5e8d4,stroke:#82b366
classDef external fill:#fff2cc,stroke:#d6b656
subgraph alice["Alice's laptop"]
direction TB
L["SSH client<br/>-R 8000:localhost:3000"]
S(["Dev server<br/>localhost:3000"])
end
subgraph intercept["Interception"]
direction TB
M["SSH-MITM"]
A(["Auditor"])
end
subgraph corp["Logfile Inc. network"]
direction TB
D["dev-server"]
C(["Colleague"])
end
L -->|"SSH tunnel"| M
M --> D
C -.->|"request localhost:8000"| D
A -.->|"request<br/>localhost:PORT injector"| M
M -.->|"forwarded via tunnel"| L
L -.->|"localhost:3000"| S
class L client
class S dest
class M mitm
class D server
class C,A external
SSH-MITM prints the injector details to its log:
The auditor can connect to the injector port alongside the original client. This makes it possible to inspect or interact with Alice’s exposed service during an audit without disrupting her session.
ProxyJump
SSH-MITM intercepts ProxyJump (-J) connections and rewrites the
forwarded channel. From the client’s perspective the jump succeeds normally;
SSH-MITM sees and controls the relay.
Because the payload of a ProxyJump is an independent SSH session, its
contents are encrypted end-to-end — SSH-MITM cannot read the inner session
without chaining a second SSH-MITM instance as the final target. However,
metadata is visible: SSH-MITM knows that a jump occurred, to which
destination, and can log or redirect the connection.