SSH is ubiquitous, yet most of us barely scratch its surface. We authenticate, we connect, we move on. But SSH has a remarkable depth of functionality that can transform how you manage infrastructure, secure access, and automate workflows. This post explores the corners of SSH that deserve more attention.

The Client Configuration File

The ~/.ssh/config file is perhaps SSH's most underutilised feature. Rather than typing out connection details repeatedly, you can define hosts with all their parameters:

Host buildserver
    HostName 192.168.1.50
    User deploy
    Port 2222
    IdentityFile ~/.ssh/buildserver_ed25519
    ForwardAgent no

Host *.internal.example.com
    User alan
    ProxyJump bastion
    IdentityFile ~/.ssh/internal_ed25519

Now ssh buildserver handles everything. But the configuration system goes much deeper.

Pattern matching allows wildcard hosts. The asterisk matches any sequence of characters, and the question mark matches exactly one character. Configuration is applied in order, with the first matching value for each option taking precedence.

Host prod-*
    User deployer
    IdentityFile ~/.ssh/prod_key

Host dev-*
    User developer
    IdentityFile ~/.ssh/dev_key

Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    AddKeysToAgent yes

The Match directive provides conditional configuration based on criteria beyond just hostname:

Match host *.example.com exec "test -f ~/.ssh/vpn_connected"
    ProxyJump none

Match host *.example.com
    ProxyJump bastion.example.com

This checks whether a VPN indicator file exists before deciding whether to use a jump host.

CanonicalizeHostname resolves short hostnames to their fully qualified versions before matching, which is useful when you have internal DNS:

CanonicalDomains internal.example.com example.com
CanonicalizeHostname yes
CanonicalizeMaxDots 0

Host webserver
    # Will match webserver.internal.example.com
    User www-data

Restricting Commands with authorized_keys

This is where SSH becomes genuinely powerful for automation and security. Each public key in ~/.ssh/authorized_keys can have options that constrain what that key can do.

The command option restricts a key to running exactly one command, regardless of what the connecting client requests:

command="/usr/local/bin/backup-script" ssh-ed25519 AAAA... backup@automation

When someone connects with this key, /usr/local/bin/backup-script runs instead of whatever they asked for. The original command is available in the SSH_ORIGINAL_COMMAND environment variable, which enables some interesting patterns:

#!/bin/bash
# /usr/local/bin/git-shell-wrapper
case "$SSH_ORIGINAL_COMMAND" in
    git-upload-pack*|git-receive-pack*)
        exec $SSH_ORIGINAL_COMMAND
        ;;
    *)
        echo "Only git operations permitted"
        exit 1
        ;;
esac

This allows git operations while blocking everything else.

Network restrictions limit where connections can come from:

from="192.168.1.0/24,10.0.0.5" ssh-ed25519 AAAA... admin@internal

Disabling features prevents various SSH capabilities:

no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAA... automated@script

The no-pty option prevents allocation of a pseudo-terminal, which makes the key unsuitable for interactive use but perfectly fine for automated commands that just need to run and return output.

Combining restrictions creates tightly scoped access:

command="/usr/bin/rsync --server --daemon .",from="backup.example.com",no-pty,no-port-forwarding ssh-ed25519 AAAA... rsync@backup

This key can only be used from one host, can only run rsync in server mode, cannot allocate a terminal, and cannot forward ports.

The restrict option (OpenSSH 7.2+) provides a sensible default that disables most features, which you can then selectively re-enable:

restrict,command="/usr/local/bin/deploy",pty ssh-ed25519 AAAA... deploy@ci

This disables everything except what's explicitly allowed.

Certificate-Based Authentication

SSH certificates solve the problem of key distribution at scale. Instead of copying public keys to every server, you sign keys with a Certificate Authority, and servers trust that CA.

Generate a CA key pair:

ssh-keygen -t ed25519 -f ca_key -C "SSH CA"

Sign a user's public key:

ssh-keygen -s ca_key -I alan@example -n alan,deployer -V +52w user_key.pub

This creates user_key-cert.pub valid for one year, allowing authentication as either alan or deployer.

