SSH — Keys, Config, Tunnels

6 interview angles 7 min read source

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; ~/.ssh must be 700. SSH refuses to use looser perms.
  • Multiple keys, wrong one used — without IdentitiesOnly yes and a specific IdentityFile, 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 ProxyJump instead.
  • Forgetting -t for interactive remote commands — interactive programs need a TTY.
  • Not using ~/.ssh/config — typing flags every time is brittle.
  • Removing ~/.ssh/known_hosts to “fix” host key warnings — defeats the security check. Use ssh-keygen -R to 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-id adds the public key locally.” — appends to the remote server’s ~/.ssh/authorized_keys.
  • -L and -R do the same thing in different directions.”-L forwards from local to remote; -R forwards 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-agent for?” — keeps decrypted private keys in memory so you don’t type the passphrase for every connection.
  • “What’s ~/.ssh/config and why use it?” — declares hostnames, users, ports, keys, jump hosts per host. Replaces long ssh invocations with ssh hostname.
  • “What’s an SSH tunnel and when do you use one?” — port forwarding through SSH. -L lets you reach a non-public service via a server you can reach (e.g. database via bastion). -R exposes 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_hosts and ~/.ssh/authorized_keys?”known_hosts lists 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.