SSH Client Configuration

Posted on 3 2026

OpenSSH ships with sensible defaults, but a properly configured ~/.ssh/config file is one of those things that pays back the time invested every single day. Instead of typing long connection strings with ports, keys, and options, you type a hostname and everything else is handled automatically. Agent forwarding, GPG agent forwarding, jump hosts, non-standard ports, per-host key selection: all of it configured once and forgotten.

This page covers the desktop SSH client configuration for a setup with multiple servers across three sites, GPG agent forwarding to servers, and Yubikey-based authentication.

System-wide hardening

Before configuring per-user settings, harden the system-wide SSH client defaults. These apply to all users and all connections unless overridden.

Create /etc/ssh/ssh_config.d/hardening.conf:

# System-wide SSH client hardening
# Applies to all users unless overridden in ~/.ssh/config

# Only use the strongest key exchange algorithms
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512

# Strong ciphers only
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr

# Strong MAC algorithms
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com

# Preferred public key types
HostKeyAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,rsa-sha2-512,rsa-sha2-256

# Use DNS to verify host keys where SSHFP records exist
VerifyHostKeyDNS yes

# Hash known_hosts entries for privacy
HashKnownHosts yes

# Never use password authentication from the system level
# (can be overridden per-host if absolutely necessary)
PasswordAuthentication no

User SSH configuration

The per-user configuration lives in ~/.ssh/config. Create the directory if it does not exist and set correct permissions:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch ~/.ssh/config
chmod 600 ~/.ssh/config

Global defaults

The first section of ~/.ssh/config sets defaults that apply to all connections unless overridden by a more specific Host block:

# ============================================================
# Global defaults
# ============================================================

Host *
    # Use the GPG agent for SSH authentication
    # This is set via SSH_AUTH_SOCK in ~/.bashrc
    # IdentityAgent is set here as a fallback
    IdentityAgent ${SSH_AUTH_SOCK}

    # Prefer ed25519 keys
    IdentityFile ~/.ssh/id_ed25519
    IdentityFile ~/.ssh/id_rsa

    # Only offer keys explicitly listed, not everything in the agent
    # Remove this if you want SSH to try all keys the agent offers
    # IdentitiesOnly yes

    # Connection persistence: reuse existing connections for new sessions
    ControlMaster auto
    ControlPath ~/.ssh/control/%r@%h:%p
    ControlPersist 10m

    # Keep connections alive
    ServerAliveInterval 60
    ServerAliveCountMax 3

    # Use DNS verification for host keys
    VerifyHostKeyDNS yes

    # Forward the locale
    SendEnv LANG LC_*

    # Disable X11 forwarding by default (enable per-host where needed)
    ForwardX11 no

    # Do not forward the agent by default (enable per-host where needed)
    ForwardAgent no

Create the control socket directory:

mkdir -p ~/.ssh/control
chmod 700 ~/.ssh/control

Internal servers

Add a block for each internal server. Using a wildcard for the internal domain keeps this concise. Adjust the domain, port, and user to match your setup:

# ============================================================
# Internal network - all three sites
# ============================================================

# Match all internal servers
Host *.yourdomain.net 10.1.* 10.2.* 10.3.*
    # Use the non-standard SSH port configured on internal servers
    # Set this to whatever port your servers are using
    Port 63508

    # Always use key authentication
    PasswordAuthentication no

    # The internal domain resolves via Unbound, no need for extra lookups
    VerifyHostKeyDNS yes

Specific server entries

Add individual entries for servers you connect to regularly. These override the wildcard block above where needed:

# ============================================================
# Burnage Mad House - Prevernal site (10.1.x.x)
# ============================================================

Host server.yourdomain.net
    Hostname 10.1.0.10
    User yourusername
    Port 63508
    # Forward GPG agent to this server
    RemoteForward /run/user/1000/gnupg/S.gpg-agent /run/user/1000/gnupg/S.gpg-agent.extra

Host nas.yourdomain.net
    Hostname 10.1.0.20
    User yourusername
    Port 63508

# ============================================================
# Fallowfield Asylum - Vernal site (10.2.x.x)
# ============================================================

Host fallowfield-server.yourdomain.net
    Hostname 10.2.0.10
    User yourusername
    Port 63508

# ============================================================
# The Lighthouse - Estival site (10.3.x.x)
# ============================================================

Host lighthouse-server.yourdomain.net
    Hostname 10.3.0.10
    User yourusername
    Port 63508

# ============================================================
# Jump host configuration
# ============================================================

