ssh network

put down PuTTY. we need to talk.

The ssh man page is 2,400 lines long. You can do 90% of what you’ll ever need with about ten of them.

You downloaded PuTTY. A 3MB application — with its own key format that nothing else on earth uses — just to type a hostname and hit enter. You opened it. You saw that tree menu on the left with “Session,” “Terminal,” “Window,” “Connection” and seventeen nested submenus. You typed the hostname in the box. You clicked “Open.” A black window appeared. You felt like a hacker.

Meanwhile, everyone on Linux and macOS has been typing ssh user@host and getting on with their lives since 1999.

Unless you’re running Windows — then what the hell are you doing here anyways?! If you want to join the party and understand what the hell we’re talking about, go install WSL2. We’ll wait. Impatiently.

Lazy sysadmins can skip straight to the cheat sheet.


Connect to a thing

The entire reason PuTTY exists. One command.

ssh user@192.168.1.50

That’s it. You just did what PuTTY needed a hostname field, a port field, a connection type dropdown, and a “Saved Sessions” panel for. One command. No tree menu.

Custom port

Your server runs SSH on port 2222 because someone read a blog about “security through obscurity” and thought changing the port was a personality.

ssh -p 2222 user@192.168.1.50

Stop typing hostnames

This is where SSH goes from “fine” to “I’m never opening PuTTY again.” The SSH config file.

vim ~/.ssh/config

Add this:

Host prod
    HostName 10.0.1.50
    User admin
    Port 22

Host dev
    HostName 10.0.2.30
    User developer
    Port 2222

Host jumpbox
    HostName bastion.company.com
    User ops
    IdentityFile ~/.ssh/work_key

Now instead of typing ssh -p 2222 developer@10.0.2.30 like a caveperson, you type:

ssh dev

Two words. That’s your entire “connection profile.” PuTTY made you click through a GUI, type in every field, then click “Save” and hope you remember what you named it. SSH config is a text file. You can version control it. You can copy it between machines. You can read it six months later and know exactly what it does.

Stop typing passwords

If you’re still typing a password every time you SSH into a server, you’re living in 2004 and we need to fast-forward you.

Generate a key pair

ssh-keygen -t ed25519

Hit enter through the prompts. You now have a private key (~/.ssh/id_ed25519) and a public key (~/.ssh/id_ed25519.pub). The private key never leaves your machine. Ever. The public key goes on the server.

Copy your key to the server

ssh-copy-id user@192.168.1.50

Type your password one last time. From now on, you’re in without a password. PuTTY needed a separate application called “PuTTYgen” to do this. A separate application. To generate a key. That it then saved in a .ppk format that literally nothing else in the world uses. Let that marinate.

Port forwarding (local)

This is where SSH stops being “a way to connect to things” and starts being “a swiss army knife that makes network engineers nervous.”

Here’s the concept in plain English: local port forwarding takes something on a remote network and makes it appear on your machine. That’s it. You’re saying “hey SSH, when I go to this port on localhost, I actually mean that port on that server over there.”

You need to access a web app running on port 8080 on a remote server, but that port isn’t exposed to the internet. No problem.

ssh -L 8080:localhost:8080 user@remote-server

Let’s break down -L 8080:localhost:8080 because this syntax trips everyone up:

-L [your machine's port]:[destination host]:[destination port]
  • 8080 (first) — the port that opens on YOUR machine
  • localhost (middle) — the destination host, as seen from the remote server. “localhost” here means the remote server itself
  • 8080 (last) — the port on the destination host you want to reach

Now open http://localhost:8080 in your browser. You’re looking at the remote app as if it were running on your machine. The traffic goes: your browser → your port 8080 → encrypted SSH tunnel → remote server’s port 8080. You just punched a hole through a firewall with one flag.

Here’s a picture, because this stuff is easier to see than read:

Your Machine                    Remote Server
┌──────────┐    SSH tunnel     ┌──────────┐
│ :8080 ───┼───────────────────┼──► :8080  │
│ (you     │   (encrypted)     │  (web app)│
│  browse  │                   │           │
│  here)   │                   │           │
└──────────┘                   └──────────┘

Access a database behind a firewall

This is where “as seen from the remote server” matters. The database server only accepts connections from the app server. You have SSH access to the app server. Game over.

ssh -L 5432:db-server:5432 user@app-server

See that middle part? It’s not localhost anymore — it’s db-server. You’re telling SSH: “connect to the app server, and from there, reach out to db-server on port 5432.” The app server is the middleman.

Your Machine           App Server              DB Server
┌──────────┐  SSH     ┌──────────┐  network   ┌──────────┐
│ :5432 ───┼──────────┼──────────┼────────────┼──► :5432  │
│ (your    │ tunnel   │ (has     │ (db-server │  (postgres│
│  SQL     │          │  access  │  trusts    │   lives   │
│  client) │          │  to DB)  │  app-srv)  │   here)   │
└──────────┘          └──────────┘            └──────────┘

Now localhost:5432 on your machine connects to the database. Point your SQL client at localhost and you’re in. No VPN. No firewall rule request. No three-week ticket in the change management queue.

You can even forward multiple ports at once. Because of course you can:

ssh -L 5432:db-server:5432 -L 8080:localhost:8080 -L 6379:redis:6379 user@app-server

You just gave yourself access to the database, the web app, and Redis. One command. Three tunnels. Zero permission requests filed.

Reverse tunneling

This is port forwarding’s unhinged older sibling. Instead of pulling remote stuff to your machine, you push your local stuff out to the remote server. You make the remote server serve traffic from your machine.

The concept: reverse tunneling takes something on YOUR machine and makes it appear on the remote server. It’s local forwarding, backwards.

Why would you do this? Because you’re behind a NAT. Or a corporate firewall. Or your ISP. Your machine has no public IP. Nobody on the internet can reach you. But you have SSH access to a server that does have a public IP. Watch this:

ssh -R 9090:localhost:3000 user@public-server

Breaking down -R 9090:localhost:3000:

-R [port on remote server]:[destination host]:[destination port on YOUR side]
  • 9090 — the port that opens on the REMOTE server
  • localhost — the destination, as seen from your machine
  • 3000 — the port on your machine where your app is running

Now anyone who hits public-server:9090 gets routed to port 3000 on your local machine. Through the internet. Through the NAT. Through the firewall. Through whatever stood between you and the outside world.

The Internet         Public Server           Your Machine (behind NAT)
┌──────────┐        ┌──────────┐  SSH       ┌──────────┐
│ someone  │        │          │  tunnel    │          │
│ visits   ├───────►│ :9090 ───┼────────────┼──► :3000 │
│ :9090    │        │ (public) │(encrypted) │ (your    │
│          │        │          │            │  dev app)│
└──────────┘        └──────────┘            └──────────┘

You just exposed your local dev environment to the internet through an SSH tunnel. Your coworker in another country can see your work-in-progress. No ngrok subscription. No Docker. No deployment. One command.

Real-world example: demo your local work

You’re building a web app locally on port 3000. Your project manager wants to see it. They’re in a different office. You could deploy it somewhere. Set up a staging environment. Configure DNS. OR:

ssh -R 8080:localhost:3000 user@your-vps

Send them the URL. Done. When the demo’s over, hit Ctrl+C. The tunnel closes. Nothing to clean up. Nothing to undeploy.

Keep it alive

Tunnels die when the connection drops. Your WiFi hiccupped and your demo just went dark. Fix that:

ssh -R 9090:localhost:3000 -o ServerAliveInterval=60 -o ServerAliveCountMax=3 user@public-server

Or better yet, put it in your SSH config:

Host tunnel
    HostName public-server.com
    User admin
    RemoteForward 9090 localhost:3000
    ServerAliveInterval 60
    ServerAliveCountMax 3

Now ssh tunnel sets the whole thing up. One command.

Dynamic port forwarding (SOCKS proxy)

Turn any SSH server into a proxy. All your traffic goes through the server.

ssh -D 1080 user@remote-server

Configure your browser to use localhost:1080 as a SOCKS5 proxy. Your browsing now exits from the remote server’s IP. This is a VPN without the VPN. No client to install. No subscription. No app that runs in the background mining your browsing data.

Jump hosts

Your production server is behind a bastion host. You need to SSH to the bastion, then SSH again to the target. You’ve been doing this in two steps like it’s 2005.

ssh -J jumpbox user@internal-server

One command. Through the bastion. To the target. Or in your config:

Host internal
    HostName 10.0.5.20
    User admin
    ProxyJump jumpbox

Now ssh internal hops through the bastion automatically. PuTTY users are currently opening two windows and copy-pasting between them.

Run a command without logging in

Don’t need a shell? Don’t open one.

ssh user@server "df -h"
ssh user@server "tail -50 /var/log/app.log"
ssh user@server "systemctl restart nginx"

Execute a command, get the output, done. No interactive session. No “oh wait, I forgot to exit.” Combine it with a loop and you’ve got poor man’s Ansible:

for host in web1 web2 web3; do
    ssh $host "systemctl restart nginx"
done

Three servers restarted. No orchestration tool. No YAML files. No “infrastructure as code” conference talk.

Pair it with tmux

Here’s the thing nobody tells you: SSH sessions are fragile. Your WiFi drops, your laptop sleeps, your VPN reconnects — and your SSH session is gone. Along with whatever you were running in it.

The fix is tmux. SSH into the server, start tmux, do your work. When the connection drops — and it will — SSH back in, type tmux a, and you’re right where you left off. Your processes are still running. Your panes are still there. Nothing was lost.

ssh myserver
tmux a || tmux new -s main

That’s your new muscle memory. We wrote a whole page about tmux because it deserves one.


The flags that actually matter

Flag What it does
-p PORT Connect on a non-standard port.
-i KEY Use a specific private key file.
-L local:host:remote Local port forward. Access remote stuff locally.
-R remote:host:local Reverse tunnel. Expose local stuff remotely.
-D PORT Dynamic SOCKS proxy. Ghetto VPN.
-J HOST Jump through a bastion host.
-N No shell. Just set up the tunnel and sit there. Use with -L, -R, or -D.
-f Background the connection. Combine with -N for background tunnels.
-o ServerAliveInterval=N Send a keepalive every N seconds. Stops your tunnel from dying.
-v Verbose. For when it doesn’t work and you need to know why. -vv and -vvv for more pain.

“But PuTTY lets me—”

No.

“PuTTY saves my sessions.” So does ~/.ssh/config. Except it’s a text file you can read, copy, and version control — not a blob in the Windows registry. Yeah. PuTTY stores sessions in the registry. In 2026.

“PuTTY has a key agent (Pageant).” So does SSH. It’s called ssh-agent. It’s already running on your machine. ssh-add ~/.ssh/id_ed25519 and you’re done. No system tray icon. No separate application.

“I need PuTTY for SFTP.” No you don’t. sftp user@host. Or even better, rsync. We have a page for that.

“PuTTY supports serial connections.” Great. For the three people who still need that, you can have PuTTY. The rest of us have moved on.

“MobaXterm has tabs and an X server.” MobaXterm is 47MB. It has a “professional edition” that costs $70. To SSH into things. With tabs. You know what else has tabs? Your terminal emulator. The one you already have.

“Termius syncs my connections across devices.” So does copying your SSH config file. Or putting it in a git repo. For free. Without creating an account. Without a subscription tier called “Premium.”


Cheat sheet

You made it. Or you skipped straight here. Either way, no judgment. Copy and paste these. Pin them. Tattoo them on your forearm. Whatever works.

What you’re doing Command
Connect to a server ssh user@host
Custom port ssh -p 2222 user@host
Use a specific key ssh -i ~/.ssh/mykey user@host
Generate a key pair ssh-keygen -t ed25519
Copy key to server ssh-copy-id user@host
Local port forward ssh -L 8080:localhost:8080 user@host
Forward to another host ssh -L 5432:db-server:5432 user@app-server
Reverse tunnel ssh -R 9090:localhost:3000 user@host
SOCKS proxy ssh -D 1080 user@host
Jump through bastion ssh -J jumpbox user@internal
Run remote command ssh user@host "command here"
Background tunnel ssh -fN -L 8080:localhost:8080 user@host

Key rule: Private key stays on your machine. Public key goes on the server. Mix this up and you’ve got bigger problems than PuTTY.

Back to the top, you overachiever.