Loopback Binding and SSH Tunnel
Loopback Binding and SSH Tunnel
Some ports need to be reachable from the internet, others should only be used inside the same host. The simplest tool to separate the two is the bind address. When a path that only outsiders can take is needed, an SSH tunnel is the lightweight answer. This piece covers the difference between 0.0.0.0 and 127.0.0.1, Docker port mapping, SSH port forwarding (-L, -R, -D), VPN tool comparisons, and an operational model for minimizing external exposure.
1. About Bind Addresses
| Address | Meaning |
|---|---|
0.0.0.0 |
Bind to every interface (externally reachable). |
127.0.0.1 |
Loopback. Only reachable from the same host. |
:: / ::1 |
IPv6 every interface / loopback. |
<specific IP> |
Only that interface. |
The bind address of a listening socket determines external reachability. If the firewall is the first line of defense, the bind address is the decision before that.
2. Docker Port Mapping
Port notation in Compose and docker run:
ports:
- "8080:8080" # exposed on the host's 0.0.0.0:8080
- "127.0.0.1:8080:8080" # exposed only on host loopback
- "8080" # arbitrary host port → container 8080
In production, a common model is to open only services that need external exposure (web, proxy) on 0.0.0.0, and keep DBs, caches, and admin UIs on loopback. When external access is needed, an SSH tunnel temporarily passes through.
3. SSH Port Forwarding
The three forwarding options of ssh:
| Option | Meaning |
|---|---|
-L |
Local port → some port on a remote host (local forwarding). |
-R |
Remote port → some port on the local machine (remote forwarding). |
-D |
A SOCKS proxy on the local machine (dynamic forwarding). |
-L — production DB access:
ssh -L 5432:127.0.0.1:5432 user@server
Connecting to local 5432 on the PC routes through the SSH tunnel to the server's loopback 5432 (the DB). The server's DB port doesn't need to be open to the internet.
-R — temporarily exposing an internal dev server:
ssh -R 8080:127.0.0.1:3000 user@server
Connections coming into the server's 8080 land on the local PC's 3000. The server may need GatewayPorts yes.
-D — SOCKS proxy:
ssh -D 1080 user@server
A SOCKS5 proxy on the local PC's 1080. Browsers and tools route through the server.
4. VPN Tools
| Tool | Note |
|---|---|
| WireGuard | Started in 2016. Linux kernel integration (2020). A simple, fast, modern VPN. |
| Tailscale | 2019. A mesh VPN built on WireGuard. Direct device-to-device connections plus coordination. |
| OpenVPN | 2001. An older SSL / TLS-based VPN. |
If an SSH tunnel is light enough for one or two ports of temporary exposure, a VPN is the place to stably stitch together many ports across many hosts. Tailscale comes up frequently for NAT traversal and zero-config setup.
5. Minimizing External Exposure
When several layers of defense overlap, the innermost decision is the bind address.
- Cloud security groups / Network ACLs.
- Host-level
ufw(Linux) or Windows Defender Firewall. - Container network isolation.
Even if a cloud security group is open to 0.0.0.0/0, a service bound to 127.0.0.1 is unreachable from outside (and vice versa).
6. bastion / jump host
Funnel access to multiple internal hosts through a single bastion:
# ~/.ssh/config
Host db1
Hostname 10.0.1.5
ProxyJump bastion
Host bastion
Hostname bastion.example.com
ssh db1 alone goes through bastion to db1. SSH's ProxyJump (-J) is standard from OpenSSH 7.3+.
7. A Typical Production Server Layout
- 80 / 443 — externally exposed (Caddy or nginx).
- 22 — externally exposed (SSH). Prefer key-based auth, fail2ban, and a non-default port.
- Every other port —
127.0.0.1binding plus access only via SSH tunnel.
services:
app:
ports:
- "127.0.0.1:8080:8080"
db:
ports:
- "127.0.0.1:5432:5432"
caddy:
ports:
- "80:80"
- "443:443"
Only the Caddy container reaches outside; the rest are only accessible through host loopback. Call them through an SSH tunnel or directly inside the host shell.
8. Reaching the Production DB from a Dev PC
ssh -L 15432:127.0.0.1:5432 user@server
psql -h 127.0.0.1 -p 15432 -U app appdb
Using local 15432 instead of 5432 avoids collisions even if a local DB is running on 5432.
9. Common Pitfalls
Unintended exposure on 0.0.0.0 — Compose's "PORT:PORT" notation defaults to 0.0.0.0. Copy-paste without review accumulates external exposure.
Permission limits of -R — the default SSH config binds remote forwards to loopback. Opening external access requires GatewayPorts yes on the server, which carries its own security review.
Tunnels going silent — when the network drops, SSH sessions can die quietly. ServerAliveInterval or autossh help.
Confusing responsibility between firewall, security group, and bind address — hard to track which layer is blocking. Inspect one layer at a time when changing things.
User-defined Docker networks vs host exposure — container-to-container communication is on the user network; external exposure is on explicit ports. Don't mix the two.
Closing thoughts
Loopback binding plus SSH tunnels together compress the production server's security surface to three ports — 80 / 443 / 22. A single carelessly written "5432:5432" line in compose can be the start of a production incident. Worth defaulting every new container to a 127.0.0.1 prefix.
Next
- single-server-philosophy
- local-https-mkcert
Refer to the OpenSSH manual, the Docker networking overview, Tailscale, WireGuard, the Mozilla SSH guidelines, and ProxyJump (OpenSSH).