Skip to main content

SSH checklist

# What port is SSH listening on?
sudo ss -tlnp | grep sshd

# Review critical sshd config settings
grep -E "^(Port|PermitRootLogin|PasswordAuthentication|MaxAuthTries|AllowUsers)" /etc/ssh/sshd_config

# Quick count of failed attempts
sudo grep "Failed password" /var/log/auth.log | wc -l

# Top offending IPs
sudo grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -20

# Recent 50 failures with timestamp
sudo grep "Failed password" /var/log/auth.log | tail -50

# Who is currently logged in
who

# More detail: IP, login time, idle
w

# All active SSH connections via socket
ss -tnp | grep :22

# SSH login history (recent)
last | grep "pts\|ssh" | head -20

# Check if installed
fail2ban-client status 2>/dev/null || echo "fail2ban NOT installed"

# Install it
sudo apt install fail2ban -y

# After install β€” check SSH jail status
sudo fail2ban-client status sshd

# List currently banned IPs
sudo fail2ban-client get sshd banip
⚑ One-liner Full Snapshot
echo "=== SSH PORT ===" && ss -tlnp | grep sshd && \
echo "=== FAILED LOGINS (top IPs) ===" && grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -10 && \
echo "=== ACTIVE SESSIONS ===" && w && \
echo "=== FAIL2BAN ===" && sudo fail2ban-client status sshd 2>/dev/null || echo "fail2ban not running"

SSH via private keys

# Generate key pair (anywhere/at client)
ssh-keygen -t ed25519 -C "comment (email/action...)"

# Validate by ssh to host using private key file
ssh -i $PRIVATE_KEY_PATH $USER@$HOST -p $PORT

# View public key (for copying)
cat ~/.ssh/id_ed25519.pub
# Windows
type $env:USERPROFILE\.ssh\id_ed25519.pub
# Initialize .ssh folder
mkdir -p ~/.ssh
# Copy
echo "PASTE YOUR PUBLIC KEY HERE" >> ~/.ssh/authorized_keys
# Set permission (just in case)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

# One-liner - Add public key to the authorized_keys of host's user, create path if not exist (process at client)
cat $PUBLIC_KEY_PATH | ssh $USER@$HOST -p $PORT "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

Change SSH port

  • Prerequisites:
# Check ssh status
which sshd
# Install sshd
sudo systemctl status ssh
# Install
sudo apt update && sudo apt install openssh-server -y
  • Modified sshd_config
# Find/Check location
find / -name "sshd_config" 2>/dev/null
# Edit
sudo vi /etc/ssh/sshd_config
  • Search & uncomment Port 22, then change to the desired port.
  • Restart ssh service
# Ubuntu
# Disable ssh.socket
sudo systemctl disable --now ssh.socket
sudo systemctl enable --now ssh.service
#sudo service ssh restart
sudo systemctl restart ssh

# AlmaLinux
systemctl restart sshd
  • Allow port via [[ufw]]/[firewalld]
  • Apply fail2ban

Setup script:

#!/usr/bin/env bash
set -e

### ===== CUSTOM OPTIONS =====
CUSTOM_PORT=2222
ALLOW_PASSWORD_LOGIN=true

NEWUSER="americio"
SSH_PUBLIC_KEY="ssh-ed25519 AAAA...your_key_here"
PASSWORDLESS_SUDO=false

BANTIME="1h"
FINDTIME="10m"
MAXRETRY=3
### ===========================

FILE=/etc/ssh/sshd_config

echo "πŸ’Ύ Backing up original sshd_config"
sudo cp "$FILE" "$FILE.backup.$(date +%F-%H%M%S)"

echo "βœ’οΈ Writing secure sshd_config..."

sudo tee "$FILE" >/dev/null <<EOF
# ===== SECURE SSH CONFIG (FRESH VPS) =====

# Port (change from default 22 reduces random bots)
Port ${CUSTOM_PORT}
# To disable IPv6 for SSH, uncomment:
# AddressFamily inet

# Authentication
PermitRootLogin no
PasswordAuthentication $( [ "$ALLOW_PASSWORD_LOGIN" = true ] && echo "yes" || echo "no" )
PermitEmptyPasswords no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

