Privacy Tools Guide

A default OpenSSH installation accepts password authentication, root logins, and listens on port 22 — the first three things attackers target. This guide hardens SSH systematically: key authentication, cipher hardening, access controls, and automated blocking of brute-force attempts.

Step 1: Generate Ed25519 Keys (Client Side)

# Generate Ed25519 key pair (smaller and faster than RSA-4096)
ssh-keygen -t ed25519 -C "your@email.com" -f ~/.ssh/id_ed25519

# Use a strong passphrase — it protects the private key at rest

# If you need RSA (legacy compatibility):
ssh-keygen -t rsa -b 4096 -C "your@email.com" -f ~/.ssh/id_rsa

# View your public key for upload to servers
cat ~/.ssh/id_ed25519.pub

Step 2: Deploy the Public Key to the Server

# Copy public key to server (preferred method)
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server

# Or manually:
cat ~/.ssh/id_ed25519.pub | ssh user@server \
  "mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
   cat >> ~/.ssh/authorized_keys && \
   chmod 600 ~/.ssh/authorized_keys"

# Verify you can log in with key before disabling passwords
ssh -i ~/.ssh/id_ed25519 user@server

Step 3: Harden sshd_config

Edit /etc/ssh/sshd_config:

sudo nano /etc/ssh/sshd_config

Apply these settings:

# Port (change from default 22 to reduce automated scan noise)
# Choose a port above 1024 that doesn't conflict with other services
Port 2222

# Protocol version (OpenSSH 7+ only supports v2 by default, but be explicit)
Protocol 2

# Key algorithms — prefer Ed25519 and ECDSA, disable old ones
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_ecdsa_key
# Remove or comment out RSA and DSA host keys if not needed

# Authentication
PermitRootLogin no
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes

# Public key authentication
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

# Key exchange algorithms — modern, strong algorithms only
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com

# Access control
AllowUsers youruser adminuser
# Or restrict to specific IPs:
# Match User deploy
#   AllowedUsers 10.0.0.0/24

# Session settings
ClientAliveInterval 300
ClientAliveCountMax 2
MaxAuthTries 3
MaxSessions 5
LoginGraceTime 20

# Disable features you don't need
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
PrintLastLog yes

# Logging
LogLevel VERBOSE
SyslogFacility AUTH

Validate and reload:

# Syntax check BEFORE reloading (critical — don't lock yourself out)
sudo sshd -t

# Reload
sudo systemctl reload sshd

# Keep your current session open and test in a NEW terminal window
ssh -p 2222 youruser@server

Step 4: Regenerate Host Keys

The default host keys generated at installation may use weak parameters. Regenerate:

# Remove old host keys
sudo rm /etc/ssh/ssh_host_*

# Generate new strong keys
sudo ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
sudo ssh-keygen -t ecdsa -b 521 -f /etc/ssh/ssh_host_ecdsa_key -N ""
# RSA 4096 for legacy client compatibility
sudo ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N ""

sudo systemctl restart sshd

Clients will see a “host key changed” warning — clear known_hosts and reconnect:

ssh-keygen -R server_ip
ssh-keygen -R server_hostname

Step 5: Install and Configure fail2ban

fail2ban monitors auth.log and blocks IPs that exceed a threshold of failed logins:

sudo apt install fail2ban

# Create local override (never edit the main .conf file)
sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 3
backend = systemd

[sshd]
enabled = true
port = 2222
logpath = %(sshd_log)s
maxretry = 3
bantime = 24h
EOF

sudo systemctl enable --now fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd

Check current bans:

sudo fail2ban-client status sshd
# Shows currently banned IPs

# Manually unban an IP (if you ban yourself)
sudo fail2ban-client set sshd unbanip YOUR_IP

Step 6: UFW Rules for SSH

# Allow SSH only from a specific IP (strongest option)
sudo ufw allow from YOUR_OFFICE_IP to any port 2222 proto tcp

# Or rate-limit if you need access from any IP
sudo ufw limit 2222/tcp comment "SSH rate limited"

sudo ufw enable
sudo ufw status

Step 7: Two-Factor Authentication (Optional)

Add TOTP 2FA on top of key authentication for additional protection:

sudo apt install libpam-google-authenticator

