Securing Linux servers toward production standards: the principle of shrinking the attack surface and least privilege, SSH protection with fail2ban, an introduction to SELinux and AppArmor, security auditing with Lynis, to a new Ubuntu server hardening checklist along with common traps.

After episode 24 where we covered system performance tuning & troubleshooting — how to keep a server fast, read bottlenecks correctly, and rescue the system when down — in this episode we discuss the equally important side of the production world: security. A fast but easily breached server is like a sports car without keys — fast, but only a matter of time before someone drives it away.
Security isn't one feature you install, but the result of a collection of small decisions that complement each other. Imagine the house you want to protect: locked doors, a fence high enough, cameras in the corners, vigilant neighbors. None of these elements makes a house "thief-proof" alone — but together, they make thieves pick another target. In the server world, the same principle is called defense in depth: many security layers covering each other's weaknesses.
The unavoidable reality: every server exposed to the internet will be attacked. Botnets scan the internet 24 hours a day for open SSH ports, trying thousands of password combinations per second, and exploiting services with weak configuration. This isn't a theoretical scenario — it's what happens every day to servers no one has even ever heard of. The question isn't whether it will be attacked, but how strong the defense is when the attack comes.
In this episode we'll cover Linux hardening thoroughly: the principle of shrinking the attack surface and least privilege, securing SSH with keys and fail2ban, understanding Mandatory Access Control (SELinux and AppArmor), conducting security audits with Lynis, and closing with a hardening checklist for a new Ubuntu server along with the traps that most often trip admins.
The technical term you must understand from the start is attack surface — the set of all points an attacker can touch: open ports, running services, existing users, and installed applications. The most fundamental hardening principle is simple: the smaller the attack surface, the fewer exploitable gaps.
Every running service is a potential entry door. Every open port is a door that can be broken down. Every user with high privileges is a spare key that can be stolen. A "convenient" default Linux installation is usually too open for production: SSH with passwords, many unused services still running, and no automatic security updates. Hardening is the process of narrowing all of that down to the true minimum needed.
There's an analogy I always use: an airplane. A plane has no unnecessary things — every system exists because it contributes to flight safety. The more "items" carried without a clear role, the bigger the risk and the more complex the maintenance. A good production server is like a plane: lean, every component has a reason to exist, and nothing is left around because "it might be useful someday".
The first step you can take immediately is auditing the running services. The simple question: what do I use this service for? If there's no clear answer, the service should probably be turned off.
systemctl list-units --type=service --state=running
ss -tlnpsystemctl list-units shows running services; ss -tlnp shows all currently listening ports. Match the two: which service opens which port? Which ports shouldn't be open? On a minimalist server, you should only see truly needed ports — for example 22 (SSH) and 80/443 (web). Other unclear ports are an alarm.
The second principle is least privilege — give every user and process the minimum rights needed to do their job. This isn't about trust, but about limiting damage: if one account is breached, how far can an attacker move? In practice:
root; use a normal user and sudo when needed (back to episode 10).nginx, mysql, www-data — not as root.sudoers rules, not by handing out full rights carelessly.Warning
Golden rule: services that open ports to the outside network must run with minimal privileges. A web application running as root is one exploit away from full server control. Always check with ps aux | grep <app> — if the USER column shows root for a service that shouldn't, fix it immediately.
The most common attack every public server faces is SSH brute-force — automatically trying millions of username/password combinations. A server allowing SSH login with passwords is a soft target, because a weak password is just waiting to be guessed or leaked via phishing.
The solution was already touched in the SSH episode: public key authentication. SSH keys are cryptographic — practically impossible to guess — and can't be faked by dictionary attacks. The mandatory configuration changes in /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication yes
PasswordAuthentication no
PubkeyAuthentication yesNote the diff above. PermitRootLogin no disables direct root login — attackers have no obvious username to guess. And PasswordAuthentication no disables password-based login entirely — the only way in is keys. But there's a non-negotiable condition: make sure your key is installed and tested before disabling passwords, or you'll lock yourself out of your own server.
After changing the config, validate and restart safely:
sudo sshd -t
sudo systemctl restart sshdsshd -t validates the config syntax before applying it — saving you from a wrong config file that kills SSH entirely.
Important
Don't close the door before the key is in hand. The safe order: (1) generate a key pair on the local machine with ssh-keygen, (2) copy the public key to the server with ssh-copy-id, (3) log in once using the key and make sure it works, (4) only then set PasswordAuthentication no. Skip any step, and you're locked out of your own server — possibly having to fix it via the cloud console.
SSH keys already neutralize brute-force password attacks — but your server can still be harassed: attackers keep trying to log in repeatedly, flooding the logs with failed attempts, and wasting CPU and bandwidth resources. This is where fail2ban works: it monitors logs, detects patterns of repeated attempts, and blocks the attacker's IP at the firewall for a set period.
Fail2ban's way of working can be compared to a doorman with a memory: if the same person tries to get in with the wrong key several times, the doorman notes their face and bans them from the building for a few hours. That's exactly what fail2ban does to IPs — based on rules defining "how many attempts within how many minutes" counts as suspicious.
Install fail2ban on Debian/Ubuntu:
sudo apt update
sudo apt install -y fail2banThe main config is in /etc/fail2ban/jail.conf, but don't edit that file directly — package updates will overwrite it. The correct pattern is writing an override in /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8
[sshd]
enabled = true
port = ssh
logpath = /var/log/auth.log
maxretry = 3The interpretation: within a findtime window of 600 seconds (10 minutes), if there are more than maxretry failed attempts (default 5, for SSH we tighten it to 3), the IP is banned for a bantime of 3600 seconds (1 hour). ignoreip ensures internal IPs are never blocked — important so the team doesn't lock itself out while trying repeatedly. logpath points to the authentication log file — /var/log/auth.log on Debian/Ubuntu or /var/log/secure on RHEL/Rocky.
After the config is created, enable it and check its status:
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshdThe fail2ban-client status sshd output shows the number of currently banned IPs and failure statistics. After that, all brute-force attempts matching the rules are automatically blocked — and you can see them in the log:
sudo grep "Ban" /var/log/fail2ban.log | tail -20Caution
Watch ignoreip and bantime — a wrong config can lock you out yourself. If ignoreip doesn't include your network's IP (e.g. office or VPN) and you're too aggressive (maxretry too low), accidentally failed attempts could get your own team's IP banned. Always include management/internal IPs in ignoreip, and test the config in staging before applying to production.
So far we've discussed security based on DAC (Discretionary Access Control) — access control determined by the file owner (the rwx permissions we learned). But if an application process is taken over by an attacker, that process inherits all its user's rights — and if that's the database user, the attacker can read all data. This is where MAC (Mandatory Access Control) comes in: a layer limiting what a process is allowed to do regardless of its user's rights.
There are two main MAC implementations in the Linux world: SELinux (the RHEL/Rocky family) and AppArmor (Ubuntu/Debian).
SELinux works by giving labels to every file, process, and port, then enforcing a policy that determines interactions between labels. There are three modes it can run in:
| Mode | Behavior |
|---|---|
Enforcing | Policy strictly enforced; violating access is blocked and logged |
Permissive | Violations are logged but not blocked — a trial mode |
Disabled | SELinux completely off; no labels verified |
Check SELinux status with sestatus, and change the mode temporarily with setenforce:
sestatus
getenforcesudo setenforce 0 # Permissive (trial)
sudo setenforce 1 # Enforcing (strict)AppArmor on the Ubuntu side works with a different approach: instead of global labels, every program has a profile describing which files and network it's allowed to access. Profiles are enabled/disabled per program, and the list can be seen with aa-status:
sudo aa-statusThe most common mistake here is the "SELinux vs AppArmor" confusion — they're not interchangeable and are handled with different tools. RHEL/Rocky servers use SELinux (check with sestatus); Ubuntu servers use AppArmor (aa-status). When an application oddly "can't access a file" even though permissions are correct, this is most likely MAC at work: on RHEL, check ausearch -m avc -ts recent; on Ubuntu, check the journal or /var/log/kern.log.
Warning
Never solve a MAC problem by turning off MAC. setenforce 0 only lasts until reboot — but if you change /etc/selinux/config to SELINUX=disabled (permanently) or leave AppArmor in complain mode forever, you've discarded an entire defense layer without noticing. The correct pattern: find the wrong policy (check the AVC logs), fix the label or profile, then return to Enforcing mode. A permanently disabled MAC is a security hole that won't be visible until an incident happens.
Hardening without verification is just a claim. How do you know the configuration is correct? This is where Lynis — the open source security audit scanner — comes in. Lynis scans hundreds of controls: SSH config, file permissions, running services, security updates, user accounts, and much more, then gives a hardening index score along with remediation recommendations.
Install and run the audit:
sudo apt install -y lynissudo lynis audit systemLynis output is very long — there's an important part you should look for at the end of the report:
[+] Security
...
Hardening index : 62 [############.....]
Tests performed : 238
Suggestions : 12
Warnings : 3The keys to reading it: Hardening index is a comprehensive score (higher is better), while Warnings and Suggestions are lists of things to fix — for details, scroll to the end of the report or read the /var/log/lynis-report.dat file. The full report is stored in /var/log/lynis.log.
Lynis doesn't fix anything — it finds problems and gives recommendations. It's your job to apply them one by one, then rerun the audit to see the score rise. This is the healthy cycle: audit → fix → re-audit. A stagnant score means your hardening isn't progressing.
Tip
Make auditing a routine, not a one-time event. Run lynis audit system after initial setup, then periodically (e.g. monthly). Save the results and compare across time — a spike of new warnings is an alarm that something changed on the system. In more mature teams, this automatic audit is scheduled and its reports sent to the security team — exactly the automation pattern we built in episode 22.
Time to combine all the principles into one checklist you can directly practice on a new Ubuntu server. Follow it in order — each step builds on the previous:
1. Update the system. A new un-updated server is a field of known vulnerabilities. This is the first non-negotiable step:
sudo apt update && sudo apt upgrade -y2. Create an admin, non-root user. Logging in as root is dangerous (episode 10); create a normal user with sudo access:
sudo adduser arman
sudo usermod -aG sudo arman3. Install the SSH key, disable passwords. From the local machine: copy the public key, then on the server set PasswordAuthentication no in /etc/ssh/sshd_config (follow the pattern from the SSH Keys section above).
4. Configure the firewall (UFW). Open only the needed ports — SSH (22) and web (80/443) — then enable:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status5. Install fail2ban. Follow the jail.local config from the previous section, with ignoreip including your management IPs.
6. Enable automatic security updates. Vendors patch vulnerabilities faster than you can update manually. unattended-upgrades installs security patches automatically in the background:
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades7. Audit the results. Close the loop with verification: lynis audit system for the overall score, and check that only allowed ports are open:
ss -tlnpRun this entire checklist in one calm session, not in the middle of an incident. A server hardened this way takes a one-time effort, but the results last — and every time there's a new server, the same checklist can be reused exactly, consistently, and tested.
Remember too that a checklist is a start, not an end. Production servers keep changing: new applications add ports, vendor updates change behavior, new teams add users. Hardening is a process that must be reviewed periodically — schedule a monthly re-audit and update the checklist as the infrastructure evolves. A checklist never reviewed is rotting documentation.
1. setenforce 0 that becomes permanent. Changing /etc/selinux/config to SELINUX=disabled to "solve" an application problem is the most expensive mistake — the entire MAC layer disappears unnoticed until an incident. Fix the policy, not turn off MAC.
2. Assuming SELinux and AppArmor are the same. Both are MAC but with different tools, philosophies, and logs. sestatus for RHEL/Rocky, aa-status for Ubuntu. Checking wrong makes you look for problems in the wrong place.
3. A fail2ban config that locks you out yourself. maxretry too low, ignoreip missing your management IPs, or bantime too long — a combination that locks the team out of the server. Test in staging and make sure there's a recovery path (e.g. cloud console) before applying.
4. Disabling SSH passwords before the key is installed. Wrong order = total lockout. Install the key, test login, then disable PasswordAuthentication.
5. Forgetting the complementary layers. Installing fail2ban but not locking down SSH, or updating once then never again. Hardening is layered and continuous — one layer isn't enough, and one weak point defeats all the others.
In this episode 25, you've built the Linux security foundation toward production standards. We discussed the principle of shrinking the attack surface and least privilege, auditing running services and disabling unused ones, securing SSH with public keys and PasswordAuthentication no, installing fail2ban to block brute-force, understanding SELinux and AppArmor as MAC layers, auditing with Lynis, and closing with an ordered new-Ubuntu-server hardening checklist.
Key points to take with you:
Now your server is fast (episode 24) and secure (episode 25). But there's one thing we haven't discussed: what happens when everything fails? In the next episode 26 we'll discuss Backup & Restore Strategy — the 3-2-1 principle, choosing the right tool (rsync, tar, restic, borgbackup), backing up databases correctly, and a tested restore procedure. Because even the best defense means nothing without a recovery plan. Stay sharp!