Installation, configuration, key-based authentication, and hardening of the OpenSSH daemon — the universal tool for secure remote access to Linux servers.
sshd · key auth · hardening · port forwarding · Debian · RHEL · Arch
01 — What is OpenSSH?
OpenSSH is the reference implementation of the SSH protocol — a suite of tools providing encrypted remote login, file transfer, and tunnelling. The server component (sshd) listens for incoming connections; the client (ssh) connects to it.
SSH is the single most important remote-access tool on Linux. Getting it installed, hardened, and backed by key authentication is the first task on any new server.
| Component | Role |
| sshd | The daemon — listens on TCP 22, handles authentication and session management. |
| ssh | Client — connects to a remote sshd. |
| ssh-keygen | Generates key pairs (Ed25519, ECDSA, RSA). |
| ssh-copy-id | Installs a public key into a remote host's authorized_keys. |
| ssh-agent | Holds decrypted private keys in memory for the session. |
| scp / sftp | Secure file copy and interactive file transfer over SSH. |
| sshd_config | Server configuration file — /etc/ssh/sshd_config. |
02 — Installation
Debian / Ubuntu
sudo apt update && sudo apt install openssh-server
# Enable and start the daemon
sudo systemctl enable --now ssh
# Check status
sudo systemctl status ssh
RHEL / Fedora / Rocky / AlmaLinux
sudo dnf install openssh-server
sudo systemctl enable --now sshd
sudo systemctl status sshd
Arch Linux
sudo pacman -S openssh
sudo systemctl enable --now sshd
sudo systemctl status sshd
Verify sshd is listening
# Check what port sshd is bound to
sudo ss -tlnp | grep sshd
# Or with netstat
sudo netstat -tlnp | grep sshd
# Confirm the version
ssh -V
ℹ The service is named ssh on Debian/Ubuntu and sshd on RHEL/Arch. Both run the same OpenSSH daemon — the name difference is a packaging convention.
03 — sshd_config Overview
All server behaviour is controlled by /etc/ssh/sshd_config. The file ships with sensible defaults commented out — uncomment and change only what you need. After every edit, test for syntax errors before restarting.
Always test before restarting
# Validate config for syntax errors — does not restart the daemon
sudo sshd -t
# Apply changes
sudo systemctl restart sshd # RHEL/Arch
sudo systemctl restart ssh # Debian/Ubuntu
Key directives reference
| Directive | Default | Meaning |
| Port | 22 | TCP port sshd listens on. Change to reduce scanner noise. |
| ListenAddress | 0.0.0.0 / :: | Bind to a specific interface IP. Useful on multi-homed hosts. |
| PermitRootLogin | prohibit-password | Controls root login. Set to no for best security. |
| PasswordAuthentication | yes | Allow password logins. Set to no once key auth is working. |
| PubkeyAuthentication | yes | Enable public-key authentication. |
| AuthorizedKeysFile | .ssh/authorized_keys | Where to look for authorised public keys. |
| MaxAuthTries | 6 | Max authentication attempts per connection. Lower to 3. |
| MaxSessions | 10 | Max concurrent sessions per connection. |
| LoginGraceTime | 120 | Seconds to complete login before the connection is dropped. |
| ClientAliveInterval | 0 | Seconds between keepalive probes. Set to 300 to drop dead sessions. |
| ClientAliveCountMax | 3 | Missed keepalives before disconnecting. |
| AllowUsers | (all) | Whitelist of users permitted to log in. Highly recommended. |
| AllowGroups | (all) | Whitelist of groups permitted to log in. |
| DenyUsers | (none) | Blacklist of users never permitted to log in. |
| X11Forwarding | no | Allow forwarding of X11 GUI sessions. Disable if unused. |
| AllowTcpForwarding | yes | Allow SSH tunnelling. Set to no if not needed. |
| Banner | (none) | Display a text banner before login (legal warning, etc.). |
| LogLevel | INFO | Logging verbosity: QUIET, FATAL, ERROR, INFO, VERBOSE, DEBUG. |
| Subsystem sftp | internal-sftp | Enables the SFTP subsystem for file transfers. |
04 — Key-Based Authentication
Key-based authentication replaces passwords with a cryptographic key pair — a private key you keep secret and a public key you deposit on the server. It is more secure than any password and immune to brute-force attacks.
Step 1 — generate a key pair (on your local machine)
# Ed25519 — recommended (fastest, smallest, most secure)
ssh-keygen -t ed25519 -C "your@email.com"
# RSA 4096 — for compatibility with older systems
ssh-keygen -t rsa -b 4096 -C "your@email.com"
# ECDSA 521-bit — alternative to Ed25519
ssh-keygen -t ecdsa -b 521 -C "your@email.com"
# Keys are saved to:
# ~/.ssh/id_ed25519 (private — NEVER share this)
# ~/.ssh/id_ed25519.pub (public — safe to share)
⚠ Always set a passphrase when generating a key. A key without a passphrase is equivalent to a password written in plaintext. Use ssh-agent to avoid typing it repeatedly.
Step 2 — copy the public key to the server
# Automated (requires password login to still be enabled)
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server-ip
# Manual method — if ssh-copy-id is unavailable
cat ~/.ssh/id_ed25519.pub | ssh user@server-ip \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
cat >> ~/.ssh/authorized_keys && \
chmod 600 ~/.ssh/authorized_keys"
Step 3 — test key login before disabling passwords
# Test that key auth works — keep your current session open while testing
ssh -i ~/.ssh/id_ed25519 user@server-ip
# Specify a non-default port
ssh -i ~/.ssh/id_ed25519 -p 2222 user@server-ip
Step 4 — disable password authentication
# /etc/ssh/sshd_config
PasswordAuthentication no
ChallengeResponseAuthentication no # or KbdInteractiveAuthentication no (newer)
UsePAM yes # keep yes — PAM handles other auth mechanisms
# Apply
sudo sshd -t && sudo systemctl restart sshd
authorized_keys file
# Location on the server
~/.ssh/authorized_keys
# Correct permissions — sshd will refuse to read if wrong
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
# Each line is one public key
# You can restrict a key with options at the start of the line:
from="203.0.113.0/24" ssh-ed25519 AAAA... admin@laptop # from specific IP only
command="/usr/bin/rsync --server ..." ssh-ed25519 AAAA... backup@host # forced command
no-port-forwarding,no-X11-forwarding ssh-ed25519 AAAA... deploy@ci # restrict capabilities
05 — ssh_config Client Configuration
The client config at ~/.ssh/config (per-user) or /etc/ssh/ssh_config (system-wide) saves you from typing long commands. Create a Host block for each server you connect to regularly.
~/.ssh/config
# Default settings for all hosts
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
AddKeysToAgent yes
IdentityFile ~/.ssh/id_ed25519
# Production web server
Host prod
HostName 203.0.113.10
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519_prod
# Bastion / jump host
Host bastion
HostName 198.51.100.5
User admin
IdentityFile ~/.ssh/id_ed25519
# Reach internal host via bastion (ProxyJump)
Host internal-db
HostName 192.168.1.50
User dbadmin
ProxyJump bastion
IdentityFile ~/.ssh/id_ed25519
# Connect using the alias — just:
ssh prod
ssh internal-db # automatically hops through bastion
06 — Hardened sshd_config
A production-ready server configuration applying defence-in-depth. Apply after key-based authentication is confirmed working.
/etc/ssh/sshd_config — hardened template
# ── Network ────────────────────────────────────────────────────────────
Port 22 # Change to a non-standard port to reduce scanner noise
ListenAddress 0.0.0.0
ListenAddress ::
# ── Protocol & Crypto ──────────────────────────────────────────────────
Protocol 2 # SSHv1 is broken — enforce v2 only (default in modern OpenSSH)
# Restrict to modern, audited algorithms only
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group18-sha512,diffie-hellman-group16-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
# ── Authentication ─────────────────────────────────────────────────────
PermitRootLogin no # Never allow direct root login
PasswordAuthentication no # Keys only — disable after confirming key login works
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
ChallengeResponseAuthentication no
UsePAM yes
MaxAuthTries 3 # Reduce from default 6
MaxSessions 5
LoginGraceTime 30 # 30s to complete login — down from 120s
# ── Access control ─────────────────────────────────────────────────────
AllowUsers alice bob deploy # Whitelist — only these users may log in
# AllowGroups sshusers # Alternative: allow by group membership
# ── Session & Keepalive ────────────────────────────────────────────────
ClientAliveInterval 300 # Send keepalive every 5 minutes
ClientAliveCountMax 2 # Drop connection after 2 missed keepalives
# ── Forwarding — disable what you don't need ───────────────────────────
AllowTcpForwarding no # Set to yes only if you use SSH tunnels
X11Forwarding no
AllowAgentForwarding no
PermitTunnel no
# ── Miscellaneous ──────────────────────────────────────────────────────
PrintMotd no
PrintLastLog yes
Banner /etc/ssh/banner # Optional legal warning — create this file
LogLevel VERBOSE # Log fingerprints and failed attempts
# ── SFTP ───────────────────────────────────────────────────────────────
Subsystem sftp internal-sftp
# Test then apply
sudo sshd -t && sudo systemctl restart sshd
07 — SFTP-Only Chroot Jail
Confine a user to SFTP file transfers only, locked into a specific directory — useful for backup accounts, upload accounts, and managed hosting.
/etc/ssh/sshd_config — SFTP chroot block
# At the end of sshd_config — Match blocks must be last
Match User sftpuser
ChrootDirectory /srv/sftp/%u # %u expands to the username
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
PasswordAuthentication no
Set up the chroot directory
# The ChrootDirectory itself must be owned by root
sudo mkdir -p /srv/sftp/sftpuser
sudo chown root:root /srv/sftp/sftpuser
sudo chmod 755 /srv/sftp/sftpuser
# Create a writable subdirectory the user can actually write to
sudo mkdir -p /srv/sftp/sftpuser/uploads
sudo chown sftpuser:sftpuser /srv/sftp/sftpuser/uploads
# Create the system user (no shell login)
sudo useradd -M -s /usr/sbin/nologin sftpuser
sudo passwd sftpuser # or use key auth — preferred
sudo sshd -t && sudo systemctl restart sshd
08 — SSH Tunnelling & Port Forwarding
SSH can securely tunnel any TCP traffic — encrypting connections to services that don't have their own TLS, or punching through firewalls to reach internal resources.
Local port forwarding (access a remote service locally)
# Forward localhost:5432 → remote-host:5432 (access remote PostgreSQL locally)
ssh -L 5432:localhost:5432 user@remote-host
# Forward localhost:8080 → internal-host:80 via a jump host
ssh -L 8080:192.168.1.10:80 user@jump-host
# As a background tunnel (-N = no command, -f = background)
ssh -fN -L 5432:localhost:5432 user@remote-host
Remote port forwarding (expose a local service on a remote host)
# Expose localhost:3000 as remote-host:9000 (e.g. show a dev server to a colleague)
ssh -R 9000:localhost:3000 user@remote-host
# Persistent reverse tunnel — useful for reaching hosts behind NAT
ssh -fN -R 2222:localhost:22 user@public-server
# Then from public-server: ssh -p 2222 localhost
Dynamic port forwarding (SOCKS5 proxy)
# Turn the SSH connection into a SOCKS5 proxy on localhost:1080
ssh -D 1080 -fN user@remote-host
# Then configure your browser or app to use SOCKS5 proxy: 127.0.0.1:1080
ProxyJump — multi-hop SSH
# Single hop through a bastion host
ssh -J user@bastion user@internal-server
# Multi-hop (chain of jump hosts)
ssh -J user@bastion1,user@bastion2 user@final-server
# In ~/.ssh/config (cleaner):
# Host internal
# ProxyJump bastion
ℹ For tunnelling to work, the server must have AllowTcpForwarding yes in sshd_config. If you disabled it in the hardened config, re-enable it selectively with a Match User block for accounts that need it.
09 — Hostkey Verification & Known Hosts
On first connect to a new server, SSH shows its host key fingerprint. Verifying it prevents man-in-the-middle attacks — you're confirming you're talking to the right machine.
Verifying a server fingerprint
# On the SERVER — print its fingerprints before connecting
ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub
# Output example:
# 256 SHA256:abc123... root@server (ED25519)
# On the CLIENT — when connecting for the first time, compare what SSH shows
# to the fingerprint you got from the server console or a trusted channel
Managing known_hosts
# View known hosts file
cat ~/.ssh/known_hosts
# Remove a stale host entry (after a server rebuild / IP reuse)
ssh-keygen -R server-hostname
ssh-keygen -R 203.0.113.10
# Scan a host and print its key (without connecting)
ssh-keyscan -H 203.0.113.10
# Add a host key manually to known_hosts
ssh-keyscan -H 203.0.113.10 >> ~/.ssh/known_hosts
10 — Firewall Rules for sshd
SSH must be allowed through the host's firewall. The method depends on which firewall stack you're using.
firewalld
# Allow SSH (default port 22)
sudo firewall-cmd --zone=public --add-service=ssh --permanent
sudo firewall-cmd --reload
# Allow a custom SSH port instead
sudo firewall-cmd --zone=public --add-port=2222/tcp --permanent
sudo firewall-cmd --zone=public --remove-service=ssh --permanent
sudo firewall-cmd --reload
nftables
# In /etc/nftables.conf — inside your input chain
tcp dport 22 ct state new accept
# Custom port
tcp dport 2222 ct state new accept
iptables
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
# Custom port
sudo iptables -A INPUT -p tcp --dport 2222 -m conntrack --ctstate NEW -j ACCEPT
ufw (Ubuntu default)
# Allow SSH (default port)
sudo ufw allow ssh
# Allow a custom port
sudo ufw allow 2222/tcp
# Restrict SSH to a specific source IP
sudo ufw allow from 203.0.113.0/24 to any port 22
⚠ Always open the new firewall rule for a custom SSH port before changing the port in sshd_config and restarting — otherwise you will lock yourself out.
11 — Certificates (SSH CA)
For environments with many servers and users, SSH certificates are more scalable than managing authorized_keys files on every host. A Certificate Authority (CA) signs user and host keys — any host that trusts the CA automatically trusts any key it signs.
Create a CA key pair
# Generate the CA key (keep this very safe — ideally offline)
ssh-keygen -t ed25519 -f /etc/ssh/ca_key -C "SSH CA"
# The public key will be distributed to all servers
# The private key signs user and host certificates
Sign a user key
# Sign a user's public key — valid for 1 week, for user "alice"
ssh-keygen -s /etc/ssh/ca_key \
-I "alice@example.com" \
-n alice \
-V +1w \
~/.ssh/id_ed25519.pub
# This creates: ~/.ssh/id_ed25519-cert.pub
# The user presents both key + cert when connecting
Configure sshd to trust the CA
# Copy the CA public key to each server
sudo cp ca_key.pub /etc/ssh/ca_key.pub
# /etc/ssh/sshd_config — add this line
TrustedUserCAKeys /etc/ssh/ca_key.pub
# Now any key signed by the CA can log in — no authorized_keys needed
sudo sshd -t && sudo systemctl restart sshd
12 — Troubleshooting
| Symptom | Cause & Fix |
| Connection refused | sshd not running or wrong port — sudo systemctl status sshd and ss -tlnp | grep sshd. |
| Permission denied (publickey) | Key not in authorized_keys, wrong permissions on ~/.ssh or authorized_keys, or PubkeyAuthentication no. |
| Permission denied (password) | PasswordAuthentication no in sshd_config — use a key, or temporarily re-enable passwords. |
| Host key verification failed | Server's host key changed (rebuild, new IP) — run ssh-keygen -R hostname to clear the old entry. |
| Too many authentication failures | ssh-agent is offering too many keys — use ssh -o IdentitiesOnly=yes -i key. |
| Slow login | DNS reverse lookup failing — set UseDNS no in sshd_config. |
| SFTP chroot fails | ChrootDirectory must be owned by root with no group-write. Check with ls -la /srv/sftp/. |
| Tunnel connection refused | AllowTcpForwarding no in sshd_config — enable for the relevant user. |
| Bad sshd_config after edit | Run sudo sshd -t before restarting — always. |
Diagnostic commands
# Test sshd config for syntax errors
sudo sshd -t
# Run sshd in debug mode (foreground, verbose — use a spare port for testing)
sudo sshd -d -p 2223
# Connect in verbose mode to see exactly what the client is doing
ssh -vvv user@server-ip
# Check the auth log for failures and successes
sudo journalctl -u sshd -f
sudo tail -f /var/log/auth.log # Debian/Ubuntu
sudo tail -f /var/log/secure # RHEL/CentOS
# List all active SSH sessions
who
ss -tnp | grep :22
# Show which keys an agent is holding
ssh-add -l
13 — Quick Reference
| Command | What it does |
| sudo systemctl status sshd | Check if sshd is running |
| sudo sshd -t | Validate sshd_config without restarting |
| sudo systemctl restart sshd | Apply config changes |
| ssh-keygen -t ed25519 -C "email" | Generate a new Ed25519 key pair |
| ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host | Install public key on a remote host |
| ssh -vvv user@host | Debug a failing connection |
| ssh-keygen -R hostname | Remove a stale known_hosts entry |
| ssh -L 5432:localhost:5432 user@host | Local tunnel — access remote DB locally |
| ssh -R 9000:localhost:3000 user@host | Remote tunnel — expose local port remotely |
| ssh -J bastion user@internal | Jump through a bastion host |
| ssh-add -l | List keys loaded in ssh-agent |
| ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub | Print server host key fingerprint |
| journalctl -u sshd -f | Follow sshd log in real time |
✔ The correct order for securing a new server: install sshd → generate keys locally → copy public key → test key login in a new window → disable password authentication → apply the hardened sshd_config → test again. Never disable passwords before confirming key login works.