# Security limits
MaxAuthTries 3
LoginGraceTime 20
MaxSessions 2

# Keepalive
ClientAliveInterval 120
ClientAliveCountMax 2

# Logging
LogLevel VERBOSE

# Default "Include" directory
Include /etc/ssh/sshd_config.d/*.conf
EOF

echo "πŸ€” Validating new SSH configuration..."
if sudo sshd -t; then
    echo "βœ… SSH config OK. Reloading sshd..."
    sudo systemctl reload sshd
    echo "πŸ‘Œ SSH hardened successfully."
else
    echo "β›” SSH config ERROR. Restoring backup..."
    sudo cp "$FILE.backup"* "$FILE"
    exit 1
fi

echo "===================================================================="

echo "πŸͺ„ Creating user: $NEWUSER"
if id "$NEWUSER" >/dev/null 2>&1; then
    echo "πŸ‘» User already exists, skipping."
else
    sudo adduser --disabled-password --gecos "" "$NEWUSER"
	GENPASS=$(openssl rand -base64 16)
	echo "$NEWUSER:$GENPASS" | sudo chpasswd
	echo "πŸ” Temporary password for $NEWUSER: $GENPASS"
fi

echo "🀝 Adding $NEWUSER to sudo group"
sudo usermod -aG sudo "$NEWUSER"
sudo usermod -aG adm "$NEWUSER"
sudo usermod -s /bin/bash "$NEWUSER"

echo "πŸ—ƒοΈ Setting up SSH directory"
sudo mkdir -p /home/$NEWUSER/.ssh
echo "$SSH_PUBLIC_KEY" | sudo tee /home/$NEWUSER/.ssh/authorized_keys >/dev/null

sudo chmod 700 /home/$NEWUSER/.ssh
sudo chmod 600 /home/$NEWUSER/.ssh/authorized_keys
sudo chown -R $NEWUSER:$NEWUSER /home/$NEWUSER/.ssh

if [ "$PASSWORDLESS_SUDO" = true ]; then
    echo "πŸ”“ Enabling passwordless sudo for $NEWUSER"
    echo "$NEWUSER ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/90-$NEWUSER >/dev/null
    sudo chmod 440 /etc/sudoers.d/90-$NEWUSER
fi

echo "πŸ”’ Locking root password (optional safety)"
sudo passwd -l root

echo "πŸ‘Œ Secure sudo user setup completed."
echo "Now log in as: ssh $NEWUSER@your-server -p $CUSTOM_PORT"

echo "===================================================================="

echo "πŸ›‘οΈ Configuring UFW firewall with safe defaults"

sudo ufw --force reset
sudo ufw default deny incoming
sudo ufw default allow outgoing

echo "πŸ”“ Allowing SSH port: $CUSTOM_PORT and other common ports (http, https)"
sudo ufw limit ${CUSTOM_PORT}/tcp
sudo ufw allow http
sudo ufw allow https

echo "πŸš€ Enabling firewall..."
sudo ufw --force enable

echo "πŸ“‹ UFW status:"
sudo ufw status verbose

echo "πŸ‘Œ UFW firewall configuration completed."

echo "===================================================================="

echo "πŸ“¦ Installing fail2ban..."
sudo apt update
sudo apt install -y fail2ban

JAIL_LOCAL="/etc/fail2ban/jail.local"

echo "πŸ“ Writing fail2ban jail.local..."
sudo tee $JAIL_LOCAL >/dev/null <<EOF
[DEFAULT]
bantime = ${BANTIME}
findtime = ${FINDTIME}
maxretry = ${MAXRETRY}
backend = systemd
banaction = ufw
ignoreip = 127.0.0.1/8 ::1

[sshd]
enabled = true
port = ${CUSTOM_PORT}
filter = sshd
backend = systemd
logpath = journal
EOF

echo "πŸ”„ Restarting Fail2ban..."
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban
sudo fail2ban-client status sshd
sudo systemctl restart fail2ban
sleep 2
echo "πŸ” Fail2ban SSH jail status:"
sudo fail2ban-client status sshd

echo "πŸ‘Œ Fail2ban installed & configured."

echo "===================================================================="

echo "πŸ“¦ Unattended Security Updates (auto-patch for critical CVEs)"
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

if false; then
	
	echo "===================================================================="
	echo "πŸ” Enabling SSH 2FA (TOTP via Google Authenticator)"

	echo "πŸ“¦ Installing TOTP PAM module..."
	sudo apt install -y libpam-google-authenticator

	echo "πŸͺͺ Generating TOTP secret for user: $NEWUSER"
	sudo -u "$NEWUSER" bash -c "google-authenticator -t -d -f -r 3 -R 30 -W --quiet"

	echo "πŸ“ Backing up SSH PAM config..."
	sudo cp /etc/pam.d/sshd /etc/pam.d/sshd.bak.$(date +%F-%H%M%S)

	echo "βœ’οΈ Updating PAM rules for TOTP"
	sudo sed -i '1i auth required pam_google_authenticator.so' /etc/pam.d/sshd

	echo "πŸ“ Backing up sshd_config..."
	sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F-%H%M%S)

	echo "βš™οΈ Configuring SSH to require BOTH publickey & TOTP"
	sudo sed -i 's/^#\?ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config
	sudo sed -i 's/^#\?KbdInteractiveAuthentication.*/KbdInteractiveAuthentication yes/' /etc/ssh/sshd_config

	# Require BOTH:
	# - publickey
	# - keyboard-interactive (Google Authenticator)
	if grep -q "^AuthenticationMethods" /etc/ssh/sshd_config; then
		sudo sed -i 's/^AuthenticationMethods.*/AuthenticationMethods publickey,keyboard-interactive/' /etc/ssh/sshd_config
	else
		echo "AuthenticationMethods publickey,keyboard-interactive" | sudo tee -a /etc/ssh/sshd_config >/dev/null
	fi

	echo "πŸ€” Validating SSH configuration with 2FA..."
	if sudo sshd -t; then
		echo "βœ… SSH config OK. Restarting sshd..."
		sudo systemctl restart sshd
		echo "πŸ” SSH 2FA enabled successfully."
	else
		echo "β›” ERROR: Invalid SSH config. Rolling back..."
		sudo cp /etc/ssh/sshd_config.bak.* /etc/ssh/sshd_config
		sudo cp /etc/pam.d/sshd.bak.* /etc/pam.d/sshd
		sudo systemctl restart sshd
		exit 1
	fi

	echo "✨ 2FA Setup Completed."
	echo "πŸ‘‰ IMPORTANT: Open a NEW terminal and test login before closing this session."
	echo "SSH Login now requires:"
	echo "  1) SSH private key"
	echo "  2) Google Authenticator 6-digit TOTP code"
	
	echo "===================================================================="
	
	echo "πŸ“œ Installing hardened auditd rules..."

	sudo tee /etc/audit/rules.d/hardening.rules >/dev/null << "EOF"
