Configure SSH key authentication and security hardening

Intermediate 25 min Apr 30, 2026 85 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Replace password authentication with SSH keys and implement comprehensive security hardening including fail2ban, audit logging, and access controls to protect your Linux servers from unauthorized access and brute force attacks.

Prerequisites

  • Root or sudo access
  • Basic command line knowledge
  • SSH client on local machine

What this solves

SSH password authentication is vulnerable to brute force attacks and credential stuffing. This tutorial replaces passwords with cryptographic key pairs, disables insecure authentication methods, and implements comprehensive SSH hardening including connection limits, fail2ban protection, and audit logging. You'll have a production-ready SSH configuration that blocks common attack vectors.

Step-by-step configuration

Generate SSH key pair

Create a strong SSH key pair on your local machine. We'll use Ed25519 for better security and performance than RSA.

ssh-keygen -t ed25519 -C "your-email@example.com" -f ~/.ssh/id_ed25519

When prompted, set a strong passphrase to protect your private key. This creates two files: ~/.ssh/id_ed25519 (private key) and ~/.ssh/id_ed25519.pub (public key).

Copy public key to server

Upload your public key to the server's authorized keys. Replace 203.0.113.10 with your server's IP address.

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@203.0.113.10

If ssh-copy-id is not available, manually copy the key:

cat ~/.ssh/id_ed25519.pub | ssh user@203.0.113.10 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

Set correct permissions on authorized_keys

SSH requires strict permissions on the .ssh directory and authorized_keys file for security.

chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
Never use chmod 777. It gives every user on the system full access to your files. SSH keys require strict permissions: 700 for directories, 600 for private files.

Test SSH key authentication

Verify key-based login works before disabling password authentication. Open a new terminal and test the connection.

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

You should be prompted for your key passphrase, not the server password. Keep your current SSH session open as a backup.

Configure SSH daemon security settings

Edit the SSH daemon configuration to disable password authentication and implement security hardening.

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
sudo nano /etc/ssh/sshd_config

Add or modify these settings in /etc/ssh/sshd_config:

# Authentication settings
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM no
PermitRootLogin no
PubkeyAuthentication yes
AuthenticationMethods publickey

Security hardening

Protocol 2 Port 22 PermitEmptyPasswords no MaxAuthTries 3 MaxStartups 3:30:10 MaxSessions 3 LoginGraceTime 30 ClientAliveInterval 300 ClientAliveCountMax 2

Restrict users and groups

AllowUsers user1 user2

AllowGroups ssh-users

Disable unused features

X11Forwarding no AllowTcpForwarding no GatewayPorts no PermitTunnel no

Logging

LogLevel VERBOSE SyslogFacility AUTHPRIV

Install and configure fail2ban

Install fail2ban to automatically block IP addresses after failed SSH attempts.

sudo apt update
sudo apt install -y fail2ban
sudo dnf install -y epel-release
sudo dnf install -y fail2ban

Configure fail2ban for SSH protection

Create a custom fail2ban configuration for SSH that's more aggressive than the defaults.

sudo nano /etc/fail2ban/jail.local
[DEFAULT]

Ban IP for 1 hour after 3 failed attempts within 10 minutes

bantime = 3600 findtime = 600 maxretry = 3 backend = systemd

Email notifications (optional)

destemail = admin@example.com

sendername = Fail2Ban

mta = sendmail

action = %(action_mwl)s

[sshd] enabled = true port = ssh logpath = %(sshd_log)s maxretry = 3 bantime = 3600 findtime = 600 [sshd-ddos] enabled = true port = ssh logpath = %(sshd_log)s maxretry = 6 bantime = 3600 findtime = 600

Enable SSH audit logging

Configure rsyslog to capture detailed SSH authentication events for security monitoring.

sudo nano /etc/rsyslog.d/10-ssh.conf
# SSH authentication logging
auth,authpriv.*                 /var/log/ssh-auth.log

Separate SSH session logging

if $programname == 'sshd' then /var/log/ssh-sessions.log & stop

Configure log rotation for SSH logs

