SSH — Keys, Config, Tunnels
SSH is what every Python dev uses to log into servers, push to git, and tunnel through firewalls. The interview questions: “how do you set up key-based auth?”, “what’s an SSH tunnel?”, “how does agent forwarding work?”
Key-based auth — the basics
ssh-keygen -t ed25519 -C "alice@laptop" # generate a keypair
# creates ~/.ssh/id_ed25519 (private) and ~/.ssh/id_ed25519.pub (public)
ssh-copy-id user@server # appends pubkey to server's ~/.ssh/authorized_keys
ssh user@server # logs in without password
| Algorithm | Status |
|---|---|
| Ed25519 | modern, fast, short keys — default choice |
| RSA 4096 | universally supported, older |
| ECDSA | works, but Ed25519 is preferred |
| RSA 1024, DSA | broken; never use |
For new keys: ed25519. RSA only when forced by an old server.
Public key, private key
| Public | Private | |
|---|---|---|
| Filename | id_ed25519.pub |
id_ed25519 |
| Where it lives | on every server you log into (~/.ssh/authorized_keys) |
only on your machine, NEVER share |
| Permissions | 644 | 600 (only owner can read) |
If your private key is 644 (group-readable), SSH refuses to use it. Common error: WARNING: UNPROTECTED PRIVATE KEY FILE!. Fix: chmod 600 ~/.ssh/id_ed25519.
The whole ~/.ssh directory should be 700:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_*
chmod 644 ~/.ssh/*.pub
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/config
chmod 644 ~/.ssh/known_hosts
ssh-agent — type passphrase once per session
If your private key has a passphrase (it should), you’d type it every time. ssh-agent keeps the decrypted key in memory:
eval "$(ssh-agent -s)" # start agent (auto-starts in modern systems)
ssh-add ~/.ssh/id_ed25519 # decrypt and load (asks for passphrase once)
ssh-add -l # list loaded keys
ssh-add -D # delete all keys from agent
macOS Keychain integration:
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
Stores passphrase in Keychain; never prompts again.
For Linux, GNOME Keyring / KDE Wallet / keychain package do similar.
~/.ssh/config — the file you should be using
Instead of typing ssh -i ~/.ssh/work_key -p 2222 alice@server.example.com every time, define hosts:
# ~/.ssh/config
Host work
HostName server.example.com
User alice
Port 2222
IdentityFile ~/.ssh/work_key
Host bastion
HostName bastion.internal
User alice
IdentityFile ~/.ssh/work_key
Host db-prod
HostName db.internal # only reachable via bastion
User alice
IdentityFile ~/.ssh/work_key
ProxyJump bastion # SSH through bastion automatically
# Wildcard for github
Host github.com
User git
IdentityFile ~/.ssh/github_key
AddKeysToAgent yes
UseKeychain yes # macOS
Now ssh work “just works.” git clone git@github.com:org/repo.git uses the right key. ssh db-prod automatically routes through bastion.
Common options:
| Option | Effect |
|---|---|
HostName |
actual hostname / IP |
User |
login user |
Port |
SSH port (default 22) |
IdentityFile |
private key to use |
IdentitiesOnly yes |
use ONLY this key, ignore agent’s other keys |
ProxyJump host |
tunnel through host (replaces old ProxyCommand syntax) |
ForwardAgent yes |
enable agent forwarding (use with care) |
ServerAliveInterval 60 |
send keepalive every 60s (prevents idle disconnect) |
ControlMaster auto + ControlPath /tmp/ssh-%r@%h:%p |
multiplex connections (subsequent SSHs reuse the first) |
Agent forwarding — convenient but risky
ssh -A user@bastion # forward your local agent to bastion
# now from bastion you can:
ssh user@target # uses your local agent's keys
git clone git@github.com:... # uses your local agent's keys
Without forwarding, you’d have to put your private key on the bastion (terrible) or use a separate key for the next hop.
Risk: anyone with root on the bastion can use your forwarded agent to log into anywhere your keys allow. Don’t enable to untrusted hosts.
Modern alternative: ProxyJump (which doesn’t expose your agent on the intermediate host):
ssh -J bastion target # SSH directly to target through bastion
Or in config:
Host target
ProxyJump bastion
This sets up a TCP tunnel through bastion; the SSH session is between you and target. No forwarded agent needed.
SSH tunnels — port forwarding
Three types:
Local port forwarding (-L)
ssh -L 5432:db.internal:5432 user@bastion
“Forward my local port 5432 → through bastion → to db.internal:5432.”
Now psql -h localhost -p 5432 connects to the remote database via the tunnel. Useful when the database isn’t directly reachable from your machine.
Remote port forwarding (-R)
ssh -R 8000:localhost:8000 user@server
“Server’s port 8000 → tunnels back to my localhost:8000.”
The server can now access something running on your laptop. Useful for sharing a local dev server with a remote teammate, or with ngrok-style services.
Dynamic SOCKS proxy (-D)
ssh -D 1080 user@bastion
Opens a SOCKS proxy on local port 1080. Apps configured to use SOCKS-localhost:1080 route through bastion. Effectively a VPN-lite.
| Browser | configure SOCKS |
|---|---|
| Firefox | Settings → Network → Manual proxy |
| Chrome | needs an extension or --proxy-server flag |
Common usage patterns
# Run a remote command
ssh user@server "uptime"
# Copy files
scp file user@server:/path/ # local → remote
scp user@server:/path/file ./ # remote → local
scp -r dir user@server:/path/ # recursive
rsync -avz --delete dir/ user@server:/path/ # better for sync (incremental)
# Mount remote dir locally
sshfs user@server:/remote/path ./mountpoint # FUSE-based
# Run an interactive shell with a working directory
ssh -t user@server "cd /var/log && bash" # -t allocates a TTY
Avoiding “host key verification failed”
First connection prompts for host key:
The authenticity of host 'server (1.2.3.4)' can't be established.
ED25519 key fingerprint is SHA256:...
Are you sure you want to continue connecting (yes/no)?
Saying yes appends to ~/.ssh/known_hosts. If the server’s key changes (rebuild, new instance), you get:
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
This is either: (a) a man-in-the-middle attack, or (b) the server was legitimately rebuilt. Verify out-of-band, then:
ssh-keygen -R server.example.com # remove the old entry
ssh server.example.com # re-prompt and accept new key
ssh-keyscan for known_hosts management
For automation:
ssh-keyscan github.com >> ~/.ssh/known_hosts
Pre-populates known_hosts with GitHub’s keys (avoids the first-connection prompt in CI scripts). Verify the fingerprint matches GitHub’s published values before trusting.
SSH server hardening (the basics)
/etc/ssh/sshd_config:
PermitRootLogin no # don't let root SSH in
PasswordAuthentication no # keys only
PubkeyAuthentication yes
ChallengeResponseAuthentication no
UsePAM yes
AllowUsers alice bob # explicit allowlist
Port 22 # change from default to reduce log noise (security through obscurity, weak)
After editing: sudo systemctl restart sshd.
Then verify you can still log in (in a second terminal!) before closing your current session — locking yourself out is the classic mistake.
Common pitfalls
- Wrong file permissions —
~/.ssh/id_*must be 600;~/.sshmust be 700. SSH refuses to use looser perms. - Multiple keys, wrong one used — without
IdentitiesOnly yesand a specificIdentityFile, SSH tries every key in your agent, may hit “too many auth attempts” rate limit on the server. - Agent forwarding to untrusted hosts — anyone with root on that host can use your agent. Use
ProxyJumpinstead. - Forgetting
-tfor interactive remote commands — interactive programs need a TTY. - Not using
~/.ssh/config— typing flags every time is brittle. - Removing
~/.ssh/known_hoststo “fix” host key warnings — defeats the security check. Usessh-keygen -Rto remove only the relevant entry.
Common interview confusions
- “Public and private keys are interchangeable.” — public goes on every server you log into; private NEVER leaves your machine. Treat private like a password.
- “
ssh-copy-idadds the public key locally.” — appends to the remote server’s~/.ssh/authorized_keys. - “
-Land-Rdo the same thing in different directions.” —-Lforwards from local to remote;-Rforwards from remote to local. Both create tunnels but go opposite ways. - “Agent forwarding is the same as ProxyJump.” —
-A(forwarding) exposes your agent on the intermediate host (risky).-J(ProxyJump) tunnels directly to the target without exposing your agent.
Interview angle
- “How does SSH key-based auth work?” — generate a keypair (
ssh-keygen), put the public key in the server’s~/.ssh/authorized_keys, keep the private key safe. SSH proves possession of the private key during handshake; no password sent over the wire. - “What’s
ssh-agentfor?” — keeps decrypted private keys in memory so you don’t type the passphrase for every connection. - “What’s
~/.ssh/configand why use it?” — declares hostnames, users, ports, keys, jump hosts per host. Replaces long ssh invocations withssh hostname. - “What’s an SSH tunnel and when do you use one?” — port forwarding through SSH.
-Llets you reach a non-public service via a server you can reach (e.g. database via bastion).-Rexposes something local to a remote server. - “Agent forwarding vs ProxyJump?” —
-A(forwarding) makes your local agent visible on the intermediate host (lets the intermediate use your keys, but exposes them).-J(ProxyJump) tunnels through the intermediate without exposing the agent. Prefer ProxyJump. - “What’s the difference between
~/.ssh/known_hostsand~/.ssh/authorized_keys?” —known_hostslists servers YOU trust (their public keys).authorized_keys(on the server) lists clients THE SERVER trusts (their public keys). Both are key-based trust lists, opposite directions.