# If connecting to servers at other sites via a jump host
# rather than through the VPN:
Host *.vernal.internal
    ProxyJump yourusername@fallowfield-server.yourdomain.net
    User yourusername

GPG agent forwarding per host

The RemoteForward lines in the server entries above forward the GPG agent as covered in the GPG Remote section. The socket paths must match what gpgconf --list-dirs reports on both the local and remote machines.

Find the local extra socket path:

gpgconf --list-dirs agent-extra-socket

Find the remote agent socket path (logged in to the remote machine):

gpgconf --list-dirs agent-socket

Update the RemoteForward lines with the correct paths if they differ from the example above.

For servers where you want GPG forwarding, add:

Host server.yourdomain.net
    RemoteForward /run/user/1000/gnupg/S.gpg-agent /run/user/1000/gnupg/S.gpg-agent.extra

The first path is the remote socket (where the remote GPG will look for the agent). The second path is the local extra socket (what the local agent exposes for forwarding).

Yubikey PIV via PKCS#11

For servers configured to use the PIV-based SSH key from the Yubikey, add the PKCS#11 provider to the relevant host entries:

Host server-piv.yourdomain.net
    PKCS11Provider /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so
    IdentitiesOnly yes

IdentitiesOnly yes combined with the PKCS11Provider ensures only the PIV key is offered, not the GPG agent key or any key files.

Known hosts management

The HashKnownHosts yes setting in the system-wide config means existing host entries in ~/.ssh/known_hosts will not be hashed. Rebuild the known hosts file with hashed entries:

ssh-keygen -H -f ~/.ssh/known_hosts
# Remove the backup created by ssh-keygen
rm -f ~/.ssh/known_hosts.old

For internal servers where SSHFP DNS records are published, you can pre-populate known hosts using the DNS records:

ssh-keyscan -D server.yourdomain.net >> ~/.ssh/known_hosts

This fetches the server’s host keys via DNS and adds them to your known hosts file without needing to connect first.

SSH certificates

If your internal CA (covered later in this series) issues SSH host certificates, configure the desktop to trust your CA rather than individual host keys. This eliminates the “do you trust this host key?” prompt for new internal servers.

Add the CA public key to your known hosts file with the @cert-authority prefix:

echo "@cert-authority *.yourdomain.net,10.1.*,10.2.*,10.3.* $(cat /path/to/ssh-ca.pub)" \
    >> ~/.ssh/known_hosts

Once this is in place, any server presenting a certificate signed by your CA is trusted automatically, without individual host key confirmation.

Useful SSH aliases and functions

Add these to ~/.bashrc for convenience:

# List all SSH hosts defined in config
alias ssh-hosts="grep '^Host ' ~/.ssh/config | grep -v '*' | awk '{print \$2}'"

# Copy SSH public key to a server (works with GPG agent key)
ssh-copy-gpg-key() {
    if [ -z "$1" ]; then
        echo "Usage: ssh-copy-gpg-key user@host"
        return 1
    fi
    ssh-add -L | ssh "$1" "cat >> ~/.ssh/authorized_keys"
    echo "GPG SSH key copied to $1"
}

# Quick SSH tunnel function
ssh-tunnel() {
    # Usage: ssh-tunnel <local-port> <remote-host> <remote-port> <gateway>
    ssh -N -L "${1}:${2}:${3}" "${4}" &
    echo "Tunnel established: localhost:${1} -> ${2}:${3} via ${4}"
    echo "Kill with: kill $!"
}

Auditing the configuration

Verify the hardened algorithms are in effect by checking what is negotiated on a test connection:

ssh -v server.yourdomain.net 2>&1 | grep -E "kex|cipher|mac|host key"

The output should show curve25519-sha256 for key exchange, chacha20-poly1305 or aes256-gcm for the cipher, and ssh-ed25519 for the host key algorithm.

Check connection multiplexing is working:

# First connection
ssh server.yourdomain.net "echo first connection"

# Second connection (should be near-instant via the control socket)
ssh server.yourdomain.net "echo second connection"

The second connection should return almost immediately without the handshake delay of the first.

Keeping the known hosts file clean

Over time the known hosts file accumulates stale entries for servers that no longer exist. Clean it up periodically:

# Remove a specific host entry
ssh-keygen -R server.yourdomain.net

# Remove entries by IP address
ssh-keygen -R 10.1.0.10

Never disable StrictHostKeyChecking globally. The warning when a host key changes is a security signal, not an inconvenience. If it appears unexpectedly, investigate before connecting, do not suppress the warning.