Complete Documentation for Production VM or VPS (Hetzner/Cloud Environments)
Last Updated: November 2025
Target OS: Ubuntu Server (24.04+)
Environment: Production VPS
Table of Contents
- User Management & SSH Key Setup
- SSH Hardening
- Firewall Configuration (UFW)
- Fail2ban Setup
- Automatic Security Updates
- System Hardening (sysctl)
- Verification & Testing
- Emergency Access
- Maintenance
1. User Management & SSH Key Setup
1.1 Generate ED25519 SSH Key (On Your Local Machine)
ED25519 is the most secure and modern SSH key algorithm. It’s faster and more secure than RSA.
# Generate new ED25519 key pair
ssh-keygen -t ed25519 -C "[email protected]"
# Save to default location: ~/.ssh/id_ed25519
# SET A STRONG PASSPHRASE for the key
Why ED25519?
- Faster than RSA
- More secure (256-bit security, equivalent to RSA 3072-bit)
- Smaller key size
- Resistant to timing attacks
- Modern standard recommended by security experts
Why passphrase?
It’s considered best practice to protect the private key with a passphrase. It only needs to be entered once per session, and can be a last resort protection in case the key leaks.
1.2 Create Admin User on Server
CRITICAL: Do NOT disable root or remove password authentication until you verify the new user works!
# Create new admin user
adduser bob
# Follow prompts to set:
# - Strong password (used for sudo commands)
# - Full name (optional)
# - Other info (skip)
# Add user to sudo group (gives administrative privileges)
usermod -aG sudo bob
# Verify sudo access
groups bob
# Should show: bob : bob sudo
Why not use root directly?
- Defense in depth: SSH key + password for sudo = two layers
- Audit trail: All privileged commands are logged with username
- Prevents accidents: sudo password prompt makes you think
- Compliance: Most security standards require this separation
- Reduces attack surface: Compromised SSH key alone can’t get root
1.3 Set Up SSH Key for New User
# Create .ssh directory with correct permissions
mkdir -p /home/bob/.ssh
chmod 700 /home/bob/.ssh
# Create authorized_keys file
nano /home/bob/.ssh/authorized_keys
# Paste your public key (from ~/.ssh/id_ed25519.pub on local machine)
# The key looks like: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... [email protected]
# Set correct permissions (CRITICAL - SSH will reject wrong permissions)
chmod 600 /home/bob/.ssh/authorized_keys
chown -R bob:bob /home/bob/.ssh
Permission Explanation:
700on .ssh directory = only owner can read/write/execute (rwx——)600on authorized_keys = only owner can read/write (rw——-)- Wrong permissions = SSH will refuse to use the key for security reasons
1.4 Test New User Login
CRITICAL STEP
Open a new terminal window (keep root session open as backup):
# Test SSH key login
ssh bob@your_server_ip/hostname
# Should login without password prompt
# If it asks for password, the key setup failed - troubleshoot before continuing
# Test sudo access
sudo -i
# Should ask for user's password, then give root shell
# If both work, you're ready to proceed
1.5 Ensure Root Has Password (Emergency Access)
# Check root password status
sudo passwd -S root
# Should show "P" (password set), not "L" (locked) or "NP" (no password)
# If root has no password, set one NOW (for console access)
sudo passwd root
# Set a STRONG password and store it securely
# You'll need this if you lock yourself out and need console access
Why root needs a password:
- VPS console access (Hetzner KVM, etc.) only allows root login
- Emergency recovery if SSH config breaks
- Single-user/rescue mode access
- NOT used for SSH (we’re disabling root SSH login)
2. SSH Hardening
2.1 Complete sshd_config
File: /etc/ssh/sshd_config
# Edit SSH configuration
sudo nano /etc/ssh/sshd_config
Replace entire file with this hardened configuration:
# ============================================
# SSH Server Configuration - Security Hardened
# Ubuntu Server Production Environment
# ============================================
# Include drop-in configs from sshd_config.d/
# This allows modular configuration management
Include /etc/ssh/sshd_config.d/*.conf
# --------------------------------------------
# Network Configuration
# --------------------------------------------
# Port to listen on (default: 22)
# Changing this provides minimal security through obscurity
# Better security comes from firewall rules and key-based auth
Port 22
# Address family to use (any = IPv4 and IPv6)
AddressFamily any
# Listen on all IPv4 addresses
ListenAddress 0.0.0.0
# Listen on all IPv6 addresses
ListenAddress ::
# --------------------------------------------
# Host Keys (Server Identity)
# --------------------------------------------
# Only use ED25519 host key (most secure)
# RSA and ECDSA are older/weaker algorithms
# When client connects, server proves identity with this key
HostKey /etc/ssh/ssh_host_ed25519_key
# If you need backward compatibility with old clients, also enable:
# HostKey /etc/ssh/ssh_host_rsa_key
# HostKey /etc/ssh/ssh_host_ecdsa_key
# --------------------------------------------
# Logging
# --------------------------------------------
# Where to send logs (AUTH facility = /var/log/auth.log)
SyslogFacility AUTH
# Logging level - VERBOSE logs fingerprints and provides audit trail
# Options: QUIET, FATAL, ERROR, INFO, VERBOSE, DEBUG, DEBUG1, DEBUG2, DEBUG3
LogLevel VERBOSE
# --------------------------------------------
# Authentication Settings
# --------------------------------------------
# Time allowed for authentication before disconnect (seconds)
# Short time reduces exposure to brute force attacks
LoginGraceTime 30
# Disable root login via SSH entirely
# Root can still be accessed via 'sudo -i' from admin user
# This provides defense in depth and audit trail
PermitRootLogin no
# Verify file modes and ownership before accepting login
# Prevents security issues from world-writable config files
StrictModes yes
# Maximum authentication attempts per connection
# After this many failures, connection is dropped
MaxAuthTries 3
# Maximum number of open sessions per connection
MaxSessions 2
# Enable public key authentication (SSH keys)
PubkeyAuthentication yes
# Location of authorized public keys
# Default is .ssh/authorized_keys in user's home directory
AuthorizedKeysFile .ssh/authorized_keys
# --------------------------------------------
# Disable Dangerous Authentication Methods
# --------------------------------------------
# Disable host-based authentication (trusting client machines)
# This is almost never needed and is a security risk
HostbasedAuthentication no
# Ignore .rhosts and .shosts files (legacy Unix trust files)
IgnoreRhosts yes
# CRITICAL: Disable password authentication
# Only SSH keys are allowed - prevents brute force attacks
PasswordAuthentication no
# Don't allow empty passwords (redundant with above, but explicit)
PermitEmptyPasswords no
# Disable challenge-response authentication (keyboard-interactive)
# This can be used to bypass password restrictions
ChallengeResponseAuthentication no
# Disable Kerberos authentication (enterprise auth system)
# Only enable if you specifically need Kerberos
KerberosAuthentication no
# Disable GSSAPI authentication (generic security services)
# Only enable if you specifically need GSSAPI
GSSAPIAuthentication no
# --------------------------------------------
# PAM (Pluggable Authentication Modules)
# --------------------------------------------
# Enable PAM for account and session management
# PAM provides additional security features like password policies
# Required for proper user session setup
UsePAM yes
# --------------------------------------------
# Disable Forwarding Features
# --------------------------------------------
# Disable SSH agent forwarding
# Agent forwarding can be exploited on compromised servers
AllowAgentForwarding no
# Disable TCP forwarding (port forwarding)
# Prevents using SSH as a tunnel to access internal services
AllowTcpForwarding no
# Disable X11 forwarding (graphical applications over SSH)
# Not needed on headless servers and is a security risk
X11Forwarding no
# Disable tunnel device forwarding (VPN-like functionality)
# Prevents creating VPN tunnels through SSH
PermitTunnel no
# --------------------------------------------
# Connection Keep-Alive
# --------------------------------------------
# Send TCP keepalive messages to detect dead connections
# Prevents hanging connections from occupying resources
TCPKeepAlive yes
# Send keep-alive message every 300 seconds (5 minutes)
# If client doesn't respond, connection is kept alive
ClientAliveInterval 300
# Maximum number of keep-alive messages without response
# After this many unanswered messages, disconnect client
# Total timeout = ClientAliveInterval * ClientAliveCountMax = 10 minutes
ClientAliveCountMax 2
# --------------------------------------------
# Miscellaneous Settings
# --------------------------------------------
# Don't print /etc/motd on login
# CloudPanel or other systems may manage their own MOTD
PrintMotd no
# Don't perform reverse DNS lookup on client IP
# Slightly faster connections, no security benefit from DNS lookup
# May need to enable in the future after asessing the potential risks
UseDNS no
# Accept locale environment variables from client
# Allows proper character encoding for non-English locales
AcceptEnv LANG LC_*
# --------------------------------------------
# SFTP Subsystem
# --------------------------------------------
# Enable SFTP (secure file transfer) subsystem
# Uses internal-sftp which is more secure than external sftp-server
Subsystem sftp /usr/lib/openssh/sftp-server
# --------------------------------------------
# User Access Control
# --------------------------------------------
# CRITICAL: Only allow specific users to SSH
# Add additional users separated by spaces as needed
# Example: AllowUsers bob alice charlie
AllowUsers bob
# Alternative restrictions (choose only one method):
# DenyUsers username1 username2 # Block specific users
# AllowGroups groupname # Only allow users in specific group
# DenyGroups groupname # Block users in specific group
# --------------------------------------------
# Advanced Security Options (Optional)
# --------------------------------------------
# Uncomment these for even stricter security:
# Limit SSH to specific key exchange algorithms (most secure)
# KexAlgorithms curve25519-sha256,[email protected]
# Limit to specific ciphers (most secure, authenticated encryption)
# Ciphers [email protected],[email protected],[email protected]
# Limit to specific MAC algorithms (most secure)
# MACs [email protected],[email protected]
# --------------------------------------------
# Notes
# --------------------------------------------
# After editing this file:
# 1. Test configuration: sudo sshd -t
# 2. If test passes, restart SSH: sudo systemctl restart sshd
# 3. IMPORTANT: Test login in NEW terminal before closing current session!
# 4. Keep one session open until you verify new config works
# Emergency access:
# - Use VPS console (Hetzner KVM, etc.) to login as root
# - Root password is required for console access
# - Never disable root account entirely
2.2 Apply SSH Configuration
# Test configuration for syntax errors
# This does NOT apply changes, just validates syntax
sudo sshd -t
# If no errors shown, configuration is valid
# If errors appear, fix them before proceeding
# Apply configuration by restarting SSH service (ssh instead of sshd on Ubuntu)
sudo systemctl restart sshd
# Check if SSH service is running (ssh instead of sshd on Ubuntu)
sudo systemctl status sshd
2.3 Critical Testing Step
DO NOT CLOSE YOUR CURRENT SSH SESSION YET!
# In a NEW terminal window, test connection:
ssh bob@your_server_ip
# Should connect with key, no password
# Test sudo:
sudo -i
# Should ask for bob's password
# Try to connect as root (should FAIL):
ssh root@your_server_ip
# Should get "Permission denied"
# Only after confirming everything works, close old session
If login fails:
- Keep your current root session open
- Check
/var/log/auth.logfor errors:sudo tail -f /var/log/auth.log - Revert sshd_config changes
- Restart sshd
- Troubleshoot the issue
3. Firewall Configuration (UFW)
UFW (Uncomplicated Firewall) is a user-friendly frontend for iptables.
3.1 Basic UFW Setup
# Install UFW (usually pre-installed on Ubuntu)
sudo apt update
sudo apt install ufw -y
# Set default policies
# DENY all incoming connections by default (whitelist approach)
sudo ufw default deny incoming
# ALLOW all outgoing connections (server can initiate connections)
sudo ufw default allow outgoing
Why these defaults?
- Deny incoming = whitelist security model (only explicitly allowed ports are open)
- Allow outgoing = server can download updates, send email, fetch data
- More secure than blacklist approach (blocking specific ports)
3.2 Allow Essential Services
# Allow SSH (port 22) from specific IPs only (MOST SECURE)
# Replace with YOUR actual IP addresses
sudo ufw allow from 31.30.174.187 to any port 22 proto tcp
sudo ufw allow from 78.156.157.77 to any port 22 proto tcp
# OR allow SSH from anywhere (less secure, use only if IPs change frequently)
# sudo ufw allow 22/tcp
# Allow HTTP (web traffic)
sudo ufw allow 80/tcp
# Allow HTTPS (secure web traffic)
sudo ufw allow 443/tcp
# Allow HTTP/3 (QUIC protocol) if needed
# QUIC uses UDP, not TCP
sudo ufw allow 443/udp
# CloudPanel admin interface - localhost only (reverse proxy access)
# This allows internal nginx to proxy to CloudPanel
sudo ufw allow from 127.0.0.1 to any port 8443 proto tcp
sudo ufw allow from ::1 to any port 8443 proto tcp
# Allow IMAPS (secure email access) if hosting email or whatever
sudo ufw allow 993/tcp
# FTP passive mode (if using ProFTPD) - restrict to your IP
# High port range for FTP data connections
sudo ufw allow from 31.30.174.187 to any port 49152:65534 proto tcp
3.3 Service-Specific Port Reference
Common ports you might need:
# Web Services
# 80/tcp - HTTP (unencrypted web traffic)
# 443/tcp - HTTPS (encrypted web traffic)
# 443/udp - HTTP/3 QUIC (modern HTTP protocol)
# 8443/tcp - Alternative HTTPS port (CloudPanel uses this)
# Email Services
# 25/tcp - SMTP (outgoing mail, server-to-server)
# 587/tcp - SMTP Submission (authenticated outgoing mail)
# 465/tcp - SMTPS (legacy encrypted SMTP)
# 143/tcp - IMAP (unencrypted mail access)
# 993/tcp - IMAPS (encrypted mail access)
# 110/tcp - POP3 (unencrypted mail download)
# 995/tcp - POP3S (encrypted mail download)
# File Transfer
# 21/tcp - FTP control (insecure, avoid if possible)
# 22/tcp - SFTP (secure FTP over SSH, preferred)
# 49152:65534/tcp - FTP passive mode data connections
# Database (ONLY if remote access needed, otherwise keep firewalled)
# 3306/tcp - MySQL/MariaDB
# 5432/tcp - PostgreSQL
# 6379/tcp - Redis
# 27017/tcp - MongoDB
# Monitoring
# 10050/tcp - Zabbix agent
# 10051/tcp - Zabbix server
# Other
# 53/tcp+udp - DNS server
# 123/udp - NTP (time sync)
3.4 Enable Firewall
# Enable UFW
sudo ufw enable
# You'll see warning about SSH connections
# This is safe if you already allowed port 22
# Check status and rules
sudo ufw status verbose
# Expected output:
# Status: active
# Logging: on (low)
# Default: deny (incoming), allow (outgoing), disabled (routed)
#
# To Action From
# -- ------ ----
# 22/tcp ALLOW 31.30.174.187
# 22/tcp ALLOW 78.156.157.77
# 80/tcp ALLOW Anywhere
# 443/tcp ALLOW Anywhere
# ... etc
3.5 UFW Management Commands
# View current rules
sudo ufw status
sudo ufw status numbered # Shows rule numbers for deletion
sudo ufw status verbose # Shows detailed information
# Add rules
sudo ufw allow 80/tcp # Allow port from anywhere
sudo ufw allow from 1.2.3.4 to any port 22 # Allow port from specific IP
sudo ufw allow from 1.2.3.0/24 to any port 3306 # Allow port from subnet
# Delete rules
sudo ufw delete allow 80/tcp # Delete by rule specification
sudo ufw status numbered # List rules with numbers
sudo ufw delete 5 # Delete rule number 5
# Disable/Enable firewall
sudo ufw disable # Turn off firewall
sudo ufw enable # Turn on firewall
# Reset firewall (removes all rules)
sudo ufw reset
# Reload firewall (apply changes)
sudo ufw reload
3.6 CloudPanel Firewall Integration
Important: CloudPanel may manage its own firewall rules. If your manual UFW rules keep getting overwritten:
- Check CloudPanel’s web interface for firewall settings
- Configure ports through CloudPanel instead of manual UFW commands
- CloudPanel typically manages rules via its admin panel
4. Fail2ban Setup
Fail2ban monitors log files and bans IPs that show malicious behavior (repeated failed login attempts).
4.1 Install Fail2ban
# Install fail2ban
sudo apt update
sudo apt install fail2ban -y
4.2 Configure Fail2ban
File: /etc/fail2ban/jail.local
# Create local configuration (overrides defaults)
sudo nano /etc/fail2ban/jail.local
Add this configuration:
# ============================================
# Fail2ban Configuration - SSH Protection
# ============================================
[DEFAULT]
# Duration of ban in seconds (3600 = 1 hour)
# After ban expires, IP can try again
bantime = 3600
# Time window to count failures (600 = 10 minutes)
# If maxretry failures occur within findtime, IP is banned
findtime = 600
# Number of failures before ban
maxretry = 3
# Ban action to use
# 'ufw' integrates with UFW firewall
# Other options: iptables-multiport, iptables-allports
banaction = ufw
# Email notifications (optional)
# Uncomment and configure if you want ban notifications
# destemail = [email protected]
# sender = [email protected]
# action = %(action_mwl)s
# ============================================
# SSH Protection Jail
# ============================================
[sshd]
# Enable this jail enabled = true # Port to monitor (must match your SSH port) port = 22 # Log file to monitor for failed attempts # Ubuntu/Debian use /var/log/auth.log # CentOS/RHEL use /var/log/secure logpath = /var/log/auth.log # Number of failures before ban (can override default) maxretry = 3 # Ban time for SSH (can override default) # 3600 = 1 hour bantime = 3600 # Find time window (can override default) findtime = 600 # ============================================
# Notes # ============================================ # Fail2ban protects against: # 1. SSH key brute force (trying many keys) # 2. Invalid username probing # 3. Protocol violations # 4. Connection spam/DoS # With SSH restricted to specific IPs via UFW, # fail2ban provides additional protection in case # your IP is compromised or you open SSH widely. # View banned IPs: # sudo fail2ban-client status sshd # Unban an IP manually: # sudo fail2ban-client set sshd unbanip 1.2.3.4 # Check fail2ban logs: # sudo tail -f /var/log/fail2ban.log
4.3 Enable and Start Fail2ban
# Enable fail2ban to start on boot
sudo systemctl enable fail2ban
# Start fail2ban service
sudo systemctl start fail2ban
# Check service status
sudo systemctl status fail2ban
# Should show "active (running)"
4.4 Verify Fail2ban is Working
# Check fail2ban status
sudo fail2ban-client status
# Should show:
# Status
# |- Number of jail: 1
# `- Jail list: sshd
# Check SSH jail specifically
sudo fail2ban-client status sshd
# Should show:
# Status for the jail: sshd
# |- Filter
# | |- Currently failed: 0
# | |- Total failed: 0
# | `- File list: /var/log/auth.log
# `- Actions
# |- Currently banned: 0
# |- Total banned: 0
# `- Banned IP list:
# Monitor fail2ban log
sudo tail -f /var/log/fail2ban.log
# You'll see bans happen in real-time as bots attack
4.5 Fail2ban Management Commands
# Check status of all jails
sudo fail2ban-client status
# Check specific jail
sudo fail2ban-client status sshd
# Unban an IP address
sudo fail2ban-client set sshd unbanip 1.2.3.4
# Ban an IP address manually
sudo fail2ban-client set sshd banip 1.2.3.4
# Reload fail2ban configuration
sudo fail2ban-client reload
# Stop/start fail2ban
sudo systemctl stop fail2ban
sudo systemctl start fail2ban
sudo systemctl restart fail2ban
# View fail2ban logs
sudo tail -f /var/log/fail2ban.log
# View which IPs are currently banned
sudo fail2ban-client status sshd | grep "Banned IP"
# See auth log for failed attempts
sudo grep "Failed password" /var/log/auth.log
sudo grep "Invalid user" /var/log/auth.log
5. Automatic Security Updates
Unattended-upgrades automatically installs security updates, keeping your server protected without manual intervention.
5.1 Install Unattended-Upgrades
# Install package
sudo apt update
sudo apt install unattended-upgrades -y
# Configure interactively
sudo dpkg-reconfigure -plow unattended-upgrades
# Select "Yes" to enable automatic updates
5.2 Configure Unattended-Upgrades
File: /etc/apt/apt.conf.d/50unattended-upgrades
# Edit configuration
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
Key settings to configure:
// Automatically upgrade packages from these origins
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}";
"${distro_id}:${distro_codename}-security";
// Also upgrade from updates repository (optional)
"${distro_id}:${distro_codename}-updates";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
// List of packages to NOT automatically upgrade
// Add packages that need careful manual updates
Unattended-Upgrade::Package-Blacklist {
// Example: don't auto-update database
// "mysql-server";
// "postgresql";
};
// Automatically remove unused kernel packages
// Frees up disk space
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
// Automatically remove unused dependencies
// Keeps system clean
Unattended-Upgrade::Remove-Unused-Dependencies "true";
// Automatically reboot if needed (e.g., kernel updates)
// IMPORTANT: This will reboot your server without asking!
// Set to "false" if you want to control reboot timing
Unattended-Upgrade::Automatic-Reboot "true";
// Time to reboot (if automatic reboot is enabled)
// 03:00 = 3 AM (low-traffic time), depending on time zone
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
// Send email notifications about updates
// Requires working mail system
// Uncomment and configure if you want notifications:
// Unattended-Upgrade::Mail "[email protected]";
// Unattended-Upgrade::MailReport "on-change";
// Automatically reboot even if users are logged in
// DANGEROUS: Only enable if you understand the implications
// Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
5.3 Automatic Reboot Considerations
Pros of automatic reboots:
- Kernel security updates take effect immediately
- Fully automated security patching
- No manual intervention needed
Cons of automatic reboots:
- Unexpected downtime (even if scheduled at 3 AM)
- May interrupt long-running processes
- Could cause issues with stateful applications
Recommendations:
- Production with high availability requirements: Set
Automatic-Reboot "false", manually reboot during maintenance windows - Personal/development servers: Set
Automatic-Reboot "true"at low-traffic time - Mission-critical servers: Use monitoring to detect pending reboots, schedule manually
5.4 Configure Update Frequency
File: /etc/apt/apt.conf.d/20auto-upgrades
# Edit auto-upgrade configuration
sudo nano /etc/apt/apt.conf.d/20auto-upgrades
// Update package lists daily (1 = daily, 0 = disabled)
APT::Periodic::Update-Package-Lists "1";
// Automatically download upgradable packages daily
APT::Periodic::Download-Upgradeable-Packages "1";
// Automatically install security upgrades daily
APT::Periodic::Unattended-Upgrade "1";
// Clean package cache weekly (7 = every 7 days)
APT::Periodic::AutocleanInterval "7";
5.5 Test Unattended-Upgrades
# Perform a dry run (shows what would be upgraded without doing it)
sudo unattended-upgrades --dry-run --debug
# Check logs to see what's been upgraded
sudo cat /var/log/unattended-upgrades/unattended-upgrades.log
# Check if reboot is required
ls -la /var/run/reboot-required
cat /var/run/reboot-required.pkgs
# Manually trigger unattended-upgrades
sudo unattended-upgrades --debug
5.6 Monitor Automatic Updates
# View upgrade log
sudo tail -f /var/log/unattended-upgrades/unattended-upgrades.log
# View dpkg log (all package changes)
sudo tail -f /var/log/dpkg.log
# Check if system needs reboot
cat /var/run/reboot-required
# If file exists, reboot is needed
# See which packages require reboot
cat /var/run/reboot-required.pkgs
# Check when last reboot occurred
uptime
last reboot | head
6. System Hardening (sysctl)
Kernel parameters can be tuned for additional security.
6.1 Network Security Settings
File: /etc/sysctl.conf or /etc/sysctl.d/99-security.conf
# Edit sysctl configuration
sudo nano /etc/sysctl.d/99-security.conf
Add these security settings:
# ============================================
# Network Security Hardening
# ============================================
# Enable IP spoofing protection (reverse path filtering)
# Prevents packets from arriving on wrong network interface
# 1 = strict mode, 2 = loose mode, 0 = disabled
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Enable TCP SYN cookie protection (SYN flood attack mitigation)
# Protects against SYN flood DDoS attacks
# 1 = enabled when SYN queue is full
net.ipv4.tcp_syncookies = 1
# Disable ICMP redirect acceptance
# Prevents MITM attacks via ICMP redirects
# 0 = disabled, 1 = enabled
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
# Disable sending ICMP redirects
# Server should not tell others to reroute traffic
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
# Disable source packet routing
# Prevents attacker from specifying packet route
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
# Disable acceptance of secure ICMP redirects
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
# Log suspicious packets (martian packets)
# Packets with impossible source addresses
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
# Ignore ICMP ping requests (optional - prevents ping)
# 1 = ignore pings, 0 = respond to pings
# Disabling ping can make troubleshooting harder
# net.ipv4.icmp_echo_ignore_all = 1
# Ignore broadcast ICMP requests (smurf attack protection)
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Ignore bogus ICMP error responses
net.ipv4.icmp_ignore_bogus_error_responses = 1
# Enable TCP Fast Open (performance + security)
# Reduces latency and mitigates certain DDoS attacks
net.ipv4.tcp_fastopen = 3
# Increase TCP SYN backlog for better DDoS resistance
net.ipv4.tcp_max_syn_backlog = 2048
# Decrease TCP FIN timeout (faster cleanup of dead connections)
net.ipv4.tcp_fin_timeout = 15
# Disable IPv6 if not used (optional)
# Only disable if you don't use IPv6
# net.ipv6.conf.all.disable_ipv6 = 1
# net.ipv6.conf.default.disable_ipv6 = 1
# net.ipv6.conf.lo.disable_ipv6 = 1
# ============================================
# Kernel Security Hardening
# ============================================
# Restrict kernel pointer visibility (prevents info leaks)
# 1 = hide from unprivileged users, 0 = visible to all
kernel.kptr_restrict = 1
# Restrict dmesg access (prevents kernel info leaks)
# 1 = only root can read dmesg, 0 = all users can read
kernel.dmesg_restrict = 1
# Disable kernel address exposure in /proc
# Makes kernel exploitation harder
kernel.kptr_restrict = 2
# Enable ASLR (Address Space Layout Randomization)
# Randomizes memory addresses, makes exploits harder
# 2 = full randomization (recommended)
kernel.randomize_va_space = 2
# Restrict access to kernel logs
kernel.dmesg_restrict = 1
# ============================================
# File System Security
# ============================================
# Restrict core dumps (can contain sensitive data)
# Prevents non-privileged users from creating core dumps
fs.suid_dumpable = 0
# Protect hard links (prevents hard link attacks)
# Only allow links to files you own
fs.protected_hardlinks = 1
# Protect symbolic links (prevents symlink attacks)
# Only follow symlinks owned by the user
fs.protected_symlinks = 1
6.2 Apply sysctl Settings
# Load new sysctl settings
sudo sysctl -p /etc/sysctl.d/99-security.conf
# Or reload all sysctl configs
sudo sysctl --system
# Verify settings were applied
sudo sysctl net.ipv4.tcp_syncookies
# Should show: net.ipv4.tcp_syncookies = 1
sudo sysctl net.ipv4.conf.all.rp_filter
# Should show: net.ipv4.conf.all.rp_filter = 1
6.3 Verify Security Settings
# Check all security-related settings
sudo sysctl -a | grep -E "rp_filter|tcp_syncookies|accept_redirects|accept_source_route"
# Check kernel security settings
sudo sysctl -a | grep kernel.kptr_restrict
sudo sysctl -a | grep kernel.randomize_va_space
7. Verification & Testing
7.1 SSH Security Verification
# Test SSH configuration
sudo sshd -t
# Check SSH service status
sudo systemctl status sshd
# View active SSH connections
who
w
# Check SSH logs for failed attempts
sudo grep "Failed" /var/log/auth.log | tail -20
sudo grep "Invalid user" /var/log/auth.log | tail -20
# Test that root login is denied
# From another machine:
ssh root@your_server_ip
# Should get: Permission denied
# Test that password auth is disabled
# From another machine (without key):
ssh -o PubkeyAuthentication=no bob@your_server_ip
# Should get: Permission denied
7.2 Firewall Verification
# Check UFW status
sudo ufw status verbose
# Test open ports from external machine
# Install nmap: sudo apt install nmap
nmap -p 22,80,443,8443 your_server_ip
# Expected results:
# 22/tcp - open (if allowed from your IP) or filtered (if restricted)
# 80/tcp - open
# 443/tcp - open
# 8443/tcp - filtered (not accessible externally)
# Check if port is listening
sudo netstat -tlnp | grep :22
sudo netstat -tlnp | grep :80
sudo netstat -tlnp | grep :443
# Or using ss (modern alternative)
sudo ss -tlnp | grep :22
7.3 Fail2ban Verification
# Check fail2ban is running
sudo systemctl status fail2ban
# Check SSH jail status
sudo fail2ban-client status sshd
# View banned IPs (if any)
sudo fail2ban-client status sshd | grep "Banned IP"
# Check fail2ban logs
sudo tail -20 /var/log/fail2ban.log
# Simulate attack (optional - from another machine)
# Try to SSH with wrong credentials 3+ times
# Then check if IP gets banned:
sudo fail2ban-client status sshd
7.4 Update System Verification
# Check for available updates
sudo apt update
sudo apt list --upgradable
# View unattended-upgrade logs
sudo tail -50 /var/log/unattended-upgrades/unattended-upgrades.log
# Check if reboot is required
ls -la /var/run/reboot-required
cat /var/run/reboot-required.pkgs
# Check last upgrade time
ls -lh /var/log/unattended-upgrades/
7.5 Security Checklist
Run through this checklist periodically:
# [ ] SSH only allows key authentication
sudo grep "PasswordAuthentication" /etc/ssh/sshd_config
# Should show: PasswordAuthentication no
# [ ] Root SSH login is disabled
sudo grep "PermitRootLogin" /etc/ssh/sshd_config
# Should show: PermitRootLogin no
# [ ] Firewall is active
sudo ufw status
# Should show: Status: active
# [ ] Fail2ban is running
sudo systemctl is-active fail2ban
# Should show: active
# [ ] Unattended-upgrades is configured
dpkg -l | grep unattended-upgrades
# Should show: ii unattended-upgrades
# [ ] System is up to date
sudo apt update && sudo apt list --upgradable
# Should show: All packages up to date
# [ ] No unauthorized users have sudo access
grep -Po '^sudo.+:\K.*$' /etc/group
# Should only show authorized admin users
# [ ] No unauthorized SSH keys
for user in /home/*; do
if [ -f "$user/.ssh/authorized_keys" ]; then
echo "=== $user ==="
cat "$user/.ssh/authorized_keys"
fi
done
# [ ] Check for listening services
sudo netstat -tlnp
# Review and ensure only expected services are listening
8. Emergency Access
8.1 If You Lock Yourself Out
Option 1: VPS Console Access (Hetzner, DigitalOcean, etc.)
- Login to your VPS provider’s control panel
- Open console/KVM access (Hetzner calls this “Console”)
- Login as root using root’s password
- Fix SSH configuration:
nano /etc/ssh/sshd_config
# Fix the issue (add your user, fix syntax error, etc.)
systemctl restart sshd
- Test SSH from another terminal
- Exit console
Option 2: Rescue Mode/Recovery
- Boot into rescue mode via VPS control panel
- Mount main filesystem:
mkdir /mnt/system
mount /dev/sda1 /mnt/system # Adjust device name
chroot /mnt/system
- Fix configuration files
- Exit chroot and reboot
- Test SSH access
Option 3: Rebuild from Snapshot
If you have snapshots/backups:
- Restore from last known good snapshot
- Re-apply security hardening carefully
- Test each step before proceeding
8.2 Adding New Admin Users
When you need to add another administrator:
# Create new admin user
sudo adduser newadmin
# Add to sudo group
sudo usermod -aG sudo newadmin
# Set up SSH keys
sudo mkdir -p /home/newadmin/.ssh
sudo nano /home/newadmin/.ssh/authorized_keys
# Paste their public key
# Set correct permissions
sudo chmod 700 /home/newadmin/.ssh
sudo chmod 600 /home/newadmin/.ssh/authorized_keys
sudo chown -R newadmin:newadmin /home/newadmin/.ssh
# Add to SSH AllowUsers
sudo nano /etc/ssh/sshd_config
# Change: AllowUsers bob
# To: AllowUsers bob newadmin
# Test configuration
sudo sshd -t
# Restart SSH
sudo systemctl restart sshd
# Test new user login (in new terminal!)
ssh newadmin@your_server_ip
8.3 Temporary SSH Access
If you need to grant temporary access:
# Add temporary user
sudo adduser tempuser
# Add SSH key
sudo mkdir -p /home/tempuser/.ssh
sudo nano /home/tempuser/.ssh/authorized_keys
# Paste their key
sudo chmod 700 /home/tempuser/.ssh
sudo chmod 600 /home/tempuser/.ssh/authorized_keys
sudo chown -R tempuser:tempuser /home/tempuser/.ssh
# Allow in SSH config
sudo nano /etc/ssh/sshd_config
# Add to AllowUsers: bob tempuser
sudo systemctl restart sshd
# IMPORTANT: Set expiration date
sudo chage -E 2025-12-31 tempuser
# User account expires on specified date
# When done, remove user
sudo deluser --remove-home tempuser
# Remove from SSH AllowUsers and restart sshd
9. Maintenance
9.1 Regular Security Checks
Weekly:
# Check for available updates
sudo apt update
sudo apt list --upgradable
# Check fail2ban status
sudo fail2ban-client status sshd
# Review auth logs for suspicious activity
sudo grep "Failed password" /var/log/auth.log | tail -50
sudo grep "Invalid user" /var/log/auth.log | tail -50
# Check for unauthorized users
sudo grep -v '^#' /etc/passwd | awk -F: '$3 >= 1000 {print $1}'
# Check sudo access
sudo grep '^sudo:' /etc/group
Monthly:
# Review all authorized SSH keys
for user in /home/*; do
if [ -f "$user/.ssh/authorized_keys" ]; then
echo "=== $user ==="
cat "$user/.ssh/authorized_keys"
echo ""
fi
done
# Check for listening services
sudo netstat -tlnp
# Verify all services are expected
# Review firewall rules
sudo ufw status numbered
# Verify all rules are still needed
# Check for users with shell access
grep -v '/nologin\|/false' /etc/passwd
# Review cron jobs (potential backdoors)
sudo crontab -l
for user in /home/*; do
echo "=== Cron for $user ==="
sudo crontab -u $(basename $user) -l 2>/dev/null
done
Quarterly:
# Full security audit
sudo apt install lynis -y
sudo lynis audit system
# Review and act on Lynis recommendations
# Check for rootkits
sudo apt install rkhunter -y
sudo rkhunter --update
sudo rkhunter --check
# Update server documentation
# Review and update this document with any changes
9.2 Log Management
# View important logs
sudo tail -f /var/log/auth.log # SSH authentication
sudo tail -f /var/log/syslog # General system
sudo tail -f /var/log/ufw.log # Firewall
sudo tail -f /var/log/fail2ban.log # Fail2ban
# Search logs for specific patterns
sudo grep "Failed password" /var/log/auth.log
sudo grep "Accepted publickey" /var/log/auth.log
sudo grep "DENIED" /var/log/ufw.log
# Compress old logs to save space
sudo find /var/log -name "*.log" -mtime +30 -exec gzip {} \;
# Set up log rotation (usually configured by default)
sudo nano /etc/logrotate.d/custom
9.3 Backup Important Configs
Before making changes, always backup configurations:
# Create backup directory
sudo mkdir -p /root/config-backups
# Backup SSH config
sudo cp /etc/ssh/sshd_config /root/config-backups/sshd_config.$(date +%Y%m%d)
# Backup firewall rules
sudo ufw status numbered > /root/config-backups/ufw-rules.$(date +%Y%m%d).txt
# Backup fail2ban config
sudo cp /etc/fail2ban/jail.local /root/config-backups/jail.local.$(date +%Y%m%d)
# Backup user list
sudo cp /etc/passwd /root/config-backups/passwd.$(date +%Y%m%d)
sudo cp /etc/group /root/config-backups/group.$(date +%Y%m%d)
# Create automated backup script
sudo nano /root/backup-configs.sh
Backup script:
#!/bin/bash
# Automated config backup script
BACKUP_DIR="/root/config-backups"
DATE=$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP_DIR"
# Backup important configs
cp /etc/ssh/sshd_config "$BACKUP_DIR/sshd_config.$DATE"
cp /etc/fail2ban/jail.local "$BACKUP_DIR/jail.local.$DATE" 2>/dev/null
ufw status numbered > "$BACKUP_DIR/ufw-rules.$DATE.txt"
cp /etc/passwd "$BACKUP_DIR/passwd.$DATE"
cp /etc/group "$BACKUP_DIR/group.$DATE"
# Delete backups older than 90 days
find "$BACKUP_DIR" -name "*" -mtime +90 -delete
echo "Backup completed: $DATE"
Make executable and add to cron:
sudo chmod +x /root/backup-configs.sh
# Run daily at 2 AM
sudo crontab -e
# Add: 0 2 * * * /root/backup-configs.sh >> /var/log/config-backup.log 2>&1
9.4 Security Updates
# Manual update process (if automatic updates disabled)
sudo apt update # Update package lists
sudo apt list --upgradable # See what needs updating
sudo apt upgrade -y # Upgrade all packages
sudo apt dist-upgrade -y # Upgrade with dependency changes
sudo apt autoremove -y # Remove unnecessary packages
sudo apt autoclean # Clean package cache
# Check if reboot needed
ls -la /var/run/reboot-required
cat /var/run/reboot-required.pkgs
# Reboot if kernel was updated
sudo reboot
9.5 User Management
# List all users with login shells
grep -v '/nologin\|/false' /etc/passwd
# List users in sudo group
getent group sudo
# Check last login times
lastlog
# Check currently logged in users
who
w
# Check user password status
sudo passwd -S username
# Lock a user account (disable login)
sudo usermod -L username
# Unlock a user account
sudo usermod -U username
# Delete a user
sudo deluser --remove-home username
# Force password change on next login
sudo chage -d 0 username
Summary of Security Layers
This hardening guide implements defense in depth with multiple security layers:
- Authentication Layer:
- SSH key-only authentication (no passwords)
- Strong ED25519 keys
- Separate admin user (not root)
- Sudo password requirement
- Network Layer:
- UFW firewall (whitelist approach)
- SSH restricted to specific IPs
- Only essential ports open
- Fail2ban for brute force protection
- System Layer:
- Kernel hardening (sysctl)
- Automatic security updates
- Regular security audits
- Log monitoring
- Access Control:
- Limited user accounts
- Principle of least privilege
- AllowUsers SSH restriction
- No root SSH access
- Monitoring & Response:
- Fail2ban automated bans
- Log file monitoring
- Update notifications
- Regular security checks
Quick Reference Commands
# SSH
sudo systemctl restart sshd
sudo sshd -t
sudo tail -f /var/log/auth.log
# Firewall
sudo ufw status
sudo ufw allow 80/tcp
sudo ufw delete allow 80/tcp
sudo ufw reload
# Fail2ban
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip 1.2.3.4
sudo tail -f /var/log/fail2ban.log
# Updates
sudo apt update && sudo apt upgrade -y
sudo unattended-upgrades --dry-run
cat /var/run/reboot-required
# Users
sudo adduser username
sudo usermod -aG sudo username
sudo deluser --remove-home username
# Monitoring
sudo netstat -tlnp
sudo ss -tlnp
who
w
lastlog
Additional Resources
- SSH Hardening: https://www.ssh.com/academy/ssh/sshd_config
- UFW Documentation: https://help.ubuntu.com/community/UFW
- Fail2ban Wiki: https://www.fail2ban.org/
- Ubuntu Security: https://ubuntu.com/security
- CIS Benchmarks: https://www.cisecurity.org/cis-benchmarks/
- Lynis Security Auditing: https://cisofy.com/lynis/
Leave a Reply