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
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
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
- Add two-factor authentication to SSH for even stronger security
- Set up SSH bastion host for centralized access control
- Configure advanced firewall rules to complement SSH security
- Implement comprehensive Linux hardening beyond SSH
- Set up centralized logging for security event correlation
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default values
SSH_USER=""
SSH_PORT="2222"
BACKUP_DIR="/opt/ssh-hardening-backup"
# Usage function
usage() {
echo "Usage: $0 -u <username> [-p <port>]"
echo " -u: Username for SSH access (required)"
echo " -p: SSH port (default: 2222)"
echo "Example: $0 -u myuser -p 2222"
exit 1
}
# Parse command line arguments
while getopts "u:p:h" opt; do
case $opt in
u) SSH_USER="$OPTARG" ;;
p) SSH_PORT="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
# Validate required arguments
if [[ -z "$SSH_USER" ]]; then
echo -e "${RED}Error: Username is required${NC}"
usage
fi
# Cleanup function
cleanup() {
echo -e "${RED}Script failed! Check logs above for details.${NC}"
echo -e "${YELLOW}Backup files are available in: $BACKUP_DIR${NC}"
}
trap cleanup ERR
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root${NC}"
exit 1
fi
# Detect distribution and set package manager
echo -e "${BLUE}[1/12] Detecting distribution...${NC}"
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update"
SERVICE_CMD="systemctl"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
SERVICE_CMD="systemctl"
FIREWALL_CMD="firewall-cmd"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
SERVICE_CMD="systemctl"
FIREWALL_CMD="firewall-cmd"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect distribution${NC}"
exit 1
fi
echo -e "${GREEN}Detected: $PRETTY_NAME${NC}"
# Create backup directory
echo -e "${BLUE}[2/12] Creating backup directory...${NC}"
mkdir -p "$BACKUP_DIR"
chmod 755 "$BACKUP_DIR"
# Update package manager
echo -e "${BLUE}[3/12] Updating package manager...${NC}"
$PKG_UPDATE
# Install required packages
echo -e "${BLUE}[4/12] Installing required packages...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL openssh-server fail2ban rsyslog
else
$PKG_INSTALL openssh-server fail2ban rsyslog
fi
# Create SSH user if it doesn't exist
echo -e "${BLUE}[5/12] Setting up SSH user...${NC}"
if ! id "$SSH_USER" &>/dev/null; then
useradd -m -s /bin/bash "$SSH_USER"
echo -e "${GREEN}Created user: $SSH_USER${NC}"
else
echo -e "${GREEN}User $SSH_USER already exists${NC}"
fi
# Create SSH directory and set permissions
SSH_HOME=$(eval echo "~$SSH_USER")
mkdir -p "$SSH_HOME/.ssh"
chmod 700 "$SSH_HOME/.ssh"
chown "$SSH_USER:$SSH_USER" "$SSH_HOME/.ssh"
# Generate SSH key for the user
echo -e "${BLUE}[6/12] Generating SSH key pair...${NC}"
if [[ ! -f "$SSH_HOME/.ssh/id_ed25519" ]]; then
sudo -u "$SSH_USER" ssh-keygen -t ed25519 -C "$SSH_USER@$(hostname)" -f "$SSH_HOME/.ssh/id_ed25519" -N ""
echo -e "${GREEN}SSH key generated${NC}"
fi
# Set up authorized_keys
if [[ ! -f "$SSH_HOME/.ssh/authorized_keys" ]]; then
touch "$SSH_HOME/.ssh/authorized_keys"
chmod 600 "$SSH_HOME/.ssh/authorized_keys"
chown "$SSH_USER:$SSH_USER" "$SSH_HOME/.ssh/authorized_keys"
fi
# Backup original SSH config
echo -e "${BLUE}[7/12] Backing up SSH configuration...${NC}"
cp /etc/ssh/sshd_config "$BACKUP_DIR/sshd_config.$(date +%Y%m%d_%H%M%S)"
# Configure SSH daemon
echo -e "${BLUE}[8/12] Configuring SSH daemon...${NC}"
cat > /etc/ssh/sshd_config << EOF
# SSH Hardened Configuration
# Network settings
Port $SSH_PORT
Protocol 2
ListenAddress 0.0.0.0
# Authentication settings
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
PermitRootLogin no
PubkeyAuthentication yes
AuthenticationMethods publickey
# Security hardening
PermitEmptyPasswords no
MaxAuthTries 3
MaxStartups 3:30:10
MaxSessions 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
# Restrict users
AllowUsers $SSH_USER
# Disable unused features
X11Forwarding no
AllowTcpForwarding no
GatewayPorts no
PermitTunnel no
# Logging
LogLevel VERBOSE
SyslogFacility AUTHPRIV
# Cryptography
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha2-512
KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
EOF
chmod 644 /etc/ssh/sshd_config
# Configure fail2ban
echo -e "${BLUE}[9/12] Configuring fail2ban...${NC}"
cat > /etc/fail2ban/jail.local << EOF
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
backend = systemd
[sshd]
enabled = true
port = $SSH_PORT
logpath = %(sshd_log)s
maxretry = 3
bantime = 3600
findtime = 600
[sshd-ddos]
enabled = true
port = $SSH_PORT
logpath = %(sshd_log)s
maxretry = 6
bantime = 3600
findtime = 600
EOF
chmod 644 /etc/fail2ban/jail.local
# Configure SSH logging
echo -e "${BLUE}[10/12] Configuring SSH logging...${NC}"
cat > /etc/rsyslog.d/10-ssh.conf << EOF
# SSH authentication logging
auth,authpriv.* /var/log/ssh-auth.log
# SSH session logging
if \$programname == 'sshd' then /var/log/ssh-sessions.log
& stop
EOF
chmod 644 /etc/rsyslog.d/10-ssh.conf
# Configure log rotation
cat > /etc/logrotate.d/ssh-logs << EOF
/var/log/ssh-auth.log
/var/log/ssh-sessions.log
{
weekly
rotate 12
compress
delaycompress
missingok
notifempty
create 640 root root
postrotate
systemctl reload rsyslog
endscript
}
EOF
chmod 644 /etc/logrotate.d/ssh-logs
# Configure firewall
echo -e "${BLUE}[11/12] Configuring firewall...${NC}"
if [[ "$FIREWALL_CMD" == "ufw" ]]; then
ufw --force enable
ufw default deny incoming
ufw default allow outgoing
ufw allow "$SSH_PORT/tcp"
elif [[ "$FIREWALL_CMD" == "firewall-cmd" ]]; then
systemctl enable --now firewalld
firewall-cmd --permanent --add-port="$SSH_PORT/tcp"
firewall-cmd --reload
fi
# Start and enable services
echo -e "${BLUE}[12/12] Starting and enabling services...${NC}"
systemctl restart rsyslog
systemctl enable --now fail2ban
systemctl enable --now ssh 2>/dev/null || systemctl enable --now sshd
# Display public key
echo -e "${GREEN}SSH hardening completed successfully!${NC}"
echo
echo -e "${YELLOW}IMPORTANT: Copy the following public key to your local machine:${NC}"
echo "----------------------------------------"
cat "$SSH_HOME/.ssh/id_ed25519.pub"
echo "----------------------------------------"
echo
echo -e "${YELLOW}To connect from your local machine:${NC}"
echo "1. Save the above public key as ~/.ssh/id_ed25519_server.pub"
echo "2. Connect using: ssh -i ~/.ssh/id_ed25519_server.pub -p $SSH_PORT $SSH_USER@\$(hostname -I | awk '{print \$1}')"
echo
echo -e "${RED}WARNING: Test the SSH connection in a new terminal before closing this session!${NC}"
echo -e "${GREEN}Backup files saved to: $BACKUP_DIR${NC}"
Review the script before running. Execute with: bash install.sh