SSH Keys and SSH Basics

Page 02 — What SSH keys are, how to use them, and how to fix common problems.

What SSH is

SSH is how you securely log into remote Linux machines over a network. It encrypts the connection so nothing is readable in transit. It is also how Git connects to remote repositories when using SSH remotes.

What an SSH key is

An SSH keypair has two parts:

When you connect to a server, the server checks whether your public key is in its authorized_keys file. If it is, it lets you authenticate using your private key — no password needed.

Generate a key

Preferred — ED25519

ssh-keygen -t ed25519 -C "you@example.com"

Creates a modern ED25519 SSH keypair. Use this unless you specifically need RSA. -t ed25519 sets the key type. -C adds a comment label so you know what the key is for. Default output: ~/.ssh/id_ed25519 (private) and ~/.ssh/id_ed25519.pub (public).

Fallback — RSA

ssh-keygen -t rsa -b 4096 -C "you@example.com"

Use only if a system requires RSA specifically.

Permissions

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
SSH is strict about permissions. If ~/.ssh or the key files are too open (e.g. world-readable), SSH silently ignores them. Permission errors are a very common cause of "Permission denied" failures.

View your public key

cat ~/.ssh/id_ed25519.pub

Shows the public key so you can paste it into a server's authorized_keys or a Git hosting site's SSH key settings.

Start ssh-agent and load your key

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
ssh-add -l

ssh-agent -s starts the agent process and prints shell commands that set the SSH_AUTH_SOCK and SSH_AGENT_PID environment variables. eval executes those commands in the current shell — without eval the variables would only exist in the subshell and your ssh client would never find the agent. ssh-add -l lists loaded keys. The agent means you only type your key passphrase once per session.

Connect to a server

ssh user@host
ssh -i ~/.ssh/id_ed25519 user@host

The first form uses whatever key is loaded in the agent or the default key file. Use -i to specify a key explicitly.

Copy key to a host

Easy way

ssh-copy-id user@host

Installs your public key into the remote user's ~/.ssh/authorized_keys. This is the simplest method if you can already log in with a password.

Manual — pipe remotely

ssh user@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys" < ~/.ssh/id_ed25519.pub
ssh user@host "chmod 600 ~/.ssh/authorized_keys"

This pipes your local public key to the remote server's authorized_keys file over SSH. The remote server's file must have permissions 600 — not the local one.

SSH config file

Edit ~/.ssh/config to save connection details by name:

Host myserver
    HostName 192.168.1.10
    User richard
    IdentityFile ~/.ssh/id_ed25519

Then connect with just:

ssh myserver
Tip: You can add multiple Host blocks for different servers. This saves typing the full command every time and is essential when you work with many hosts.

known_hosts

~/.ssh/known_hosts stores the fingerprint of every host you have connected to. On the next connection, SSH checks the fingerprint matches. If it does not, SSH refuses to connect and warns you.

Remove a stale entry when a host is rebuilt:

ssh-keygen -R hostname

Common errors

Permission denied (publickey)

Usually means one of:

REMOTE HOST IDENTIFICATION HAS CHANGED

The stored fingerprint for this host does not match. This happens when:

Fix it by removing the old entry:

ssh-keygen -R hostname
Warning: In rare cases this warning can indicate a man-in-the-middle attack. Verify through another channel before removing the old key.

Debugging connections

When SSH refuses a connection and the error is not obvious, run with verbose output:

ssh -vvv user@host

-v adds one level of debug output. -vvv gives the most detail. In the output, look for:

Tip: ssh -vvv is the first tool to reach for when you get a vague "Permission denied (publickey)" and can not tell why.

Jump hosts / bastion

A jump host (or bastion) is an intermediate server you must go through to reach machines on a private network. Instead of SSH-ing to the bastion and then SSH-ing again, you can chain the connection in one command:

ssh -J bastion.example.com user@internal-server

SSH connects to the bastion first, then tunnels onward to the internal server. You only need one command.

Better still, put this in your ~/.ssh/config so you never have to type it:

Host internal-server
    HostName 10.0.0.5
    User richard
    ProxyJump bastion.example.com

Host bastion.example.com
    User richard
    IdentityFile ~/.ssh/id_ed25519

After this, ssh internal-server routes through the bastion automatically.

Note: Your public key must be in authorized_keys on both the bastion and the internal server, unless the bastion is configured for agent forwarding.

Configuring sshd (server-side)

Most SSH problems are client-side, but sometimes you need to harden or change the server daemon configuration. The main file is /etc/ssh/sshd_config.

# Key settings in /etc/ssh/sshd_config
PubkeyAuthentication yes          # allow key-based login (usually default)
PasswordAuthentication no         # disable password login once keys are set up
PermitRootLogin no                # never allow direct root SSH in production
AllowUsers alice bob deploy       # whitelist of users who can SSH in
AllowGroups sshusers              # or restrict by group

# After changes, test config then reload — do NOT restart with active sessions
sshd -t                           # test config syntax
systemctl reload sshd             # RHEL (sshd.service)
systemctl reload ssh              # Debian (ssh.service)
Important: Always test sshd -t before reloading. A syntax error in sshd_config can lock you out of the server if you restart (rather than reload). Keep an existing session open until you have confirmed the new session works.

Port forwarding

SSH can forward network connections through an encrypted tunnel — useful for reaching services on private networks or for debugging without opening firewall rules.

# Local forwarding: access remote-host:5432 as localhost:5432 on YOUR machine
ssh -L 5432:localhost:5432 user@remote-host
# Now: psql -h localhost -p 5432 connects through the tunnel

# Forward through a jump host to a database on an internal server
ssh -L 5432:db-server:5432 user@bastion.example.com

# Remote forwarding: expose localhost:8080 on the remote server's port 9090
# Useful for sharing a local dev service with someone on the remote
ssh -R 9090:localhost:8080 user@remote-host

# Dynamic (SOCKS proxy): turn SSH into a SOCKS5 proxy on localhost:1080
# Configure your browser/tool to use SOCKS5 proxy 127.0.0.1:1080
ssh -D 1080 user@remote-host

# Add -N to not open a shell (just the tunnel), -f to background
ssh -N -f -L 5432:localhost:5432 user@remote-host

Local forwarding (-L) is by far the most common — it brings a remote service to your local machine. The format is -L local_port:target_host:target_port where target_host is resolved from the SSH server's perspective.

ControlMaster — connection reuse

Opening multiple SSH connections to the same host (common with Ansible or repeated scp) is slow due to repeated authentication. ControlMaster reuses an existing connection.

# Add to ~/.ssh/config
Host *
    ControlMaster auto
    ControlPath ~/.ssh/cm-%r@%h:%p
    ControlPersist 10m

ControlMaster auto: use an existing multiplexed connection if available, or create one. ControlPath: the socket file path (one per user@host:port). ControlPersist 10m: keep the master connection alive for 10 minutes after the last client disconnects, so the next ssh or ansible run connects instantly without re-authenticating.

# Check active multiplexed connections
ssh -O check user@remote-host

# Close a multiplexed connection manually
ssh -O stop user@remote-host