## ===============================
## HARDENED AUDITD SECURITY RULES
## ===============================

# Track all commands executed via sudo
-w /var/log/sudo.log -p wa -k sudo_logs

# Track changes to privileged binaries (SUID/SGID)
-w /usr/bin -p wa -k privileged-bins
-w /usr/sbin -p wa -k privileged-bins

# Track changes to essential system config
-w /etc/passwd -p wa -k passwd_changes
-w /etc/group -p wa -k group_changes
-w /etc/shadow -p wa -k shadow_changes
-w /etc/sudoers -p wa -k sudoers_changes
-w /etc/sudoers.d/ -p wa -k sudoers_changes

# Track SSH configuration changes
-w /etc/ssh/sshd_config -p wa -k ssh_config
-w /etc/ssh/sshd_config.d/ -p wa -k ssh_config

# Track all login/logout events, including failures
-w /var/log/faillog -p wa -k auth_failures
-w /var/log/lastlog -p wa -k auth_changes
-w /var/log/tallylog -p wa -k auth_failures

# Track modification to systemd service files
-w /etc/systemd/system/ -p wa -k systemd_changes
-w /usr/lib/systemd/system/ -p wa -k systemd_changes

# Track kernel module loading/unloading
-w /sbin/modprobe -p x -k kernel_modules
-w /usr/sbin/modprobe -p x -k kernel_modules

