Your VPS faces hundreds of SSH brute-force attempts daily. This checklist hardens SSH against every common attack — key-only auth, non-root user, fail2ban, port hopping, 2FA, jump host — all tested on DomainIndia AlmaLinux / Ubuntu VPS.
The attack surface
Scanners probe every public IP's port 22 continuously. Logs on a new VPS show 500-2000 failed SSH attempts per day within the first hour. Default configuration is a ticking bomb.
The eight layers of SSH defense:
- Disable password auth entirely
- Require SSH key authentication
- Disable root login
- Create a non-root sudo user
- Install fail2ban
- Firewall rules
- (Optional) Change SSH port
- (Optional) Two-factor auth + jump host
Layer 1 — Generate strong SSH key (on your laptop)
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/domainindia_vps -N 'strong-passphrase'ed25519is faster and more secure than RSA- Passphrase protects the key if your laptop is stolen
Add to SSH agent:
ssh-add ~/.ssh/domainindia_vpsLayer 2 — Add public key to VPS
# From laptop
ssh-copy-id -i ~/.ssh/domainindia_vps.pub root@your-vps-ip
# Or manually — paste contents of .pub file into:
# /root/.ssh/authorized_keys on VPSTest before continuing:
ssh -i ~/.ssh/domainindia_vps root@your-vps-ip
# Should succeed without prompting for passwordCritical — don't proceed until key login works.
Layer 3 — Create non-root user
# On VPS as root
adduser admin # or your name
usermod -aG wheel admin # AlmaLinux (sudo group)
# or on Ubuntu:
usermod -aG sudo admin
# Copy authorized_keys
mkdir -p /home/admin/.ssh
cp /root/.ssh/authorized_keys /home/admin/.ssh/
chown -R admin:admin /home/admin/.ssh
chmod 700 /home/admin/.ssh
chmod 600 /home/admin/.ssh/authorized_keysTest login as new user:
ssh -i ~/.ssh/domainindia_vps admin@your-vps-ip
sudo whoami # should return "root" after password promptConfigure passwordless sudo (optional, for automation):
echo 'admin ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/adminLayer 4 — Harden sshd config
Edit /etc/ssh/sshd_config:
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
sudo vi /etc/ssh/sshd_configSet these:
# Disable root login entirely
PermitRootLogin no
# Keys only
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
UsePAM yes
# Limit to specific users
AllowUsers admin deploy
# Disable empty passwords
PermitEmptyPasswords no
# Limit protocol
Protocol 2
# Disable X11 forwarding if not needed
X11Forwarding no
# Reduce login grace time
LoginGraceTime 30
# Max auth tries per connection
MaxAuthTries 3
# Session timeout (idle disconnect)
ClientAliveInterval 300
ClientAliveCountMax 2
# Modern ciphers only
Ciphers [email protected],[email protected],aes256-ctr
KexAlgorithms curve25519-sha256,[email protected],diffie-hellman-group16-sha512
MACs [email protected],[email protected]Validate + reload:
sudo sshd -t # validate syntax — must return no output
sudo systemctl reload sshdCritical: keep your current SSH session open. Open a NEW terminal to test. If locked out, use the old session to revert.
Layer 5 — fail2ban (auto-ban brute-forcers)
# AlmaLinux
sudo dnf install -y epel-release
sudo dnf install -y fail2ban
# Ubuntu
sudo apt install -y fail2banConfigure /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 24h
findtime = 10m
maxretry = 3
ignoreip = 127.0.0.1/8 YOUR_HOME_IP/32
[sshd]
enabled = true
port = ssh
logpath = /var/log/secure
backend = systemdStart:
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
# Currently banned: 2
# IP list: 45.123.x.x 62.45.x.xLayer 6 — Firewall
firewalld (AlmaLinux / Rocky)
sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reloadUFW (Ubuntu)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableRestrict SSH to specific IP
If you have a static IP at office:
# firewalld
sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="YOUR.OFFICE.IP/32" service name="ssh" accept'
sudo firewall-cmd --reload
# ufw
sudo ufw delete allow 22/tcp
sudo ufw allow from YOUR.OFFICE.IP to any port 22Layer 7 — Change SSH port (security through obscurity)
Scanners mostly hit port 22. Moving to 2222 or 22022 cuts 95% of noise (not a real defense, just log hygiene).
/etc/ssh/sshd_config:
Port 2222
# or add alongside 22 during transition:
# Port 22
# Port 2222Open firewall for new port, reload sshd, test, then close port 22:
# AlmaLinux: SELinux may block new port
sudo semanage port -a -t ssh_port_t -p tcp 2222
sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --reload
sudo systemctl reload sshd
ssh -p 2222 admin@your-vpsTell team. Update ~/.ssh/config on laptops:
Host domainindia-vps
HostName your-vps-ip
User admin
Port 2222
IdentityFile ~/.ssh/domainindia_vpsNow ssh domainindia-vps just works.
Layer 8 — Two-factor auth (Google Authenticator)
For extra paranoia:
sudo dnf install -y google-authenticator
# Ubuntu: sudo apt install libpam-google-authenticator
# As user (admin), run:
google-authenticator
# Scan QR with Authy/Google Auth on phone
# Save backup codes!Edit /etc/pam.d/sshd — add at top:
auth required pam_google_authenticator.soEdit /etc/ssh/sshd_config:
ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactiveReload sshd. Now login requires: SSH key AND 6-digit code.
Layer 9 — Jump host / bastion
For multiple VPS, expose SSH only on one bastion host. All others only accept SSH from bastion's internal IP.
~/.ssh/config:
Host bastion
HostName bastion.yourcompany.com
User admin
Host internal-*
User admin
ProxyJump bastionUsage:
ssh internal-db # routes via bastion automaticallyAttack surface: only bastion is public.
Monitoring
Watch SSH logs
sudo journalctl -u sshd --follow
# or
sudo tail -f /var/log/secureAlert on successful root-like sudo
Set up a simple cron that monitors and emails:
# /etc/cron.hourly/ssh-monitor
#!/bin/bash
LAST_HOUR=$(date -d '1 hour ago' +'%Y-%m-%d %H')
SUDO_COUNT=$(journalctl --since "$LAST_HOUR:00:00" | grep -c 'sudo.*COMMAND')
if [ "$SUDO_COUNT" -gt 20 ]; then
mail -s "High sudo activity on $(hostname)" [email protected] <<EOF
Sudo commands in past hour: $SUDO_COUNT
Review: journalctl --since "$LAST_HOUR:00:00" | grep sudo
EOF
fiAudit login sessions
# Last 20 logins
last -20
# Failed attempts
lastb -20
# Who is currently logged in
whoSSH config management
Keep your ~/.ssh/config organized:
# Default settings
Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 60
# DomainIndia VPS
Host domainindia-prod
HostName 65.108.x.x
User admin
Port 2222
IdentityFile ~/.ssh/domainindia_vps
Host domainindia-staging
HostName 65.108.y.y
User admin
Port 2222Common pitfalls
sshd -t before reload.sudo semanage port -a -t ssh_port_t -p tcp 2222.authorized_keysFAQ
Do I need fail2ban if I disabled password auth?
Yes — still useful. It blocks scanning bots wasting CPU on auth attempts even if they'd never succeed. Also catches misconfigured services exposing other ports.
Should I change port from 22?
Security benefit is marginal (scanners scan other ports too). Main benefit: cleaner logs. If you want it, fine; don't rely on it as defense.
SSH key compromised — what do I do?
1) Remove pub key from authorized_keys on every server. 2) Generate new key, add to authorized_keys. 3) Rotate any service account keys. 4) Review logs for unauthorized access since compromise.
Cloudflare Zero Trust for SSH?
Excellent option — no public SSH port, audit trail, MFA built in. See our Cloudflare Zero Trust article.
Can I disable SSH entirely?
On a VPS — no, you lose access. On a cPanel/DirectAdmin shared server — yes, use the panel. VPS admins always need some recovery path.
Harden your DomainIndia VPS from day one. Get a VPS