A step-by-step practical guide to locking down SSH, configuring firewalls, enforcing least privilege, hardening the kernel, and setting up audit trails on your production systems.
The moment you spin up a fresh virtual machine on AWS, DigitalOcean, Hetzner, or a bare-metal server in a datacenter, the clock starts ticking.
Within minutes of your public IP address going live, automated bots and port scanners around the globe will begin probing your server. They will scan port 22, attempt thousands of default password combinations, search for open web ports, and test for known vulnerabilities.
If your server runs on default settings, it is only a matter of time before someone finds a crack.
A default Linux installation (whether Ubuntu, Debian, Rocky Linux, or AlmaLinux) is built for convenience, not fortress-grade security. Default configurations often leave password authentication enabled, root logins permitted, unused network ports exposed, and kernel settings tuned for general desktop workloads rather than high-security production environments.
Security is not a single tool you install. It is a process of defense-in-depth, building multiple overlapping layers of protection around your system. If an attacker bypasses one layer, the next layer stops them in their tracks.
Here is a practical, battle-tested Linux security checklist you can use to harden your production servers from day one.
1. SSH Hardening: Locking Down the Front Door
Secure Shell (SSH) is your primary administrative interface, which also makes it the number one target for automated brute-force attacks. Securing SSH is the first and most critical step in server hardening.
All SSH server configurations live in /etc/ssh/sshd_config or modular files inside /etc/ssh/sshd_config.d/.
Step 1: Disable Root Login and Password Authentication
Never allow direct logins to the root account over SSH, and never allow plain text passwords. Always require cryptographic SSH key pairs (preferably Ed25519 keys).
Generate a secure Ed25519 key on your local machine if you have not already:
$ ssh-keygen -t ed25519 -C "admin@yourcompany.com"
Copy your public key to the remote server:
$ ssh-copy-id -i ~/.ssh/id_ed25519.pub asep@203.0.113.10
Now, edit the SSH daemon configuration on the server:
$ sudo nano /etc/ssh/sshd_config.d/99-hardening.conf
Add the following hardening directives:
# Disable root login over SSH
PermitRootLogin no
# Enforce public key authentication only
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
# Disable legacy authentication methods
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
GSSAPIAuthentication no
# Limit authentication attempts per connection
MaxAuthTries 3
MaxSessions 4
# Terminate idle SSH sessions after 10 minutes of inactivity
ClientAliveInterval 300
ClientAliveCountMax 2
# Disable risky forwarding features
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
# Restrict SSH access to specific users or groups
AllowGroups sudo sysadmin
Step 2: Use Modern Ciphers and Key Exchange Algorithms
Legacy SSH implementations may still negotiate outdated ciphers like 3DES, blowfish, or SHA-1 hashes. Restrict your SSH daemon to modern, secure cryptographic algorithms:
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
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
Step 3: Test and Apply Configuration
Before restarting the SSH daemon, always test the configuration syntax. A single typo in sshd_config can lock you out of a remote server permanently:
$ sudo sshd -t
If the command returns no output, your syntax is valid. Now restart the SSH service:
$ sudo systemctl restart sshd
Safety Tip: Do not close your current active terminal session after restarting SSH. Open a new terminal window and test logging in with your SSH key to confirm you can still connect before disconnecting your existing session.
2. User & Access Control: Applying the Principle of Least Privilege
Every user and service on your server should operate with the minimum level of privileges necessary to perform its job. If a service account is compromised, least privilege stops the attacker from taking over the entire host.
Lock Default and Unused Accounts
Linux distributions ship with dozens of system accounts (like games, news, ftp, lp). Verify that these system accounts have their login shells set to /usr/sbin/nologin or /bin/false, and lock unused accounts:
$ sudo passwd -l root
$ sudo usermod -s /usr/sbin/nologin games
Configure Modular Sudo Access
Never edit /etc/sudoers directly with a normal text editor. Always use visudo, which checks syntax before saving to prevent corrupting your superuser configuration.
Create dedicated sudo rules inside /etc/sudoers.d/:
$ sudo visudo -f /etc/sudoers.d/99-sysadmin
Add granular permissions instead of handing out blanket ALL access where possible. For instance, allowing a deploy user to only restart a specific web service:
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl reload nginx
Ensure the permissions on any file in /etc/sudoers.d/ are strictly set to 0440:
$ sudo chmod 0440 /etc/sudoers.d/99-sysadmin
Run Applications Under Dedicated Non-Root Users
Never run web applications, Node.js backends, Python scripts, or Docker containers as root. Create isolated system users without home directories or interactive login shells:
$ sudo useradd -r -s /usr/sbin/nologin -d /var/www/my-app appuser
When building self-hosted network applications, restricting application permissions and enforcing strict path isolation is a fundamental design rule. For example, in projects like AiroShare, strict path isolation guards against directory traversal attacks, ensuring that even if an HTTP request attempts to access ../../etc/shadow, the application layer and underlying non-root user permissions reject the request immediately.
3. Network Security & Firewall Configuration
A production server should never expose internal ports to the public internet. If a database, cache, or internal metrics exporter does not need public access, bind it strictly to 127.0.0.1 or a private VPN interface (like WireGuard or Tailscale).
Set Up a Default-Deny Firewall with UFW
On Ubuntu and Debian systems, Uncomplicated Firewall (UFW) provides a simple, dependable interface for managing iptables and nftables rules.
Step 1: Set the default policy to deny all incoming traffic and allow outgoing traffic:
$ sudo ufw default deny incoming
$ sudo ufw default allow outgoing
Step 2: Allow your SSH port (make sure you do this before enabling the firewall):
$ sudo ufw allow 22/tcp comment "SSH Management"
Step 3: Allow only required public application traffic (e.g., HTTP and HTTPS):
$ sudo ufw allow 80/tcp comment "HTTP Web Traffic"
$ sudo ufw allow 443/tcp comment "HTTPS Web Traffic"
Step 4: Enable the firewall and check status:
$ sudo ufw enable
$ sudo ufw status verbose
Output:
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere # SSH Management
80/tcp ALLOW IN Anywhere # HTTP Web Traffic
443/tcp ALLOW IN Anywhere # HTTPS Web Traffic
22/tcp (v6) ALLOW IN Anywhere (v6) # SSH Management
80/tcp (v6) ALLOW IN Anywhere (v6) # HTTP Web Traffic
443/tcp (v6) ALLOW IN Anywhere (v6) # HTTPS Web Traffic
Audit Open Sockets and Listening Ports
Check what services are currently listening on network sockets:
$ sudo ss -tulnp
Look closely at the Local Address:Port column:
-
0.0.0.0:*or[::]:*means the service is listening on all network interfaces, including public IPs. -
127.0.0.1:*or[::1]:*means the service is bound strictly to localhost and unreachable from outside.
If you see Redis (6379), PostgreSQL (5432), or MySQL (3306) bound to 0.0.0.0, edit their respective configuration files immediately and set their bind address to 127.0.0.1.
4. Automated Intrusion Prevention with Fail2ban
Even with password authentication disabled, automated bots will flood your SSH port with connection requests, filling up your authentication logs and consuming system resources.
Fail2ban monitors system log files (like /var/log/auth.log or systemd-journald) for repeated failed login attempts and dynamically updates firewall rules to ban the offending IP addresses.
Install and Configure Fail2ban
$ sudo apt update && sudo apt install fail2ban -y
Copy the default configuration to a local override file:
$ sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
$ sudo nano /etc/fail2ban/jail.local
Configure your global ban policies and enable the SSH jail:
[DEFAULT]
# Ban hosts for 1 hour after failed attempts
bantime = 1h
# Window of time to track failures
findtime = 10m
# Number of failures before triggering a ban
maxretry = 4
# Ignore trusted IP addresses (like your office VPN or home static IP)
ignoreip = 127.0.0.1/8 ::1 198.51.100.45
[sshd]
enabled = true
port = ssh
filter = sshd
maxretry = 3
bantime = 24h
Start and enable Fail2ban:
$ sudo systemctl enable --now fail2ban
Check the status of your SSH jail to see active bans:
$ sudo fail2ban-client status sshd
Output:
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 48
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 5
|- Total banned: 14
`- Banned IP list: 185.220.101.5 194.26.29.112 45.154.255.88 ...
If you ever accidentally ban yourself, unban your IP from another session with:
$ sudo fail2ban-client set sshd unbanip 203.0.113.50
5. Package Management & Automatic Security Patching
Unpatched software vulnerabilities are one of the most common vectors for server compromises. Production servers should receive critical security patches automatically without requiring manual sysadmin intervention.
Configure Unattended Upgrades (Debian / Ubuntu)
Install the unattended upgrades package:
$ sudo apt install unattended-upgrades update-notifier-common -y
Enable automatic upgrades:
$ sudo dpkg-reconfigure --priority=low unattended-upgrades
Review /etc/apt/apt.conf.d/50unattended-upgrades to ensure security repositories are included:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Package-Blacklist {
// Add packages you want to hold back from automatic updates
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::InstallOnShutdown "false";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
Test unattended upgrades in dry-run mode to confirm it works properly:
$ sudo unattended-upgrades --dry-run --debug
Remove Compilers and Unnecessary Utilities
Production web servers do not need software compilers, debuggers, or legacy networking tools installed. Attackers who gain a low-privilege shell often look for gcc, g++, make, netcat, or telnet to compile kernel exploits or establish reverse shells.
Remove tools that are not required for your production runtime:
$ sudo apt purge -y gcc g++ make telnet rsh-client
$ sudo apt autoremove --purge -y
6. Filesystem Hardening & SUID Permission Audits
Hardening the filesystem prevents attackers from executing downloaded malware from temporary directories or abusing setuid binaries for privilege escalation.
Secure Shared Memory and Temporary Directories
Attackers frequently download and execute exploit payloads in /tmp, /var/tmp, and /dev/shm because these directories are world-writable by default.
You can restrict these mount points by adding noexec, nosuid, and nodev mount options in /etc/fstab:
-
noexec: Prevents binaries and scripts from executing directly from the filesystem. -
nosuid: Blocks the SUID and SGID bits from granting elevated privileges. -
nodev: Prevents character or block device files from being created.
Add the following entries to /etc/fstab:
# Hardening temporary filesystems
tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev 0 0
tmpfs /var/tmp tmpfs defaults,noexec,nosuid,nodev 0 0
tmpfs /dev/shm tmpfs defaults,noexec,nosuid,nodev 0 0
Remount the filesystems to apply the changes:
$ sudo mount -o remount /tmp
$ sudo mount -o remount /dev/shm
Audit SUID and SGID Binaries
SUID (Set User ID) binaries run with the file owner's permissions (usually root) regardless of who executes them. While tools like /usr/bin/sudo and /usr/bin/passwd require SUID, unnecessary SUID binaries introduce dangerous privilege escalation paths.
Search for all SUID and SGID files across your system:
$ sudo find / -perm -4000 -o -perm -2000 -type f 2>/dev/null
Review the list. If you find legacy tools that regular users should never run (such as chfn, chsh, pkexec, or mount), remove their SUID bit:
$ sudo chmod u-s /usr/bin/chfn
$ sudo chmod u-s /usr/bin/chsh
Find World-Writable Files
World-writable files can be modified by any local user. Run a scan to identify any world-writable files outside of /tmp:
$ sudo find / -xdev -type f -perm -0002 -not -path "/proc/*" -not -path "/sys/*"
If you discover configuration files or executable scripts with 0777 or 0666 permissions, change them back to secure ownership and modes:
$ sudo chmod 0640 /path/to/insecure/file
$ sudo chown root:root /path/to/insecure/file
7. Mandatory Access Control: AppArmor and SELinux
Standard Linux permissions (Discretionary Access Control) only check user ownership and permission bits (rwxrwxrwx). If your Nginx web server runs as www-data and a remote exploit gives the attacker command execution as www-data, the attacker can read any file that www-data has read access to across the entire disk.
Mandatory Access Control (MAC) systems like AppArmor (Ubuntu/Debian) and SELinux (RHEL/Rocky Linux) confine processes strictly to the specific files, sockets, and capabilities they need, regardless of user privileges.
Enforce AppArmor on Ubuntu and Debian
Check the status of loaded AppArmor profiles:
$ sudo aa-status
Output:
apparmor module is loaded.
42 profiles are loaded.
38 profiles are in enforce mode.
/usr/sbin/nginx
/usr/sbin/named
/usr/bin/man
...
4 profiles are in complain mode.
0 processes are unconfined but have a profile defined.
If a profile is in "complain" mode, put it into active "enforce" mode:
$ sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
When AppArmor enforces a profile on Nginx, even if a remote code execution vulnerability is discovered in your web app, AppArmor will block the web server process from reading /etc/passwd, launching /bin/bash, or modifying files outside its defined root directory.
Verify SELinux on RHEL and Rocky Linux
On Red Hat family distributions, ensure SELinux is set to enforcing mode:
$ sestatus
Output:
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
Mode from config file: enforcing
Never set SELinux to disabled on a production server. If you run into permission denials, inspect the audit logs with ausearch -m avc -ts recent and generate targeted policy modules rather than turning off system-wide protection.
8. Kernel Hardening & Sysctl Parameters
The Linux kernel exposes hundreds of tunable parameters through the /proc/sys/ interface. You can set persistent kernel security parameters in /etc/sysctl.d/99-security.conf.
Create a dedicated hardening configuration file:
$ sudo nano /etc/sysctl.d/99-security.conf
Add the following kernel security tuning parameters:
# Enable Address Space Layout Randomization (ASLR)
kernel.randomize_va_space = 2
# Restrict access to kernel logs (dmesg) to root only
kernel.dmesg_restrict = 1
# Restrict ptrace process inspection to parent processes only
kernel.yama.ptrace_scope = 1
# Disable core dumps for setuid binaries to prevent memory leak of secrets
fs.suid_dumpable = 0
# Protect against SYN flood attacks (TCP SYN cookies)
net.ipv4.tcp_syncookies = 1
# Ignore ICMP echo broadcast requests (prevents Smurf attacks)
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Ignore bogus ICMP error responses
net.ipv4.icmp_ignore_bogus_error_responses = 1
# Do not accept ICMP redirects (prevents MITM route alterations)
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
# Do not send ICMP redirects
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
# Enable Reverse Path Filtering (prevents IP spoofing)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Disable IP source routing
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
# Log suspicious packets (martians)
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
Apply the sysctl parameters immediately without rebooting:
$ sudo sysctl --system
Disable Unused Kernel Filesystem Modules
Linux supports legacy filesystems and esoteric network protocols (like cramfs, squashfs, dccp, sctp). If your server does not need them, blacklist their kernel modules in /etc/modprobe.d/blacklist.conf:
install cramfs /bin/true
install freevxfs /bin/true
install jffs2 /bin/true
install hfs /bin/true
install hfsplus /bin/true
install udf /bin/true
install dccp /bin/true
install sctp /bin/true
install rds /bin/true
install tipc /bin/true
9. Audit Logging & System Integrity Monitoring
If an incident occurs, your logs are the only record of what happened, how the intruder got in, and what files they modified. Without proper auditing and off-host log shipping, an attacker can erase /var/log/ and cover their tracks completely.
Install and Configure auditd
The Linux Audit daemon (auditd) logs security-relevant events directly from the kernel.
Install auditd:
$ sudo apt install auditd audispd-plugins -y
Configure audit rules in /etc/audit/rules.d/audit.rules to monitor critical identity files and system binaries:
# Record modifications to user and group account files
-w /etc/passwd -p wa -k identity_changes
-w /etc/shadow -p wa -k identity_changes
-w /etc/group -p wa -k identity_changes
-w /etc/gshadow -p wa -k identity_changes
-w /etc/security/opasswd -p wa -k identity_changes
# Monitor changes to sudoers configuration
-w /etc/sudoers -p wa -k sudoers_changes
-w /etc/sudoers.d/ -p wa -k sudoers_changes
# Monitor changes to system network configurations
-w /etc/issue -p wa -k system_banner
-w /etc/hosts -p wa -k network_configs
-w /etc/network/ -p wa -k network_configs
# Monitor changes to system time
-a always,exit -F arch=b64 -S adjtimex -S settimeofday -k time_change
-a always,exit -F arch=b64 -S clock_settime -k time_change
Load the audit rules:
$ sudo augenrules --load
Search for audit logs associated with password file changes:
$ sudo ausearch -k identity_changes --start recent
Ship Logs to an External Centralized System
Local log files on a compromised server cannot be trusted. Always forward system logs (systemd-journald and /var/log/auth.log) to a centralized log aggregator like Grafana Loki, Elasticsearch, or a remote Rsyslog server using TLS.
Configure alerting rules to trigger a Grafana alert or Prometheus alert whenever a spike in authentication failures or unexpected privilege escalation occurs.
10. Interesting Fact
The concept of Secure Shell (SSH) was born out of a massive security breach in 1995.
Tatu Ylönen, a researcher at the Helsinki University of Technology in Finland, discovered that an attacker had installed a password-sniffing packet capture program on the university's backbone network.
At the time, almost all remote administration across the internet was done using Telnet, rlogin, and FTP. None of these protocols used encryption. Every single keystroke, username, and password was transmitted across physical network wires in plain, readable text.
The sniffer captured more than 25,000 usernames and plain-text passwords, compromising hundreds of university servers, research labs, and connected institutions.
Horrified by the scale of the vulnerability, Ylönen spent the next three months designing and writing the first version of SSH (SSH-1). He released it as free software in July 1995. Within six months, SSH was adopted by over 20,000 users in 50 countries, marking the beginning of encrypted system administration as we know it today.
The 10-Minute Production Security Audit Checklist
Before you declare any new Linux server ready for production traffic, run through this quick checklist:
- [ ] SSH Hardened: Root login disabled, password authentication disabled, Ed25519 key authentication enforced, and idle timeout set.
- [ ] Firewall Active: Default-deny policy applied with UFW or firewalld, only essential ports (22, 80, 443) opened.
-
[ ] Listening Ports Checked: Audited with
ss -tulnp, internal databases bound strictly to127.0.0.1. -
[ ] Least Privilege Enforced: Services run under dedicated unprivileged system users with
/usr/sbin/nologinshells. -
[ ] Sudo Permissions Locked: Managed via modular files in
/etc/sudoers.d/with0440permissions, edited only withvisudo. - [ ] Brute-Force Protection: Fail2ban active with SSH jail enabled and bans verified.
-
[ ] Automatic Updates Configured:
unattended-upgradesenabled for security patches. -
[ ] Filesystem Secured:
/tmpand/dev/shmmounted withnoexec,nosuid,nodev. SUID binaries audited. - [ ] MAC Enforced: AppArmor or SELinux running in active enforcing mode.
-
[ ] Kernel Hardened: ASLR enabled, SYN flood protection active, and ICMP redirects blocked via
/etc/sysctl.d/99-security.conf. -
[ ] Auditing & Centralized Logs:
auditdactive with critical file watches, logs shipped off-host with Prometheus alert or Grafana alert triggers configured.
Key Takeaways
Securing a production Linux server is not about installing a single magic tool. It is about applying consistent, layered defenses across every component:
- Minimize the Attack Surface: Disable unused services, close unnecessary ports, and remove unneeded packages like compilers from production nodes.
- Eliminate Plain-Text Credentials: Use SSH keys exclusively, enforce strong password policies, and lock down superuser access.
- Isolate Workloads: Run applications under unprivileged accounts with strict path boundaries and mandatory access controls like AppArmor or SELinux.
- Automate Maintenance: Enable automatic security patches and brute-force IP bans so your server stays protected around the clock.
- Trust, but Verify: Maintain immutable audit trails and forward your logs off-host so you always have full visibility into system events.
By making this checklist a standard part of your server provisioning workflow or Infrastructure-as-Code pipelines, you can deploy production infrastructure with confidence.
What is on Your Server Hardening Checklist?
Which security measures do you always configure on a fresh Linux server? Do you enforce custom AppArmor profiles or use automated CIS benchmark scripts in your deployment pipelines? Let me know in the comments below!
About the Author
Asep Sayyad is a Linux and DevOps engineer passionate about Linux administration, automation, cloud technologies, containers, and open-source software. He enjoys solving real-world infrastructure challenges and sharing practical knowledge through in-depth technical articles, tutorials, and hands-on guides.
His goal is to help aspiring and experienced engineers build stronger Linux and DevOps skills with content focused on real production scenarios rather than theory alone.
Connect with Me
Portfolio: https://asepsayyad007.in
GitHub: https://github.com/asepsayyad007
LinkedIn: https://www.linkedin.com/in/asepsayyad
Medium: https://asepsayyad007.medium.com
Enjoyed this article?
If you found this guide helpful, consider:
- Starring my open-source projects on GitHub.
- Sharing this article with fellow Linux and DevOps engineers.
You can also follow me for more practical content on Linux, DevOps, Cloud, Containers, Automation, and Open Source. Thanks for reading, and enjoy your learning!
© 2026 Asep Sayyad
Top comments (0)