# Record attempts to become root
-w /bin/su -p x -k root_escalation

# Monitor unauthorized file permission or attribute changes
-a always,exit -F arch=b64 -S chmod,chown,chgrp,fchmod,fchown -k perms
-a always,exit -F arch=b32 -S chmod,chown,chgrp,fchmod,fchown -k perms

# Detect root-level shell spawning (/bin/bash executed by UID 0)
-a always,exit -F arch=b64 -S execve -F uid=0 -k root_shell
-a always,exit -F arch=b32 -S execve -F uid=0 -k root_shell

# Detect modifications to firewall rules
-w /etc/ufw/ -p wa -k ufw_changes
-w /etc/firewalld/ -p wa -k firewalld_changes

# Track crontab modifications (cron persistence attacks)
-w /etc/crontab -p wa -k cron_changes
-w /etc/cron.d/ -p wa -k cron_changes
-w /etc/cron.daily/ -p wa -k cron_changes
-w /etc/cron.hourly/ -p wa -k cron_changes

# Ensure audit configuration itself is immutable
-e 2
EOF
	echo "πŸ”„ Reloading audit rules..."
	sudo augenrules --load
	sudo systemctl restart auditd

	echo "βœ… auditd hardening rules installed!"
fi

echo "πŸ’― Finished initial VPS Setup! Goodbye!"
  • Run:
curl -O https://gist.githubusercontent.com/username/abcdef1234567890/raw/harden.sh
chmod +x ./harden.sh
sudo ./harden.sh
# One-liner, no download
curl -sSL https://gist.githubusercontent.com/username/abcdef1234567890/raw/harden.sh | sudo bash

πŸ›‘οΈ SSH Protection Layers β€” Basic to Advanced

Layer 1 β€” Immediate Basics (Do These First) - sshd_config
No.ActionCommand/File
1Change default SSH portPort XXXX
2Disable root loginPermitRootLogin no
3Limit max auth triesMaxAuthTries 3
4Reduce login graceLoginGraceTime 30
5Restrict to specific userAllowUsers <<yourusername>>
Layer 2 β€” Key-Based Auth (Eliminates Brute Force)
No.ActionCommand/File
6Generate ed25519 keypairssh-keygen -t ed25519
7Copy key to serverssh-copy-id
8Disable password authPasswordAuthentication no
9Disable empty passwordsPermitEmptyPasswords no
Layer 3 β€” Firewall (UFW)
No.ActionCommand/File
10Default deny all incomingufw default deny incoming
11Allow only your SSH portufw allow [YOUR_PORT]/tcp
12Whitelist your IP for SSHufw allow from [YOUR_IP] to any port [YOUR_PORT]
13Enable UFWufw enable
Layer 4 β€” Auto-Blocking (fail2ban)
No.ActionCommand/File
14Install fail2bansudo apt install fail2ban
15Configure [sshd]jailmaxretry=5, bantime=1h
16Recidive jail (repeat offenders)bantime=-1 permanent ban
17Email alerts on bansaction = %(action_mwl)
Layer 5 β€” Log Monitoring (Visibility)
No.ActionCommand/File
18Check failed loginsgrep "Failed password" /var/log/auth.log
19Find top attacker IPsawk '{print $11}' | sort | uniq -c | sort -rn
20Verify successful loginsgrep β€œAccepted” /var/log/auth.log
21Watch live auth logtail -f /var/log/auth.log
Layer 6 β€” Advanced Hardening
No.ActionCommand/File
22Fix Docker bypassing UFWdaemon.json β†’ "iptables": false
23Bind services to localhostdocker-compose.yml β†’ 127.0.0.1:PORT
24Block Postgres publiclyufw deny 5432
25Nginx attack detection customfail2ban filter jail
26Block bot scannerprobesbotsearch-common, fail2ban jail
Layer 7 β€” Expert Level
No.ActionCommand/File
27Two-factor auth for SSHlibpam-google-authenticator
28Port knockingknockd
29Geo-blocking by countrygeoip-shell or nftables
30Intrusion detection systemOSSEC or Wazuh
31Centralized log monitoringGrafana + Loki + Promtail