Turbocharge development: the magic of SSH port forwarding
Security policies block database ports. Firewalls prevent external connections. Remote services remain inaccessible except through carefully controlled channels. SSH port forwarding creates encrypted tunnels that make distant services appear local—you connect to localhost whilst traffic routes securely to remote resources, maintaining security boundaries without compromising workflow efficiency.I was debugging a database connection issue on a production server when the network administrator informed me that direct database access from external networks was prohibited. Security policy. The application worked fine—it connected to the database through the internal network—but I couldn't run diagnostic queries from my local machine. I needed to inspect query performance, check indexes, examine slow query logs. The options were either SSH into the server and work from there, abandoning my local tools and database clients, or find a way to access the remote database as if it were running locally.
SSH port forwarding solved this in one command. I created a tunnel that made the remote database appear on my local machine. My database client connected to localhost, the traffic routed through an encrypted SSH connection to the server, and from the database's perspective, connections came from localhost on the server itself—exactly where the security policy expected them to originate. I kept my local tools, the security policy remained satisfied, and the debugging took twenty minutes instead of the hours it would have required working directly on the server.
This is what SSH port forwarding does: it creates secure tunnels between your local machine and remote resources, making distant services appear local whilst maintaining encryption and respecting security boundaries. The technique extends far beyond database access—development servers behind firewalls, web applications in containers, APIs running in private networks, any service listening on a port becomes accessible through SSH's encrypted channel.
SSH itself—Secure Shell—forms the foundation. It's the protocol that replaced Telnet and other unencrypted remote access methods decades ago. When you connect to a remote server via SSH, everything transmits through an encrypted channel. Commands you type, responses the server sends, files you transfer—all encrypted. Anyone intercepting the network traffic sees gibberish. SSH provides authentication, ensuring you're connecting to the server you expect and that the server trusts your credentials. The combination of encryption and authentication made SSH the standard for remote server access.
Port forwarding extends SSH's encrypted tunnel to redirect network traffic. Instead of just providing a remote shell, SSH can forward traffic from a port on your local machine through the encrypted connection to a port on the remote server, or even to ports on machines the remote server can reach. From your local machine's perspective, you're connecting to localhost. From the remote service's perspective, the connection originates from the SSH server. The tunnel bridges them securely.
Basic SSH connections
Before exploring port forwarding, understanding basic SSH connections provides context. The standard SSH connection opens a remote shell—you execute commands on the remote server as if you were sitting at its console. The basic syntax is straightforward:
ssh username@server_address
This connects to the remote server using your username. SSH defaults to port 22, the standard SSH port. If the server administrator configured SSH on a different port—common practice to reduce automated attack attempts—specify the port explicitly:
ssh username@server_address -p 2222
The -p flag designates the port. Many servers use non-standard ports as security through obscurity, making automated port scans less likely to find the SSH service. It's not robust security—determined attackers scan all ports—but it reduces noise from opportunistic bots.
SSH supports two primary authentication methods: passwords and public key cryptography. Password authentication is simplest—you connect, SSH prompts for your password, you type it, authentication succeeds or fails. But passwords have problems. They transmit over the network (encrypted, but still transmitted). They're vulnerable to brute force attacks. Users choose weak passwords. Password authentication works but isn't ideal.
Public key authentication eliminates these issues. You generate a key pair—one private key that never leaves your machine, one public key you place on the server. The private key proves your identity through cryptographic signatures without ever transmitting the key itself. To use key-based authentication, specify your private key:
ssh -i ~/.ssh/id_rsa username@server_address
The -i flag points to your private key file. SSH uses this key to authenticate without passwords. Most developers configure their SSH client to automatically use the correct key for each server, eliminating even this flag. Key-based authentication is both more secure and more convenient—once configured, you connect without typing passwords whilst maintaining stronger security than password authentication provides.
These basic connections establish shells on remote servers. You're logged in, executing commands, working directly on the remote machine. But this isn't always what you want. Sometimes you need access to a specific service running on that server—a database, a web server, an API—without needing a shell. This is where port forwarding becomes essential.
Local port forwarding
Local port forwarding—the most common type—makes a remote service appear on your local machine. You specify a local port, and SSH forwards connections to that port through the encrypted tunnel to a destination on or accessible from the remote server. The syntax follows this pattern:
ssh -L local_port:destination_host:destination_port username@ssh_server
The -L flag specifies local port forwarding. The first number is the local port you'll connect to. The destination host and port specify where the traffic should go once it reaches the remote server. The SSH server is the machine you're connecting to via SSH.
Consider the database example from earlier. The remote server runs MySQL on port 3306, but firewall rules prevent direct external access. Local port forwarding creates a tunnel:
ssh -nNL 3306:127.0.0.1:3306 username@server_address
Breaking down this command: -L 3306:127.0.0.1:3306 creates local port forwarding from your local port 3306 to 127.0.0.1:3306 from the perspective of the SSH server. The 127.0.0.1 refers to localhost on the remote server, not your local machine. From the remote server's point of view, connections originate from localhost—exactly where the database expects them.
The -n flag prevents SSH from reading stdin, useful when running in the background. The -N flag tells SSH not to execute any remote commands—this connection exists purely for port forwarding, not for running a shell. Together, these flags create a forwarding-only tunnel that runs quietly in the background.
Now your database client connects to localhost:3306 on your local machine. MySQL Workbench, DataGrip, the mysql command-line client—they all connect to localhost. But the connection travels through the SSH tunnel to the remote database. From your perspective, the database is local. From the database's perspective, connections come from localhost on the server. The firewall stays closed to external connections whilst your local tools work as if the database were running on your machine.
The destination doesn't have to be localhost on the remote server. The SSH server can forward to any host it can reach:
ssh -nNL 3306:internal-db.company.local:3306 username@bastion.company.com
This forwards your local port 3306 to internal-db.company.local:3306, as seen from bastion.company.com. The bastion host acts as a jump point into the internal network. You connect to localhost:3306, traffic routes through the bastion to the internal database server. This pattern—using SSH to reach otherwise inaccessible internal services—appears constantly in corporate environments where security policies segment networks.
Dynamic port forwarding
Dynamic port forwarding creates a SOCKS proxy on your local machine. Instead of forwarding a specific port to a specific destination, dynamic forwarding lets applications route traffic through the SSH tunnel to arbitrary destinations. The SSH client acts as a SOCKS proxy server, and applications configured to use that proxy send traffic through the tunnel.
ssh -nND 1080 username@server_address
The -D flag enables dynamic forwarding on local port 1080. The -n and -N flags work as before—prevent stdin reading and disable remote command execution. Now configure your browser or other applications to use localhost:1080 as a SOCKS proxy. All traffic from that application routes through the SSH tunnel to the remote server, then out to its destination from the remote server's network.
This has practical applications beyond the obvious privacy implications. Testing geo-restricted features becomes straightforward—connect through a server in the appropriate region and your traffic originates from there. Accessing internal company resources from external networks works when the company provides SSH access to an internal bastion host. Development scenarios where you need to test how your application behaves when accessed from different network contexts benefit from dynamic forwarding.
The browser sees only localhost:1080 as the proxy. It sends all requests through that proxy. SSH handles the actual forwarding, routing traffic through the encrypted tunnel to destinations the remote server can reach. From the destination's perspective, requests originate from the SSH server's IP address, not your local machine.
Multiple forwards and persistent tunnels
Real workflows often require multiple forwarded ports simultaneously. A web application might need database access, Redis access, and Elasticsearch access—three separate services, three separate ports. SSH supports multiple -L flags in a single connection:
ssh -nN \
-L 3306:db.internal:3306 \
-L 6379:redis.internal:6379 \
-L 9200:es.internal:9200 \
username@bastion.company.com
One SSH connection, three forwarded ports. Your local machine now has localhost:3306 forwarding to the database, localhost:6379 forwarding to Redis, localhost:9200 forwarding to Elasticsearch. Configure your application to use these localhost addresses, and it connects to all three services through a single encrypted tunnel.
These tunnels need to persist. SSH connections time out, networks change, laptops sleep. Manually reconnecting every time becomes tedious. Tools like autossh automatically restart SSH connections when they fail, maintaining persistent tunnels:
autossh -M 0 -nN \
-o "ServerAliveInterval 30" \
-o "ServerAliveCountMax 3" \
-L 3306:db.internal:3306 \
username@bastion.company.com
The -M 0 disables autossh's monitoring port (using SSH's built-in keepalive instead). ServerAliveInterval sends keepalive packets every 30 seconds. ServerAliveCountMax declares the connection dead after three failed keepalives. When the connection fails, autossh automatically reconnects, maintaining the tunnel indefinitely.
For permanent infrastructure, systemd services or launch agents handle tunnel management, starting them on boot and restarting them on failure. SSH tunnels become invisible infrastructure—always present, requiring no manual intervention, making remote services feel genuinely local.
The value of SSH port forwarding becomes obvious the moment you encounter a remote service you can't access directly. Security policies block database ports. Firewalls prevent external connections. Services run on internal networks without public IPs. These aren't edge cases—they're the default state of production infrastructure. Security demands services remain inaccessible except through carefully controlled channels.
SSH port forwarding provides that controlled channel whilst maintaining encryption and audit trails. You authenticate via SSH, the connection is logged, traffic flows through an encrypted tunnel, and services receive connections from localhost on the SSH server—exactly where security policies expect them. The security boundary stays intact. No firewall rules need changing. No exceptions get carved out. The policy remains "deny all external connections" whilst you access services as if they were local.
The workflow improvements compound over time. Initially, port forwarding seems like a convenience—you avoid installing tools on remote servers or working in unfamiliar environments. But the real benefit emerges when this becomes infrastructure. Scripts assume localhost addresses for remote services. Documentation references localhost:3306 for database connections, and everyone on the team knows that means "forward the port" rather than "install MySQL locally." Configuration stays simple and consistent. The complexity of remote access hides behind a single SSH command.
Team consistency particularly matters. Without port forwarding, developers either expose services to external networks—creating security risks—or each person configures access differently, leading to documentation that works for one person's setup but fails for everyone else's. Port forwarding provides a standard approach: everyone connects to localhost, everyone uses the same ports, everyone establishes tunnels the same way. The shared mental model makes collaboration straightforward.
The alternative to port forwarding typically involves either compromising security or adding complexity. Opening database ports to external networks violates security policies and creates attack surface. VPNs provide network-level access but require infrastructure, client software, and configuration management. SSH tunnels require only SSH access—something most developers already have—and work identically across platforms. The barrier to entry is minimal whilst the capability is powerful.
That database debugging session that prompted this article took twenty minutes because port forwarding made the remote database accessible to local tools. Without it, I would have spent hours working directly on the server, using unfamiliar tools, or installing software just to run diagnostic queries. The tunnel made the remote database feel local. My tools worked. The workflow felt natural. The debugging proceeded efficiently.
SSH port forwarding turns remote services into local infrastructure. Networks collapse into localhost addresses. Firewalls stay closed whilst development proceeds unimpeded. Security boundaries remain enforced whilst productivity doesn't suffer. It's infrastructure that becomes invisible once established—always working, rarely thought about, solving access problems so consistently that you forget they were ever problems.
Published on:
Updated on:
Reading time:
10 min read
Article counts:
40 paragraphs, 1,990 words
Topics
TL;DR
Production infrastructure keeps services inaccessible by design—databases block external ports, firewalls prevent direct connections, internal networks lack public IPs. SSH port forwarding creates encrypted tunnels where local ports connect to remote services through the SSH server. Local port forwarding makes remote databases appear on localhost, dynamic forwarding creates SOCKS proxies for arbitrary destinations, multiple tunnels run simultaneously through single connections. Tools like autossh maintain persistent tunnels that survive disconnections. The security boundary stays intact whilst local tools access remote resources naturally. Configuration becomes consistent across teams: everyone connects to localhost, documentation references standard ports, complexity hides behind single commands. Remote services become local infrastructure—firewalls closed, productivity unimpeded, access problems solved invisibly.