Servers connected to the internet are active targets. Within minutes after a new instance goes online, automated bots will begin scanning for open ports and attempting various common credential combinations. This is not a reason to panic, but rather an urgency to act before threats materialize.
Server hardening is a systematic process to reduce the attack surface, disable unnecessary services, tighten default configurations, and add layers of monitoring and access control. Server hardening is not a one-time task, but rather an ongoing practice.
This guide is compiled based on experience and best practices from the global Linux sysadmin community, intended for administrators managing Ubuntu Server in production environments. Each step includes executable commands along with testing methods, so you not only perform configurations but also verify that those configurations work properly.
⚠️ Important Before Starting
Backup your data before applying any changes to a running server. Ensure you have alternative access (VPS console, out-of-band access) to anticipate lockout risks when configuring SSH or firewall.
1. System Update & Patching
The first and most fundamental step in hardening is ensuring all system packages are updated to their latest versions. The majority of real-world exploits leverage publicly known vulnerabilities for which patches are already available. This means unpatched systems are vulnerable systems.
1.1 Initial Manual Update
Immediately after a new server is provisioned, run a full system update:
# Update package lists from repositories
sudo apt update
# Upgrade all packages that have new versions available
sudo apt upgrade -y
# Remove old packages that are no longer needed
sudo apt autoremove -y
sudo apt autocleanIf the kernel was updated, reboot the server to apply the changes:
sudo reboot1.2 Enabling Automatic Security Updates
For production environments, performing manual updates daily is not realistic. The unattended-upgrades package allows the system to apply security patches automatically without manual intervention.
# Install the unattended-upgrades package
sudo apt install -y unattended-upgrades
# Interactive configuration (select Yes to enable)
sudo dpkg-reconfigure --priority=low unattended-upgradesVerify the resulting configuration:
cat /etc/apt/apt.conf.d/20auto-upgradesExpected output:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";Edit /etc/apt/apt.conf.d/50unattended-upgrades to customize configuration, such as enabling email notifications or automatic reboot:
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades// Enable automatic reboot if required (optional, note the reboot time)
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
// Send email notifications (fill with administrator email address)
Unattended-Upgrade::Mail "[email protected]";Testing: Verify Automatic Updates
# Simulate a dry-run without installing anything
sudo unattended-upgrade --dry-run --debug
# Check automatic upgrades log
cat /var/log/unattended-upgrades/unattended-upgrades.log2. User Management & Sudo Privileges
Running all operations as root is a very poor practice. A single command mistake can permanently delete system files. Additionally, if a root session is compromised by an attacker, they will have unlimited access to the entire system. The solution: create a non-root user with limited and audited sudo privileges.
2.1 Creating a New User
Avoid common usernames like admin, user, or sysadmin; these names are the first targets in dictionary attacks.
# Create a new user with a home directory
sudo adduser namauser
# Follow prompts to set password and user information2.2 Granting Sudo Privileges
# Add user to sudo group
sudo usermod -aG sudo namauser
# Verify group membership
groups namauser2.3 Disable Root Login
After a non-root user with sudo access is ready, lock the root account to prevent direct login. This step will be continued in SSH configuration, but as an additional security layer, also permanently lock the root password:
# Lock root account password (does not delete the account, only prevents password login)
sudo passwd -l root2.4 Verify No Non-Root Users with UID 0
Only the root account should have UID 0. The presence of other users with UID 0 indicates a compromise or misconfiguration.
# Display all accounts with UID 0
awk -F: '($3 == "0") {print}' /etc/passwdValid output should only show:
root:x:0:0:root:/root:/bin/bash2.5 Locking Unused System Accounts
# Display accounts that have active login shells
grep -vE '(false|nologin)$' /etc/passwd
# Lock system accounts that don't need login
sudo usermod -L daemon
sudo usermod -L bin
sudo usermod -L sys2.6 Restricting sudo Usage
For stricter environments, you can restrict specific commands that users are allowed to run using visudo:
# Edit sudoers safely using visudo
sudo visudoAs an example restriction, user deploy is only allowed to restart the nginx service:
deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart nginxTesting: Verify Sudo Privileges
# Login as the new user and check sudo access
su - namauser
sudo whoami
# Expected output: root
# Try to login as root (should fail if password is locked)
su - root
# Expected output: su: Authentication failure3. SSH (Secure Shell) Hardening
SSH is the primary entry point to your server. For attackers, this service is the number one target. Default SSH configuration is functional enough, but far from optimal for production environments. SSH hardening includes using key-based authentication, disabling root login via SSH, restricting allowed users, and tightening cryptographic parameters.
3.1 Generate SSH Key Pair
SSH key-based authentication is far more secure than passwords. Keys with thousands of bits of entropy cannot be brute-forced within a reasonable timeframe. Run the following command on your local machine (not on the server):
# On local machine — generate Ed25519 key pair (more modern and secure than RSA)
ssh-keygen -t ed25519 -C "namauser@production-server"
# Copy public key to server
ssh-copy-id -i ~/.ssh/id_ed25519.pub namauser@ip-serverOr manually on the server, add the public key to authorized_keys:
# On server, as namauser
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
# Paste contents of id_ed25519.pub file from local machine
chmod 600 ~/.ssh/authorized_keys🚨 Critical
Ensure you can login using SSH key BEFORE disabling password authentication. Otherwise, you will experience a server lockout.
3.2 Hardening SSHD Configuration
Edit the SSH daemon configuration file:
sudo nano /etc/ssh/sshd_configApply the following hardening configuration:
# ── Authentication ──────────────────────────────────────
# Disable direct root login via SSH
PermitRootLogin no
# Disable password authentication — require SSH key
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
# Use SSH Protocol 2 only (Protocol 1 is deprecated)
Protocol 2
# Limit number of authentication attempts
MaxAuthTries 3
MaxSessions 3
# ── Port & Interface ────────────────────────────────────
# Optional: change default port 22 to reduce bot scanning
# Ensure UFW allows the new port before changing this
Port 22
# ── Access Control ──────────────────────────────────────
# Whitelist users allowed to login via SSH
AllowUsers namauser
# Or allow from specific IP only
# AllowUsers [email protected]
# Disable .rhosts and hosts.equiv
IgnoreRhosts yes
HostbasedAuthentication no
# ── Session ─────────────────────────────────────────────
# Disconnect idle connections after 5 minutes (300 seconds)
ClientAliveInterval 300
ClientAliveCountMax 2
# Disable X11 forwarding if not needed
X11Forwarding no
# ── Cryptography (Modern Ciphers) ───────────────────────
# Use strong ciphers and key algorithms
Ciphers [email protected],[email protected],[email protected]
MACs [email protected],[email protected]
KexAlgorithms curve25519-sha256,[email protected],diffie-hellman-group16-sha512
# Disable unnecessary authentication methods
KerberosAuthentication no
GSSAPIAuthentication noTest the configuration, then restart the SSH daemon:
# Validate configuration syntax without restarting
sudo sshd -t
# If no errors, restart SSH
sudo systemctl restart sshTesting: Verify SSH Hardening
# Test login as root (should be denied)
ssh root@ip-server
# Expected: Permission denied (publickey)
# Test login with password (should be denied)
ssh -o PreferredAuthentications=password namauser@ip-server
# Expected: Permission denied (publickey)
# Test login with SSH key (should succeed)
ssh -i ~/.ssh/id_ed25519 namauser@ip-server
# Expected: Login successful
# Verify active SSH configuration
sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication|maxauthtries|allowusers'4. Firewall Configuration with UFW
UFW (Uncomplicated Firewall) is a more user-friendly frontend for iptables. Its basic principle: deny all inbound connections by default, and allow only truly necessary connections. Every open port is a potential attack vector.
🚨 Critical Order
Always allow SSH port BEFORE enabling the firewall. Otherwise, you will immediately experience a server lockout.
4.1 Install and Configure UFW
# Install UFW (usually already available on Ubuntu)
sudo apt install -y ufw
# Set default policies: deny all incoming, allow all outgoing
sudo ufw default deny incoming
sudo ufw default allow outgoing
# IMPORTANT: Allow SSH first before enabling firewall
sudo ufw allow 22/tcp comment 'SSH'
# Enable firewall
sudo ufw enable
# Check status
sudo ufw status verbose4.2 Allowing Additional Service Ports
Only open ports that are actually needed by services you run:
# Web server
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
# If using custom SSH port (e.g., 2222)
sudo ufw allow 2222/tcp comment 'SSH Custom Port'
# Allow from specific IP only (more secure)
sudo ufw allow from 203.0.113.10 to any port 22 comment 'SSH from office IP'
# Block specific suspicious IPs
sudo ufw deny from 185.220.101.0/244.3 Managing UFW Rules
# View rules with sequence numbers
sudo ufw status numbered
# Delete a rule by number
sudo ufw delete 3
# Reload firewall after changes
sudo ufw reload💡 Production Tip
For servers accessed only from office IPs or specific static IPs, consider restricting SSH access to only those addresses using the command ufw allow from OFFICE_IP to any port 22. This step will drastically reduce exposure to internet scanning.
Testing: Verify UFW Firewall
# Display full status with rules
sudo ufw status verbose
# Verify from outside using nmap (from another machine)
nmap -sV ip-server
# Only ports allowed by UFW should appear as 'open'
# Check UFW logs
sudo tail -f /var/log/ufw.log5. Brute Force Protection with Fail2Ban
Even with password authentication disabled, bots will still continuously attempt SSH connections, consuming resources and filling log files. Fail2Ban is an intrusion prevention system that monitors log files, detects failed login patterns, and automatically blocks suspicious IPs using iptables rules.
5.1 Installing Fail2Ban
sudo apt install -y fail2ban
# Enable and start the service
sudo systemctl enable fail2ban
sudo systemctl start fail2ban5.2 Configuring jail.local
Never edit jail.conf directly, as this file will be overwritten during updates. Instead, create a jail.local file:
# Copy configuration template
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
# Edit local configuration
sudo nano /etc/fail2ban/jail.localApply the following configuration to the [DEFAULT] and [sshd] sections:
[DEFAULT]
# Ban duration in seconds (3600 = 1 hour)
bantime = 3600
# Monitoring time window (600 seconds = 10 minutes)
findtime = 600
# Number of failed attempts before ban
maxretry = 5
# Log monitoring backend (systemd for modern Ubuntu)
backend = systemd
# IPs that will never be banned (local IPs, team IPs)
ignoreip = 127.0.0.1/8 ::1 203.0.113.10
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600Restart Fail2Ban after configuration is applied:
sudo systemctl restart fail2ban5.3 Managing Fail2Ban
# View status of all active jails
sudo fail2ban-client status
# View IPs currently banned for SSH
sudo fail2ban-client status sshd
# Manually unban a specific IP
sudo fail2ban-client set sshd unbanip 1.2.3.4
# Manually ban an IP
sudo fail2ban-client set sshd banip 1.2.3.4Testing: Verify Fail2Ban
# Check service status
sudo systemctl status fail2ban
# Check list of active jails
sudo fail2ban-client status
# Expected output: Number of jail: 1 — Jail list: sshd
# Monitor Fail2Ban logs in real-time
sudo tail -f /var/log/fail2ban.log
# Simulate ban (optional, perform carefully in production)
# Attempt SSH login with wrong password maxretry times from a different remote IP
# Then verify if that IP address appears in the ban list:
sudo fail2ban-client status sshd6. Securing Shared Memory
Shared memory (/run/shm) is an efficient mechanism for sharing data between processes (inter-process communication). However, because it is mounted as read/write by default, this area can be exploited by malicious processes to escalate privileges or execute malicious code. By remounting with noexec and nosuid options, we can eliminate this potential threat.
6.1 Configuring /etc/fstab
# Edit fstab
sudo nano /etc/fstabAdd the following line at the very bottom:
tmpfs /run/shm tmpfs defaults,noexec,nosuid 0 0Apply the changes without rebooting:
sudo mount -o remount /run/shmTesting: Verify Shared Memory
# Verify active mount options
mount | grep /run/shm
# Expected: tmpfs on /run/shm type tmpfs (rw,nosuid,noexec,...)
# Try to execute a binary from shm (should fail)
cp /bin/ls /run/shm/test
/run/shm/test
# Expected: bash: /run/shm/test: Permission denied
rm /run/shm/test7. Kernel Hardening via Sysctl
Kernel parameters in /proc/sys can be configured to enhance network and system resilience against various attacks, such as IP spoofing, SYN flood, ICMP redirect, and other threats. These configurations are made persistent through sysctl.d.
7.1 Create Hardening Configuration File
sudo nano /etc/sysctl.d/99-hardening.confEnter the following configuration:
## ── Network Security ─────────────────────────────────────
# IP Spoofing Protection (Reverse Path Filtering)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignore ICMP broadcast (prevents Smurf attack)
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Disable source routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# Disable ICMP redirect acceptance
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# Disable ICMP redirect sending
net.ipv4.conf.all.send_redirects = 0
# Enable SYN flood protection (SYN cookies)
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 5
# Disable IP forwarding (enable only if this server is a router/gateway)
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0
# Log suspicious packets (martian packets)
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
## ── Kernel Protection ────────────────────────────────────
# Restrict access to dmesg (kernel logs)
kernel.dmesg_restrict = 1
# Restrict access to kernel pointers via /proc
kernel.kptr_restrict = 2
# Disable magic SysRq key
kernel.sysrq = 0
# Restrict ptrace to parent-child processes only
kernel.yama.ptrace_scope = 1Apply configuration without rebooting:
sudo sysctl -p /etc/sysctl.d/99-hardening.confTesting: Verify Kernel Parameters
# Verify applied parameters
sudo sysctl net.ipv4.tcp_syncookies
# Expected: net.ipv4.tcp_syncookies = 1
sudo sysctl net.ipv4.conf.all.rp_filter
# Expected: net.ipv4.conf.all.rp_filter = 1
sudo sysctl kernel.dmesg_restrict
# Expected: kernel.dmesg_restrict = 1
# View all active hardening parameters
sudo sysctl -a 2>/dev/null | grep -E 'rp_filter|syncookies|accept_redirects|log_martians'8. Removing Unnecessary Services
Every running service is a potential attack surface. Legacy services like telnet, FTP, and rsh transmit data, including passwords, in plaintext. There is no logical reason to allow these services to remain installed in a production environment.
8.1 Remove Unsafe Legacy Services
# Remove outdated and unsafe services
sudo apt --purge remove -y \
xinetd \
nis \
yp-tools \
tftpd \
atftpd \
tftpd-hpa \
telnetd \
rsh-server \
rsh-redone-server
# Clean up dependencies no longer needed
sudo apt autoremove -y8.2 Audit Running Services
# View all listening ports
sudo ss -tulpn
# View all active services
systemctl list-units --type=service --state=running
# Disable unneeded services (example: avahi-daemon)
sudo systemctl disable avahi-daemon
sudo systemctl stop avahi-daemon8.3 Disable IPv6 if Not Used
If your infrastructure does not use IPv6, disabling it helps reduce the attack surface. Add the following configuration to /etc/sysctl.d/99-hardening.conf:
# Disable IPv6 completely (if not used)
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1Testing: Audit Open Ports
# Only ports you allow in UFW should be listening
sudo ss -tulpn | grep LISTEN
# Confirm telnet cannot be used
telnet localhost 22
# Expected: command not found or connection refused9. Automatic Security Audit with Lynis
After implementing all the steps above, use Lynis, an open-source security auditing tool, to thoroughly verify configurations and receive additional recommendations specific to your system. Lynis evaluates hundreds of security aspects and provides a hardening index score.
9.1 Install and Run Lynis
# Install Lynis
sudo apt install -y lynis
# Run a full system audit
sudo lynis audit systemLynis will generate a comprehensive report. Pay attention to the Warnings and Suggestions sections, and prioritize items labeled [HIGH]:
9.2 Reading Audit Results
# View full audit log
sudo cat /var/log/lynis.log
# View only warnings and suggestions
sudo grep -E "WARNING|SUGGESTION" /var/log/lynis.log
# View hardening index score
sudo lynis audit system --quick 2>/dev/null | grep "Hardening index"💡 Tip
Run Lynis periodically (e.g., weekly via cron) and compare hardening index scores over time. An increasing score indicates improved security posture, while a decreasing score signals configuration changes that need immediate evaluation.
Testing: Interpreting Lynis Results
# View audit summary
sudo lynis show summary
# Minimum target for production server: Hardening Index >= 70
# Scores above 80 indicate solid configurationFinal Security Checklist
Use this checklist to verify that all hardening steps have been applied before the server enters production:
- [ ] All system packages updated to latest versions (apt upgrade)
- [ ] Automatic security updates (unattended-upgrades) enabled and configured
- [ ] Non-root user with sudo access created; username is not common or generic
- [ ] Root login via SSH disabled (PermitRootLogin no)
- [ ] No non-root users with UID 0 in /etc/passwd
- [ ] SSH key pair (Ed25519) generated and configured
- [ ] Password authentication on SSH disabled (PasswordAuthentication no)
- [ ] AllowUsers parameter in sshd_config configured to whitelist specific users
- [ ] MaxAuthTries and ClientAliveInterval parameters in SSH configured
- [ ] SSH cipher suites using modern algorithms
- [ ] UFW firewall enabled with default deny incoming policy
- [ ] Only necessary ports open in UFW configuration
- [ ] Fail2Ban installed and active, with sshd jail configured
- [ ] Shared memory (/run/shm) mounted with noexec and nosuid options
- [ ] Kernel parameters in sysctl hardened (SYN cookies, rp_filter, etc.)
- [ ] Legacy services (telnet, rsh, plaintext FTP) removed from system
- [ ] Unnecessary ports confirmed not in listening state (verified via ss -tulpn)
- [ ] Lynis audit executed and [HIGH] labeled recommendations addressed
- [ ] Configuration backups created before and after hardening
Hardening Steps Summary
| # | Step | Tool / File | Priority |
|---|---|---|---|
| 1 | Update & Patching | apt, unattended-upgrades | 🔴 Critical |
| 2 | User Management & Sudo | adduser, visudo | 🔴 Critical |
| 3 | SSH Hardening | /etc/ssh/sshd_config | 🔴 Critical |
| 4 | Firewall Configuration | ufw | 🔴 Critical |
| 5 | Brute Force Protection | fail2ban | 🟠 High |
| 6 | Securing Shared Memory | /etc/fstab | 🟠 High |
| 7 | Kernel Hardening | /etc/sysctl.d/99-hardening.conf | 🟠 High |
| 8 | Remove Unnecessary Services | apt remove, systemctl | 🟡 Medium |
| 9 | Security Audit | lynis | 🟡 Medium |
Conclusion
Server hardening is not a one-time action; it is an ongoing mindset. Threats evolve, new vulnerabilities are discovered, and configurations can change over time due to software updates or new operational requirements. After completing all steps in this guide, you have significantly reduced the attack surface of your Ubuntu Server: remote access is tightly controlled via SSH keys, the firewall blocks unauthorized traffic, Fail2Ban protects against brute force, and the kernel is configured to reject common network exploitation techniques. Solid security is not about one perfect defense, but rather about defense in depth: multiple layers of protection that complement each other, so that even if one layer is breached, the next layer still stands.
💡 Remember
Document every configuration change you make. Run Lynis audits periodically. Monitor server logs regularly. And always test that the security configurations you implement actually work before the server enters production.