On servers, trust the CA by adding to /etc/ssh/sshd_config:

TrustedUserCAKeys /etc/ssh/ca_key.pub

Certificates can include restrictions just like authorized_keys entries:

ssh-keygen -s ca_key -I backup-automation -n backup \
    -O clear -O no-pty -O force-command=/usr/local/bin/backup \
    -O source-address=10.0.0.0/8 \
    -V +24h backup_key.pub

Breaking this down:

This creates a short-lived certificate that can only run the backup command from internal addresses.

Host certificates work similarly, solving the "trust this host key?" problem:

ssh-keygen -s ca_key -I webserver.example.com -h \
    -n webserver.example.com,webserver,192.168.1.10 \
    /etc/ssh/ssh_host_ed25519_key.pub

Clients that trust the CA will automatically accept this host without prompting.

Jump Hosts and ProxyCommand

Sometimes you can't reach a server directly — it's on a private network, behind a firewall, or only accessible from a specific machine. Jump hosts (also called bastion hosts) let you route your SSH connection through an intermediate server to reach the final destination. ProxyCommand provides the same capability with more flexibility for custom setups.

The ProxyJump directive (or -J flag) routes connections through intermediate hosts:

Host internal-db
    HostName 10.0.0.50
    ProxyJump bastion.example.com

Multiple jumps chain together:

Host deep-internal
    HostName 192.168.1.100
    ProxyJump bastion,internal-gateway

ProxyCommand offers more flexibility when you need custom proxy behaviour:

Host *.onion
    ProxyCommand socat - SOCKS4A:localhost:%h:%p,socksport=9050

Host corporate-*
    ProxyCommand corkscrew proxy.corp.com 8080 %h %p

The first routes .onion addresses through Tor; the second tunnels through an HTTP proxy.

Port Forwarding Patterns

Local forwarding (-L) creates a listening socket on your machine that forwards to a remote destination:

ssh -L 5432:db.internal:5432 bastion

Now localhost:5432 connects to the internal database via the bastion.

Remote forwarding (-R) works in reverse, exposing a local service through the remote host:

ssh -R 8080:localhost:3000 public-server

Anyone connecting to public-server:8080 reaches your local development server.

Dynamic forwarding (-D) creates a SOCKS proxy:

ssh -D 1080 server

Configure your browser or application to use localhost:1080 as a SOCKS5 proxy, and all traffic routes through the server.

Restricting forwarding on the server can be done in sshd_config:

AllowTcpForwarding local
PermitOpen 10.0.0.0/8:*
GatewayPorts no

This allows local forwarding only, restricts destinations to internal addresses, and prevents remote forwards from binding to public interfaces.

SSH Tunnelling

Port forwarding covers specific ports, but SSH can create full network tunnels that route arbitrary traffic. This is useful when you need more than point-to-point forwarding.

TUN/TAP Tunnelling

TUN/TAP tunnelling creates virtual network interfaces. TUN operates at layer 3 (IP packets), TAP at layer 2 (ethernet frames):

# On client
ssh -w 0:0 root@server
# This creates tun0 on both ends

The -w local:remote specifies tunnel device numbers. Both sides need configuration in sshd_config:

PermitTunnel point-to-point   # or 'ethernet' for TAP, 'yes' for both

After the tunnel establishes, configure the interfaces:

# Client side
ip addr add 10.10.10.1/32 peer 10.10.10.2 dev tun0
ip link set tun0 up

# Server side
ip addr add 10.10.10.2/32 peer 10.10.10.1 dev tun0
ip link set tun0 up

Now you have a routable point-to-point link. Add routes to direct traffic through it:

# Route a subnet through the tunnel
ip route add 192.168.50.0/24 via 10.10.10.2

Practical Uses for TUN Tunnels

Accessing an entire remote subnet rather than individual ports. If you need to reach dozens of services on 10.0.0.0/24, a tunnel with appropriate routing beats maintaining dozens of port forwards.

Building a poor man's VPN when you can't install proper VPN software. The server needs IP forwarding enabled and appropriate iptables rules:

# On server
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A POSTROUTING -s 10.10.10.0/24 -o eth0 -j MASQUERADE

TAP Tunnelling

TAP tunnelling bridges ethernet segments. This is heavier but allows broadcast traffic and non-IP protocols:

ssh -o Tunnel=ethernet -w 0:0 root@server

TAP interfaces can join bridges, making two physically separate networks appear as one broadcast domain. Useful for certain clustering scenarios or when you need ARP to work across the tunnel.

Combining Tunnels with Configuration

You can automate tunnel setup in your SSH config:

Host remote-lan
    HostName gateway.example.com
    Tunnel point-to-point
    TunnelDevice 0:0
    PermitLocalCommand yes
    LocalCommand ip addr add 10.10.10.1/32 peer 10.10.10.2 dev tun0 && ip link set tun0 up

The LocalCommand runs after connection, automating the interface setup.

Tunnelling vs SOCKS Proxy

Dynamic forwarding (-D) creates a SOCKS proxy, which works well for applications that support SOCKS. TUN tunnelling works at the IP level, capturing all traffic regardless of application support. The tradeoff is that TUN requires root privileges and more setup, while SOCKS works as an unprivileged user.

For most cases, SOCKS suffices:

ssh -D 1080 -fN bastion
# Route specific traffic through it
curl --proxy socks5h://localhost:1080 http://internal.service/

The socks5h variant does DNS resolution on the remote side, which matters when internal hostnames aren't resolvable locally.

Transparent Proxying

If you need to route traffic from applications that don't support SOCKS, tools like tun2socks create a TUN interface that forwards to a SOCKS proxy:

ssh -D 1080 -fN bastion
tun2socks -device tun0 -proxy socks5://127.0.0.1:1080
# Configure interface
ip addr add 10.10.10.1/24 dev tun0
ip link set tun0 up
ip route add 192.168.0.0/16 dev tun0

This gives you tunnel-like behaviour without needing root on the remote server.

Security Considerations for Tunnelling

Tunnelling effectively extends your network perimeter. A compromised client with tunnel access can route attacks into your internal network. Restrict tunnel permissions carefully:

# sshd_config
PermitTunnel no

Match User vpn-user
    PermitTunnel point-to-point

Consider whether you actually need full tunnelling or whether targeted port forwards would suffice with less risk.

Multiplexing and Connection Sharing

SSH can share a single TCP connection across multiple sessions, dramatically speeding up repeated connections:

Host *
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600

The first connection to a host establishes the master; subsequent connections reuse it. ControlPersist 600 keeps the master alive for ten minutes after the last session closes.

This matters more than you might think. Without multiplexing, each connection requires TCP handshake, key exchange, and authentication. With multiplexing, additional sessions are nearly instant.

Managing multiplexed connections: The -O flag sends control commands to an existing master connection from another terminal.

Check if a master connection exists:

ssh -O check server

Add a port forward through the existing master without opening a new session:

ssh -O forward -L 8080:localhost:80 server

Close the master connection when finished:

ssh -O exit server

Environment Variables

SSH starts a clean shell by default — your local environment doesn't carry across to the remote session. This is usually what you want for security, but sometimes you need specific variables to follow you.

Common use cases include locale settings (so output displays correctly) and Git configuration (so commits have the right identity). To send variables, configure your client:

# Client config (~/.ssh/config)
Host server
    SendEnv LANG LC_* GIT_*

The server must explicitly accept these variables in sshd_config:

# Server sshd_config
AcceptEnv LANG LC_* GIT_*

This server whitelist is a security boundary — arbitrary environment variables can alter program behaviour in unexpected ways, so servers only accept what they've explicitly allowed.

If you don't control the server configuration, SetEnv offers an alternative that doesn't require server cooperation:

Host server
    SetEnv EDITOR=vim PAGER=less

These variables are set on the remote side regardless of the server's AcceptEnv configuration.

Escape Sequences

SSH sessions can hang — the network drops, the remote server freezes, or something else goes wrong. Closing your terminal works, but you lose your local shell state. SSH provides escape sequences to handle these situations without killing your terminal.

