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 machinelocalhost(middle) — the destination host, as seen from the remote server. “localhost” here means the remote server itself8080(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 serverlocalhost— the destination, as seen from your machine3000— 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.