Securing Internal Services with SSH Local Port Forwarding
Learn to use SSH local port forwarding to securely reach internal services like private databases through an encrypted tunnel, without opening firewall ports to the internet.
06 Sept 2025, 20:44 UTC

You have a database, a legacy web dashboard, or a management tool running on a private network that shouldn't be exposed to the public internet. The instinct is to open a firewall port for your IP, but that increases the attack surface. A more robust engineering alternative is SSH port forwarding, which creates an encrypted tunnel to reach internal services through an existing SSH gateway.
SSH local port forwarding maps a port on your local machine to a service reachable by the remote SSH server. This effectively treats the SSH server as a secure proxy, letting you interact with private resources as if they were running on your own localhost.
How SSH Local Port Forwarding Works
When you initiate a local port forward, the SSH client listens on a specific port on your local machine. Any traffic sent to that local port is encrypted and sent through the established SSH tunnel to the remote server. The remote server then decrypts the traffic and forwards it to the destination address and port you specified.
This is particularly useful for wrapping unencrypted protocols—like older VNC instances or database connections—inside an encrypted SSH layer, protecting them over untrusted networks such as public Wi-Fi.
Practical Example: Accessing a Private PostgreSQL Instance
Imagine a PostgreSQL database running on private IP 10.0.0.50 within a remote VPC. You have SSH access to a bastion host at gateway.example.com that can reach that internal subnet.
Run the following command in a terminal on your local machine (no elevated privileges needed if you pick a local port above 1024):
ssh -L 5432:10.0.0.50:5432 [contact removed]
Breakdown of the command:
-L: Indicates local port forwarding.5432: The port on your local machine you will connect to.10.0.0.50:5432: The destination address and port from the perspective of the SSH server.[contact removed]: Your SSH login on the gateway.
Once the tunnel is active, point your local database tool to localhost:5432. The traffic is securely tunneled through the gateway to the database. Keep the SSH session open for as long as you need the tunnel; closing it tears the tunnel down.
Verifying the Connection
To verify the tunnel is listening and responding, use nc or ss on your local machine:
# Check if the local port is listening ss -tln | grep 5432 # Test the connection to the forwarded service nc -zv localhost 5432
If the connection fails, check the server-side SSH logs (commonly /var/log/auth.log on Debian-based systems, or via journalctl -u sshd) to confirm the forwarding request was authorized. A server-side setting of AllowTcpForwarding no in sshd_config disables this feature entirely.
Limitations and Security Trade-offs
While powerful, SSH tunneling has specific constraints to keep in mind:
- TCP only: Standard SSH port forwarding only supports TCP traffic. Tunneling UDP (like DNS) requires additional tools such as
socat. - TCP-over-TCP overhead: Wrapping a TCP stream inside another TCP stream means packet loss can cause the nested congestion control mechanisms to conflict, degrading throughput on high-latency links.
- Binding risks: By default, SSH binds the forwarded port to
127.0.0.1. If you explicitly bind to0.0.0.0, anyone on your local network can reach the forwarded service—an easy way to accidentally expose internal resources.
Use SSH port forwarding as your primary method for ad-hoc administrative access to private resources. Always verify your bind addresses, and prefer keeping tunnels bound to localhost unless you have a deliberate reason to share them.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.