The escape character is ~, but it's only recognised immediately after a newline. Press Enter, then the escape sequence:

~.Disconnect immediately, even if the session is hung
~^ZSuspend SSH and return to local shell
~#List forwarded connections
~COpen command line for adding/removing forwards
~&Background SSH while waiting for forwards to close
~?Show all escape sequences

The most important is ~. — your escape hatch when a connection freezes. Instead of closing the terminal, press Enter, then ~. and you're cleanly disconnected.

The command line (~C) lets you add or remove port forwards without restarting the session:

ssh> -L 8080:localhost:80
Forwarding port.

ssh> -KL 8080
Cancelled forwarding.

For chained SSH sessions (SSH into a server, then SSH again to another), double the tilde to reach the inner connection: ~~. disconnects the inner session while keeping the outer one alive.

AuthorizedKeysCommand

Instead of static authorized_keys files, you can fetch keys dynamically:

# sshd_config
AuthorizedKeysCommand /usr/local/bin/fetch-keys %u
AuthorizedKeysCommandUser nobody

The script receives the username and should output authorized_keys format:

#!/bin/bash
# /usr/local/bin/fetch-keys
USER=$1
curl -sf "https://keys.internal/users/$USER/keys"

This enables central key management, integration with identity providers, and dynamic access control based on whatever logic you need.

Hardening sshd_config

A few settings worth considering:

# Require specific users or groups
AllowUsers deploy admin@10.0.0.*
AllowGroups ssh-users

# Restrict authentication methods
AuthenticationMethods publickey
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no

# Limit login attempts
MaxAuthTries 3
MaxSessions 5
LoginGraceTime 30

# Require recent key types
PubkeyAcceptedAlgorithms ssh-ed25519,sk-ssh-ed25519@openssh.com

# Disable unused features
X11Forwarding no
AllowAgentForwarding no
PermitTunnel no

Per-user or per-group overrides use Match blocks:

Match Group developers
    AllowTcpForwarding yes
    PermitTunnel yes

Match User deploy
    AllowTcpForwarding local
    PermitOpen 10.0.0.0/8:*
    ForceCommand /usr/local/bin/deploy-wrapper

Practical Applications

Restricted deployment key:

command="cd /var/www/app && git pull && systemctl reload app",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAA... deploy@ci

Database tunnel with automatic local forward:

Host db-tunnel
    HostName bastion.example.com
    LocalForward 5432 db.internal:5432
    RequestTTY no
    ExitOnForwardFailure yes

Running ssh -fN db-tunnel establishes the tunnel in the background.

Git access with validation:

#!/bin/bash
# forced command script
if [[ "$SSH_ORIGINAL_COMMAND" =~ ^git-(upload|receive)-pack\ \'/[a-zA-Z0-9_-]+\.git\'$ ]]; then
    exec git-shell -c "$SSH_ORIGINAL_COMMAND"
fi
logger -t git-access "Rejected command from $SSH_CLIENT: $SSH_ORIGINAL_COMMAND"
exit 1

Short-lived access via certificates:

# Grant someone temporary access
ssh-keygen -s ca_key -I "contractor-jane-$(date +%Y%m%d)" \
    -n deployer -V +8h -O source-address=203.0.113.50 \
    contractor_key.pub

Eight hours of access from a specific IP, then the certificate expires.

Full subnet access via tunnel:

#!/bin/bash
# vpn-connect.sh - Establish tunnel and configure routing
ssh -f -w 0:0 root@gateway sleep 3600 &
sleep 1

# Configure local interface
sudo ip addr add 10.10.10.1/32 peer 10.10.10.2 dev tun0
sudo ip link set tun0 up

# Route internal subnets through tunnel
sudo ip route add 10.0.0.0/8 via 10.10.10.2
sudo ip route add 192.168.0.0/16 via 10.10.10.2

echo "Tunnel established. Internal networks now accessible."

SSH rewards deep exploration. These features exist because real operational needs demanded them. Whether you're managing a handful of servers or architecting access control for a larger infrastructure, understanding what SSH can actually do opens up solutions that might otherwise require additional tooling or complexity.