Set up logrotate to manage SSH log files and prevent disk space issues.

sudo nano /etc/logrotate.d/ssh-logs
/var/log/ssh-auth.log /var/log/ssh-sessions.log {
    daily
    missingok
    rotate 90
    compress
    delaycompress
    notifempty
    create 0640 syslog adm
    postrotate
        systemctl reload rsyslog
    endscript
}

Test SSH configuration syntax

Validate the SSH configuration before applying changes to avoid lockout.

sudo sshd -t

If there are no errors, the configuration is valid. Any syntax errors will be displayed and must be fixed before proceeding.

Apply all configuration changes

Restart services to apply the new configurations. Keep your current SSH session open as a safety net.

sudo systemctl restart rsyslog
sudo systemctl enable --now fail2ban
sudo systemctl reload sshd

Configure SSH client settings

Optimize your local SSH client configuration for security and convenience.

nano ~/.ssh/config
Host production-server
    HostName 203.0.113.10
    User yourusername
    IdentityFile ~/.ssh/id_ed25519
    ServerAliveInterval 60
    ServerAliveCountMax 3
    StrictHostKeyChecking yes
    UserKnownHostsFile ~/.ssh/known_hosts
    

Global settings

Host * AddKeysToAgent yes UseKeychain yes HashKnownHosts yes Protocol 2

Set correct permissions on the SSH client config:

chmod 600 ~/.ssh/config

Verify your setup

Test all security configurations to ensure they're working properly.

# Verify SSH daemon is running with new config
sudo systemctl status sshd

Check fail2ban is active and monitoring SSH

sudo fail2ban-client status sshd

Test key-based authentication (from local machine)

ssh production-server

Check SSH logs are being written

sudo tail -f /var/log/ssh-auth.log

Verify fail2ban is processing SSH logs

sudo fail2ban-client status

To test fail2ban protection, try connecting with wrong credentials from another IP (or ask a colleague to test). After 3 failed attempts, the IP should be banned:

# Check banned IPs
sudo fail2ban-client status sshd

View fail2ban log

sudo tail /var/log/fail2ban.log

Advanced SSH security hardening

Implement SSH jump host configuration

For additional security, configure SSH to only allow connections through a bastion host. This creates a centralized access point for your infrastructure.

Host bastion
    HostName 203.0.113.5
    User bastionuser
    IdentityFile ~/.ssh/id_ed25519
    ControlMaster auto
    ControlPath ~/.ssh/control-%r@%h:%p
    ControlPersist 5m

Host production-server
    HostName 10.0.1.10
    User appuser
    IdentityFile ~/.ssh/id_ed25519
    ProxyJump bastion

Configure SSH certificate authentication

For large environments, implement SSH certificates for scalable key management.

# Generate CA key pair (do this on a secure, offline machine)
ssh-keygen -t ed25519 -f ssh_ca -C "SSH Certificate Authority"

Sign user public key with CA

ssh-keygen -s ssh_ca -I "user-cert" -n user1,user2 -V +52w ~/.ssh/id_ed25519.pub

Configure the server to trust certificates signed by your CA:

# Add to sshd_config
TrustedUserCAKeys /etc/ssh/trusted_ca.pub

Copy CA public key to server

scp ssh_ca.pub server:/etc/ssh/trusted_ca.pub

SSH monitoring and alerting

Set up SSH connection monitoring

Create a monitoring script to track SSH sessions and alert on suspicious activity.

sudo nano /usr/local/bin/ssh-monitor.sh
#!/bin/bash

SSH session monitoring script

LOGFILE="/var/log/ssh-sessions.log" ALERT_EMAIL="admin@example.com" MAX_SESSIONS=5

Count active SSH sessions

ACTIVE_SESSIONS=$(who | grep -c "pts")

Check for root logins (should be 0 with PermitRootLogin no)

ROOT_LOGINS=$(grep "Accepted" $LOGFILE | grep "root" | wc -l)

Alert if too many sessions

