SSH keys aren’t just for logging in; they’re a surprisingly powerful way to delegate access and build secure, automated workflows.

Let’s see it in action. Imagine you have a web server, web.example.com, and a database server, db.example.com. You want your web server to connect to the database, but you don’t want to expose the database directly to the internet.

First, on web.example.com, generate an SSH key pair:

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_db_access -N "your_strong_passphrase"

This creates id_ed25519_db_access (private key) and id_ed25519_db_access.pub (public key). The passphrase is crucial.

Next, add the public key to the authorized_keys file on db.example.com for a specific user, say appuser:

# On db.example.com, as appuser
mkdir -p ~/.ssh
chmod 700 ~/.ssh
cat >> ~/.ssh/authorized_keys <<EOF
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... your_public_key_here ... web.example.com
EOF
chmod 600 ~/.ssh/authorized_keys

Now, web.example.com can connect to db.example.com as appuser using this key.

But we want to tunnel. On web.example.com, establish a local tunnel:

# On web.example.com
ssh -i ~/.ssh/id_ed25519_db_access -N -L 5432:localhost:5432 appuser@db.example.com

Here’s what’s happening:

  • -i ~/.ssh/id_ed25519_db_access: Specifies the private key to use for authentication.
  • -N: Tells SSH not to execute a remote command, just to forward ports.
  • -L 5432:localhost:5432: This is the core of the tunnel. It says: "Listen on port 5432 on this machine (web.example.com). Any connection that comes to this port should be forwarded through the SSH connection to db.example.com, and then connect to localhost on port 5432 on db.example.com."

Your web application on web.example.com can now connect to localhost:5432 (e.g., a PostgreSQL database running on the default port on db.example.com) as if it were local, but the traffic is securely proxied through the SSH tunnel.

The problem this solves is exposing sensitive services (like databases) to the public internet. By using SSH tunneling, the service only needs to be accessible from the SSH server, and SSH handles the secure transport.

The mental model is a secure pipe. You establish an authenticated SSH connection, and then you tell SSH to carry traffic for specific local ports over that pipe to specific remote destinations.

The actual levers you control are:

  • Key Generation: ssh-keygen parameters like -t (type), -f (filename), -N (passphrase).
  • Authentication: Placing the correct public key in authorized_keys on the target server, ensuring file permissions (~/.ssh is 700, authorized_keys is 600).
  • Tunneling: The -L (local forward), -R (remote forward), and -D (dynamic forward/SOCKS proxy) flags in the ssh command.
  • Server Configuration: sshd_config on the server side, controlling things like PasswordAuthentication, PubkeyAuthentication, AllowUsers, AllowGroups, and PermitRootLogin.

To make this tunnel persistent and automatically reconnect if it drops, you’d typically use autossh:

# On web.example.com
autossh -M 0 -i ~/.ssh/id_ed25519_db_access -N -L 5432:localhost:5432 appuser@db.example.com

The -M 0 disables the monitoring port, relying on SSH’s keepalives. You’d also want to configure ServerAliveInterval and ServerAliveCountMax in ~/.ssh/config on web.example.com or in /etc/ssh/ssh_config for a system-wide setting.

The one thing most people don’t realize is how powerful SSH’s authorized_keys file is for fine-grained control. You can restrict a key to only execute specific commands, or even bind it to a particular source IP address, making it a much more robust access control mechanism than just a password. For example, adding command="/usr/bin/db_access_script.sh" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... limits that key to running only that script.

After setting up keys and tunnels, the next logical step is to explore SSH multiplexing for efficiency.

Want structured learning?

Take the full Linux & Systems Programming course →