# Run as the user who will log in
google-authenticator
# Answer yes to all prompts
# Save the emergency scratch codes

# Edit /etc/pam.d/sshd
# Add BEFORE the @include common-auth line:
# auth required pam_google_authenticator.so

# Edit /etc/ssh/sshd_config
# AuthenticationMethods publickey,keyboard-interactive
# ChallengeResponseAuthentication yes

sudo systemctl restart sshd

Now SSH requires both your private key AND the TOTP code.

Verify Your Hardened Configuration

# Check what ciphers the server advertises
ssh -vvv user@server 2>&1 | grep -i "kex\|cipher\|hmac"

# Audit with ssh-audit
pip3 install ssh-audit
ssh-audit server:2222

# Check for common vulnerabilities
# ssh-audit grades your configuration and lists specific issues

SSH Client Configuration

Harden your local SSH client too. Edit ~/.ssh/config:

Host *
    # Use Ed25519 keys preferentially
    IdentityFile ~/.ssh/id_ed25519

    # Strong algorithms only
    KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
    Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
    MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com

    # Security
    VerifyHostKeyDNS yes
    AddKeysToAgent yes
    StrictHostKeyChecking ask

    # Connection reuse (faster connections after first)
    ControlMaster auto
    ControlPath ~/.ssh/controlmasters/%r@%h:%p
    ControlPersist 10m

SSH Jump Hosts and Bastion Configuration

For teams that access production servers from different networks, a bastion (jump host) is cleaner than opening SSH to the world on every server. Only the bastion has port 2222 open; production servers accept SSH only from the bastion’s IP.

# On production servers — allow SSH only from bastion
sudo ufw allow from BASTION_IP to any port 2222 proto tcp
sudo ufw deny 2222/tcp  # deny all other sources
sudo ufw enable

Configure your client ~/.ssh/config to tunnel through the bastion transparently:

# ~/.ssh/config

Host bastion
    HostName bastion.example.com
    User admin
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
    ServerAliveInterval 60

Host prod-*
    User deploy
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
    ProxyJump bastion
    # ProxyCommand alternative for older OpenSSH:
    # ProxyCommand ssh -W %h:%p bastion

Host prod-web-01
    HostName 10.0.1.10

Host prod-db-01
    HostName 10.0.2.10

With this config, ssh prod-web-01 connects to 10.0.1.10 through the bastion without any manual tunneling. The bastion itself should be hardened identically to production servers — it is a high-value target.

Restrict the bastion’s sshd_config further to prevent it from being used as a pivot:

# Additional bastion-specific sshd_config settings
AllowTcpForwarding local    # allow local port forwarding, not remote
GatewayPorts no             # no remote port forwarding
AllowStreamLocalForwarding no
PermitTTY yes               # bastion users need a shell
ForceCommand /usr/bin/bastion-shell  # optional: restrict to a menu script

Detecting SSH Brute Force Attempts

fail2ban blocks IPs after repeated failures, but reviewing the raw attack pattern helps tune your configuration. Check auth.log for attack signatures:

# Count failed SSH attempts by IP (last 24 hours)
grep "Failed password\|Invalid user" /var/log/auth.log \
  | awk '{print $(NF-3)}' \
  | sort | uniq -c | sort -rn | head -20

# Show currently banned IPs with unban time
sudo fail2ban-client status sshd

# Watch live attack attempts
sudo tail -f /var/log/auth.log | grep -E "Failed|Invalid|Accepted"

# Count valid logins vs failed attempts
echo "Successful logins:"
grep "Accepted" /var/log/auth.log | wc -l
echo "Failed attempts:"
grep "Failed password" /var/log/auth.log | wc -l

For high-volume servers, consider sshguard as an alternative — it monitors multiple log formats (sshd, nginx, postfix) and uses exponential backoff:

sudo apt install sshguard

# sshguard integrates directly with nftables on newer systems
# Check blocked hosts:
sudo sshguard -l /var/log/auth.log
sudo nft list set inet sshguard attackers

Set LoginGraceTime 10 in sshd_config to close unauthenticated connections faster — this helps when scanners hold connections open to occupy your MaxStartups slots:

MaxStartups 10:30:60  # start throttling at 10, drop at 60 unauthenticated connections
LoginGraceTime 10     # close unauthenticated connection after 10 seconds