Ubuntu Server Hardening Guide for Production Environments

Ubuntu Server Hardening Guide for Production Environments

Bitnesia Linux Apr 2, 2026 351 ID

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 autoclean

If the kernel was updated, reboot the server to apply the changes:

sudo reboot

1.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-upgrades

Verify the resulting configuration:

cat /etc/apt/apt.conf.d/20auto-upgrades

Expected 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.log

2. 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 information

2.2 Granting Sudo Privileges

# Add user to sudo group
sudo usermod -aG sudo namauser

# Verify group membership
groups namauser

2.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 root

2.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/passwd

Valid output should only show:

root:x:0:0:root:/root:/bin/bash

2.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 sys

2.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 visudo

As an example restriction, user deploy is only allowed to restart the nginx service:

deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart nginx

Testing: 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 failure

3. 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-server

Or 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_config

Apply 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            no

Test the configuration, then restart the SSH daemon:

# Validate configuration syntax without restarting
sudo sshd -t

# If no errors, restart SSH
sudo systemctl restart ssh

Testing: 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 verbose

4.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/24

4.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.log

5. 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 fail2ban

5.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.local

Apply 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  = 3600

Restart Fail2Ban after configuration is applied:

sudo systemctl restart fail2ban

5.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.4

Testing: 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 sshd

6. 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/fstab

Add the following line at the very bottom:

tmpfs /run/shm tmpfs defaults,noexec,nosuid 0 0

Apply the changes without rebooting:

sudo mount -o remount /run/shm

Testing: 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/test

7. 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.conf

Enter 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                  = 1

Apply configuration without rebooting:

sudo sysctl -p /etc/sysctl.d/99-hardening.conf

Testing: 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 -y

8.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-daemon

8.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      = 1

Testing: 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 refused

9. 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 system

Lynis 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 configuration

Final 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

#StepTool / FilePriority
1Update & Patchingapt, unattended-upgrades🔴 Critical
2User Management & Sudoadduser, visudo🔴 Critical
3SSH Hardening/etc/ssh/sshd_config🔴 Critical
4Firewall Configurationufw🔴 Critical
5Brute Force Protectionfail2ban🟠 High
6Securing Shared Memory/etc/fstab🟠 High
7Kernel Hardening/etc/sysctl.d/99-hardening.conf🟠 High
8Remove Unnecessary Servicesapt remove, systemctl🟡 Medium
9Security Auditlynis🟡 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.

Help me create more! Your donations go directly toward better equipment and research for future tutorials.

Support future guides

Related Posts