if [ $ACTIVE_SESSIONS -gt $MAX_SESSIONS ]; then echo "High SSH session count: $ACTIVE_SESSIONS" | mail -s "SSH Alert" $ALERT_EMAIL fi

Alert on any root login attempts

if [ $ROOT_LOGINS -gt 0 ]; then echo "Root login detected in SSH logs" | mail -s "Security Alert" $ALERT_EMAIL fi

Log current session count

echo "$(date): Active sessions: $ACTIVE_SESSIONS" >> /var/log/ssh-monitor.log
sudo chmod +x /usr/local/bin/ssh-monitor.sh

Schedule monitoring with systemd timer

Create a systemd timer to run SSH monitoring every 5 minutes.

sudo nano /etc/systemd/system/ssh-monitor.service
[Unit]
Description=SSH Connection Monitor
Wants=ssh-monitor.timer

[Service]
Type=oneshot
ExecStart=/usr/local/bin/ssh-monitor.sh
User=root
Group=root
sudo nano /etc/systemd/system/ssh-monitor.timer
[Unit]
Description=Run SSH Monitor every 5 minutes
Requires=ssh-monitor.service

[Timer]
OnCalendar=*:0/5
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now ssh-monitor.timer

Integration with centralized logging

Forward SSH logs to remote syslog

For centralized security monitoring, configure rsyslog to forward SSH events to a remote log server. This integrates well with centralized logging infrastructure.

sudo nano /etc/rsyslog.d/20-ssh-remote.conf
# Forward SSH authentication events to remote syslog server
auth,authpriv.* @@logserver.example.com:514

Also forward to local file

auth,authpriv.* /var/log/ssh-auth.log

Configure SSH metrics for Prometheus

Export SSH connection metrics for monitoring dashboards. This works with system monitoring setups.

sudo nano /usr/local/bin/ssh-metrics.sh
#!/bin/bash

Export SSH metrics for Prometheus node_exporter textfile collector

METRICS_FILE="/var/lib/node_exporter/textfile_collector/ssh.prom"

Count active SSH sessions

ACTIVE_SESSIONS=$(ss -t state established '( dport = ssh or sport = ssh )' | wc -l) ACTIVE_SESSIONS=$((ACTIVE_SESSIONS - 1)) # Remove header line

Count failed authentication attempts in last hour

FAILED_AUTHS=$(grep "Failed password" /var/log/auth.log | grep "$(date --date='1 hour ago' '+%b %d %H')" | wc -l)

Write metrics

echo "ssh_active_sessions $ACTIVE_SESSIONS" > "$METRICS_FILE" echo "ssh_failed_auth_last_hour $FAILED_AUTHS" >> "$METRICS_FILE" echo "ssh_config_last_reload $(stat -c %Y /etc/ssh/sshd_config)" >> "$METRICS_FILE"
sudo chmod +x /usr/local/bin/ssh-metrics.sh
sudo mkdir -p /var/lib/node_exporter/textfile_collector

Common issues

Symptom Cause Fix
"Permission denied (publickey)" Wrong key permissions or path chmod 600 ~/.ssh/id_ed25519 and verify key path in config
"Agent admitted failure to sign" SSH agent not running or key not loaded eval $(ssh-agent) then ssh-add ~/.ssh/id_ed25519
Locked out after configuration change Invalid SSH config or firewall rule Access via console/VNC, restore /etc/ssh/sshd_config.backup
Fail2ban not blocking IPs Wrong log path or backend Check sudo fail2ban-client status sshd and log path in jail config
"Host key verification failed" Server key changed or MITM attack Verify server identity, then ssh-keygen -R hostname and reconnect
SSH logs not appearing Rsyslog configuration error sudo systemctl restart rsyslog and check /etc/rsyslog.d/ files

Next steps

Running this in production?

Want this handled for you? Setting up SSH security once is straightforward. Keeping it monitored, maintaining fail2ban rules, rotating keys, and responding to security events across environments is the harder part. See how we run infrastructure like this for European SaaS and fintech teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle infrastructure security hardening for businesses that depend on uptime. From initial setup to ongoing operations.