Guide

Linux server hardening is a process, not a one-time checklist

A hardened server can drift. Packages change, users are added, services are enabled, applications open ports, and teams make emergency changes. This guide gives each control with the reason for it, the commands per distribution, a way to verify it and a way to undo it.

The short answer

Linux server hardening means reducing what an attacker can reach and what a mistake can break: install security updates, require keys for SSH, deny inbound traffic by default, close services you do not use, limit who can become root, and keep logs and backups you can rely on. Done once, it decays, because servers change. Good hardening therefore combines four things: a secure baseline, verification of each control, awareness of change, and ongoing monitoring. Every control below has a verify step and a rollback, for Debian, Ubuntu, Rocky Linux and AlmaLinux.

Before you start

The order below is by value per minute, not by severity. The first fifteen minutes remove the most common ways in; the first hour closes what is left open by default; the first day and week are about being able to see and recover. Work on one server first, and only then repeat it elsewhere.

Three habits make every change on this page safe to try.

  • Keep a second SSH session open the whole time. Reloading SSH or a firewall does not close sessions that already exist, so a mistake in one window can be fixed from the other.
  • Know how to reach the provider's web console or rescue mode, and log in to it once before you need it.
  • Take a provider snapshot before the first change if you can. Each control below has its own rollback, but a snapshot is the only one that covers everything.

Commands are written for a user with sudo. "Debian and Ubuntu" means Debian 12 and 13 and Ubuntu 22.04 and 24.04; "Rocky Linux and AlmaLinux" means releases 9 and 10, and applies to other RHEL-compatible systems of the same generation. Where release 8 differs, the control says so.

First 15 minutes

Six controls that close the most common ways into a new server. None of them needs a maintenance window, and all of them can be verified in one command.

1. Install pending updates

Why
Known vulnerabilities in installed packages are the most common way into a server that is otherwise configured well. A new server image can be months behind on the day it is created.
Apply
Debian and Ubuntu
sudo apt update
sudo apt upgrade
Rocky Linux and AlmaLinux
sudo dnf upgrade --refresh
Verify
Debian and Ubuntu
apt list --upgradable
[ -f /var/run/reboot-required ] && echo "reboot required"
An empty list means nothing is pending. If the second line prints its message, a reboot is needed before the new kernel or library is actually running. The file is written by a hook that ships with the unattended-upgrades package (control 8); on a server without it, nothing records that a reboot is pending.
Rocky Linux and AlmaLinux
sudo dnf check-update --security
sudo needs-restarting -r
check-update exits with 100 when updates are available and 0 when there are none. needs-restarting is part of the yum-utils package, which a minimal installation does not include ("sudo dnf install yum-utils"), and exits with 1 when a reboot is needed.
Roll back
On Rocky Linux and AlmaLinux, "sudo dnf history" lists transactions and "sudo dnf history undo ID" reverses one. apt on these releases has no undo (one arrives with apt 3.2 in Ubuntu 26.04): /var/log/apt/history.log records what changed, and a single package can be put back with "sudo apt install package=version" while that version is still available. For anything larger, the snapshot is the rollback.
By distribution
  • Debian. Security fixes come from security.debian.org in the suite named after the release, for example bookworm-security or trixie-security. Check that the line is present in your apt sources; without it, "up to date" means only up to date with the last point release.
  • Ubuntu. Security fixes arrive in the -security pocket, for example noble-security.
  • Rocky Linux and AlmaLinux. The --security filter works because both projects publish advisory metadata with their repositories, as RLSA and ALSA advisories.
How SecAI sees drift
The agent reads pending updates on every server without changing anything: a simulated upgrade on Debian and Ubuntu, and check-update on the RHEL family, once in total and once for security updates. It reports both counts, up to twenty package names, and whether a reboot is pending. Separately, every installed package is matched against published CVE data every 12 hours.

2. Work from a named administrator account

Why
One account per person makes every privileged action attributable, and lets you remove one person's access without touching anyone else's. Direct root login gives neither.
Apply
Debian and Ubuntu
sudo adduser alice
sudo usermod -aG sudo alice
Rocky Linux and AlmaLinux
sudo useradd -m alice
sudo passwd alice
sudo usermod -aG wheel alice
Verify
id alice
sudo -l -U alice
The first shows the sudo or wheel group; the second lists what the account may run as root.
Roll back
Remove the account from the group with "sudo gpasswd -d alice sudo" (or wheel). Delete the account only after checking that nothing runs as it.
By distribution
  • Debian. If a root password was set during installation, the installer does not set up sudo. Install it as root with "apt install sudo" before adding the account to the group.
  • Ubuntu. Cloud images arrive with a default "ubuntu" account that already has sudo; create named accounts anyway if more than one person will log in.
How SecAI sees drift
SecAI raises a finding for every new local account, rated lower when the package manager confirms that a package created it, and for every change to sudo grants. A privilege change that happens outside any session of a registered administrator key is reported separately, because that is the shape an intruder's change has.

3. Prove that key login works before changing SSH

Why
Turning off password login is the single most valuable change on this page, and the only one that can lock you out. Proving key login first, in a new session, removes that risk.
Apply
On your own computer
ssh-keygen -t ed25519
ssh-copy-id alice@your-server
Verify
On your own computer
ssh -o PasswordAuthentication=no alice@your-server true && echo "key login works"
The option makes the client refuse to fall back to a password, so the message only appears when the key was accepted.
Roll back
Nothing to undo: adding a key changes no existing access. To remove a key later, delete its line from ~/.ssh/authorized_keys on the server.
How SecAI sees drift
A new line in any authorized_keys file is a finding of its own, with the key's fingerprint and comment, because an added key is the most common way access is kept after a password is changed. The text of the change is kept, so you can see exactly which key appeared and when.

4. List what is listening

Why
Every listening service is reachable attack surface. Most servers listen on more than their owner thinks: a database installed for a test, a panel, a development server somebody left running.
Verify
sudo ss -tulpn
Read the local address column. 127.0.0.1 and [::1] are reachable only from the server itself. 0.0.0.0, [::] and * are reachable from anywhere the firewall allows. Every line should be something you can explain.
Roll back
Read-only; nothing changes.
How SecAI sees drift
Listening ports are part of the inventory the agent reports. It also reads the configuration of the services most often exposed by accident, MySQL, Redis, FTP servers and ports published by Docker, and flags an open mail relay or an open DNS resolver, each from the service's own configuration rather than by probing.

5. Deny inbound traffic by default

Why
A default-deny firewall turns "everything that happens to be listening" into "only what you chose to expose". It is also what protects you on the day somebody installs something that binds to every interface.
Apply
Debian and Ubuntu (ufw)
sudo apt install ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw enable
Allow SSH before enabling, and use your real SSH port if it is not 22. Then allow only the ports you serve, for example "sudo ufw allow 443/tcp".
Rocky Linux and AlmaLinux (firewalld)
sudo dnf install firewalld
sudo systemctl enable --now firewalld
sudo firewall-cmd --list-all
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
The default zone already rejects what is not listed, and the ssh service, which means port 22, is listed. If your SSH listens on another port, allow it before firewalld starts: "sudo firewall-offline-cmd --add-port=PORT/tcp". Remove listed services you do not use with "sudo firewall-cmd --permanent --remove-service=NAME", then reload.
Verify
Debian and Ubuntu
sudo ufw status verbose
Expect "Status: active" and "Default: deny (incoming)".
Rocky Linux and AlmaLinux
sudo firewall-cmd --state
sudo firewall-cmd --list-all
Roll back
"sudo ufw disable" switches ufw off and leaves its rules in place for next time. In firewalld, add the service back with --permanent --add-service and reload. If you have locked yourself out, the provider console still works, because it does not come in over the network.
By distribution
  • Debian. A default installation has no firewall rules at all and does not install ufw. nftables is the framework underneath.
  • Rocky Linux and AlmaLinux. A standard installation enables firewalld. The official cloud images are different: Rocky Linux builds its GenericCloud, EC2 and Azure images without firewalld, and AlmaLinux builds its GenericCloud image with the firewall disabled, so on a cloud server check with "sudo firewall-cmd --state" before assuming it is there.
  • Docker hosts. Ports published by Docker are handled before ufw's rules are consulted, so a published container port is reachable even when ufw appears to block it. Publish to 127.0.0.1 unless the port is meant to be public.
How SecAI sees drift
The agent queries whichever of ufw, firewalld, nftables or iptables answers first and reports whether it is active, its default inbound policy and its rule count. It also reports something most checks miss: whether the firewall actually turns away traffic it does not list. A firewall whose policy is accept and whose only rules ban individual addresses counts as running and protects nothing, and SecAI says so.

6. Check that the clock is synchronised

Why
Logs are evidence only if their times can be trusted, and several security mechanisms, certificate validation among them, fail in confusing ways when the clock is wrong.
Apply
Debian and Ubuntu
sudo timedatectl set-ntp true
Rocky Linux and AlmaLinux
sudo systemctl enable --now chronyd
chronyc tracking
Verify
timedatectl
Expect "System clock synchronized: yes" and an active NTP service.
Roll back
"sudo timedatectl set-ntp false" turns synchronisation off again; there is rarely a reason to.
How SecAI sees drift
The agent reports whether the clock is synchronised, which service does it and the measured offset where one is available. When it cannot tell, it reports unknown rather than guessing.

First hour

The changes that close what most distributions leave open by default. Each one edits configuration, so each one has a test step before the service is reloaded.

7. Turn off SSH password login, and restrict root login

Why
With password login off, guessing passwords stops being an attack at all: attempts still arrive and none can succeed. Restricting root to key-only login takes away the account every botnet tries first.
Apply
All four distributions
sudo tee /etc/ssh/sshd_config.d/00-hardening.conf >/dev/null <<'EOF'
PasswordAuthentication no
PermitRootLogin prohibit-password
EOF
sudo sshd -t
sshd uses the first value it reads for each setting, and the drop-in directory is read in name order, so the file is named 00- to be read before a cloud image's own drop-in. "sshd -t" checks the syntax and prints nothing when the configuration is valid. "prohibit-password" still lets root log in with a key; once every administrator uses a named account with sudo (control 2), set "PermitRootLogin no" instead, which is what makes every privileged session start as a named person.
Debian and Ubuntu
sudo systemctl reload ssh
Rocky Linux and AlmaLinux
sudo systemctl reload sshd
Verify
sudo sshd -T | grep -i -E '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin)'
This prints the configuration sshd is really using, after every included file. Expect "passwordauthentication no", "kbdinteractiveauthentication no" and "permitrootlogin prohibit-password" (some versions print "without-password", which means the same). Then, from your own computer, confirm that a password is refused: "ssh -o PubkeyAuthentication=no alice@your-server" should end in "Permission denied".
Roll back
Delete /etc/ssh/sshd_config.d/00-hardening.conf, run "sudo sshd -t" and reload. Sessions that are already open survive a reload, which is why the second session matters.
By distribution
  • Cloud images. Many ship /etc/ssh/sshd_config.d/50-cloud-init.conf with "PasswordAuthentication yes". Because the first value wins, a file of yours named 99- would silently lose to it. List the directory before choosing a name.
  • Release 8 of Rocky Linux and AlmaLinux. The package does not ship a drop-in directory. Run "grep -i '^include' /etc/ssh/sshd_config"; if nothing prints, put the two lines in /etc/ssh/sshd_config itself, above any existing value.
  • Rocky Linux and AlmaLinux. Ciphers, MACs and key exchange are governed by the system-wide crypto policy ("update-crypto-policies --show"). Change the policy rather than hand-editing cipher lists in sshd_config.
How SecAI sees drift
The agent reads the effective configuration with "sshd -T", the same command as above, so a drop-in that quietly re-enables passwords is seen even though the main file looks correct. Password login or unrestricted root login becomes a finding with a proposed fix that waits for your approval; an approved fix is applied with a snapshot first and restored automatically if SSH does not come back healthy. Changes to sshd_config and its drop-in directory are reported by file integrity monitoring, and a root login over SSH is an alert of its own.

8. Make security updates automatic

Why
Manual patching does not survive a busy month. Security-only automatic updates close most published vulnerabilities within a day, with far less outage risk than updating everything unattended.
Apply
Debian and Ubuntu
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Rocky Linux and AlmaLinux
sudo dnf install dnf-automatic
sudoedit /etc/dnf/automatic.conf
sudo systemctl enable --now dnf-automatic.timer
In the [commands] section of automatic.conf set "upgrade_type = security" and "apply_updates = yes".
Verify
Debian and Ubuntu
apt-config dump | grep -i periodic
sudo unattended-upgrade --dry-run
Expect Update-Package-Lists "1" and Unattended-Upgrade "1". The dry run shows what would be installed without installing it; the real runs are logged in /var/log/unattended-upgrades/.
Rocky Linux and AlmaLinux
systemctl list-timers 'dnf-automatic*'
Roll back
On Debian and Ubuntu run the dpkg-reconfigure command again and answer No. On the RHEL family, "sudo systemctl disable --now dnf-automatic.timer".
By distribution
  • Ubuntu. Server installations enable unattended security updates by default; verify rather than assume.
  • Debian. The Debian installer does not install unattended-upgrades, so a server installed that way has no automatic updates until you add the package; installing it switches it on, and the second command lets you confirm or change the answer. Debian's official cloud images already include it.
  • Rocky Linux and AlmaLinux. Red Hat's own procedure enables "dnf-automatic-install.timer" instead, which downloads and installs whatever the configuration file says; either works, and only one should be enabled.
  • All. Neither tool reboots the server by default. A kernel or core library update is installed but not running until the next reboot, which is what the reboot check in control 1 is for.
How SecAI sees drift
SecAI does not read the automatic-update configuration itself. What it shows is the outcome: a count of pending security updates that keeps growing, or a reboot that stays pending, means the mechanism you enabled is not doing its job.

9. Stop services you do not use

Why
A service that is not running cannot be attacked, and cannot surprise you by listening on a new port after an update.
Apply
systemctl list-units --type=service --state=running
sudo systemctl disable --now NAME.service
Disable first and remove later. Removing a package can take others with it, so read the list the package manager shows before confirming.
Verify
systemctl is-enabled NAME.service
sudo ss -tulpn
Roll back
"sudo systemctl enable --now NAME.service".
How SecAI sees drift
New or changed units under /etc/systemd/system are reported by file integrity monitoring together with the text of the change, because a new unit is also a common way for an intruder to survive a reboot. Units in a failed state and the list of listening ports are reported with the rest of the server's posture.

10. Keep databases and caches off the public interface

Why
A database reachable from the internet is attacked within hours, and most were never meant to be reachable: the application that uses them runs on the same server.
Apply
MySQL and MariaDB
bind-address = 127.0.0.1
On Debian and Ubuntu the line lives in /etc/mysql/mysql.conf.d/mysqld.cnf (MySQL) or /etc/mysql/mariadb.conf.d/50-server.cnf (MariaDB); on Rocky Linux and AlmaLinux in /etc/my.cnf or a file under /etc/my.cnf.d/. Restart the service afterwards.
PostgreSQL
listen_addresses = 'localhost'
This is the default in postgresql.conf; check that nobody changed it to '*'.
Redis
bind 127.0.0.1 -::1
protected-mode yes
Verify
sudo ss -tlnp | grep -E ':(3306|5432|6379|27017|9200)\b'
Every line should show 127.0.0.1 or [::1] as the local address. No output means none of these is listening at all.
Roll back
Copy the file before editing ("sudo cp -a FILE FILE.before-hardening"), and copy it back if the application stops connecting. An application on another server needs a private network address and a firewall rule for that one source, not a public listener.
How SecAI sees drift
The agent checks MySQL's bind address and networking setting, and PostgreSQL's listen addresses, TLS setting and any "trust" authentication line, and reads the exposure of MySQL, Redis, FTP servers and Docker-published ports from their own configuration.

11. Review who can become root

Why
sudo rules accumulate. A rule added for one deployment two years ago is still a way to become root today, and "NOPASSWD: ALL" turns any compromise of that account into a compromise of the server.
Apply
sudo visudo -f /etc/sudoers.d/FILE
sudo visudo -c
Always edit through visudo, which refuses to save a file that does not parse. "visudo -c" checks every sudoers file.
Verify
getent group sudo wheel
sudo ls -la /etc/sudoers.d/
sudo grep -r NOPASSWD /etc/sudoers /etc/sudoers.d/
sudo -l -U alice
Roll back
Keep a root shell open while you edit sudoers. If a rule is wrong, fix it from that shell; a broken sudoers file cannot be repaired with sudo.
How SecAI sees drift
Every sudo grant is read on each pass and compared with the last, and changes to /etc/sudoers and /etc/sudoers.d are reported with the text before and after.

12. Hide version banners and directory listings on the web server

Why
A version banner tells every scanner which exploits to try first, and a directory listing hands over file names nobody meant to publish. Neither is a defence on its own; both are free.
Apply
nginx
server_tokens off;
In the http block of /etc/nginx/nginx.conf. Remove any "autoindex on;" you did not intend. Then "sudo nginx -t && sudo systemctl reload nginx".
Apache
ServerTokens Prod
ServerSignature Off
On Debian and Ubuntu in /etc/apache2/conf-available/security.conf, then "sudo apache2ctl configtest && sudo systemctl reload apache2". On Rocky Linux and AlmaLinux in /etc/httpd/conf/httpd.conf, then "sudo apachectl configtest && sudo systemctl reload httpd".
Verify
curl -sI https://www.example.com | grep -i '^server'
Expect "Server: nginx" or "Server: Apache" with no version number.
Roll back
Restore the previous line and reload. The config test before each reload means a typo never takes the site down.
How SecAI sees drift
The agent checks nginx for version banners, directory listing, a missing X-Frame-Options header, TLS and the request body limit, and Apache for its version banner, reading the configuration the web server actually loads.

13. Check permissions on the files that hold accounts and keys

Why
These files decide who can log in and as what. They are correct on a fresh install, so the useful work is confirming nobody loosened them and recording what normal looks like.
Verify
stat -c '%a %U:%G %n' /etc/passwd /etc/shadow /etc/gshadow /etc/ssh/sshd_config
sudo find / -xdev -perm -4000 -type f 2>/dev/null
Save the second list while the server is known-good. A setuid binary that appears later, outside a package update, is a serious finding.
Roll back
Read-only. If a mode is wrong, compare with a fresh installation of the same release before changing it; guessing at system file permissions breaks logins.
By distribution
  • Debian and Ubuntu. /etc/shadow is 640 and owned by root with group shadow.
  • Rocky Linux and AlmaLinux. /etc/shadow has mode 0 and is owned by root:root; that is correct, because root reads it regardless of the mode.
How SecAI sees drift
New or changed setuid, setgid and capability binaries are findings of their own, and privileged binaries that changed are checked against the package manager's record (dpkg -V or rpm -V) so that an ordinary package update is not reported as tampering. The account and authentication files are hashed on every pass.

First day

By now the server is much harder to get into. The first day is about what happens if something gets in anyway: being able to see it, and being able to recover.

14. Keep logs across reboots, and know where they are

Why
An investigation that starts a month late needs a month of logs. On some installations the journal lives in memory and is gone after a reboot, which is exactly when you want it.
Apply
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
sudo journalctl --flush
With the directory present, journald keeps its files on disk, and the flush moves what it already holds in memory there. Cap the size with "SystemMaxUse=" in a file under /etc/systemd/journald.conf.d/; by default it may use up to a tenth of the filesystem, to a maximum of 4 GB.
Verify
journalctl --disk-usage
journalctl --list-boots | head
More than one boot in the list means the journal is persistent.
Roll back
Remove the journald.conf.d file you added. The logs themselves are worth keeping.
By distribution
  • Debian and Ubuntu. The journal is already persistent on a fresh installation of the releases covered here, so the verify step is usually all you need.
  • Debian 12 and later. rsyslog is no longer installed by default, so /var/log/auth.log does not exist and SSH events are only in the journal: "journalctl -u ssh". Tools that expect the file, fail2ban among them, need to be pointed at the journal.
  • Ubuntu. Server installations still write /var/log/auth.log as well as the journal; minimal and container images may not.
  • Rocky Linux and AlmaLinux. The journal is kept in memory under /run/log/journal by default and is lost at every reboot, so this control matters most here. The text logs written by rsyslog survive: authentication events are in /var/log/secure, and in the journal as "journalctl -u sshd".
How SecAI sees drift
The agent reads authentication events from /var/log/auth.log or /var/log/secure where the file exists, and from sshd's journal where it does not. What it reports is kept by SecAI, off the server, so the record of logins, findings and actions survives an intruder who edits the local logs.

15. Watch the files that should not change

Why
Almost every way of keeping access to a server leaves a file behind: a key, a cron entry, a systemd unit, a changed sshd_config, a new line in sudoers. Knowing that one of those files changed, and when, turns a breach you would have found in months into one you find today.
Verify
Debian and Ubuntu
sudo dpkg -V
Rocky Linux and AlmaLinux
sudo rpm -Va
Both compare installed files with the package manager's own record and print only what differs. Changed configuration files (marked c) are expected; a changed binary under /usr/bin or /usr/sbin is not.
Roll back
Read-only.
How SecAI sees drift
This is what the agent does continuously. It hashes the account and authentication files, sshd_config and its drop-in directory, PAM, /etc/crontab, /etc/cron.d and the user crontabs (in /var/spool/cron/crontabs on Debian and Ubuntu, /var/spool/cron on the RHEL family), /etc/sudoers and /etc/sudoers.d, root's authorized_keys, and units under /etc/systemd/system. For cron, sudoers, authorized_keys and systemd units it keeps the text of the change, so the finding shows what was added rather than only that something was.

16. Know which installed packages are vulnerable

Why
Pending updates tell you what your repositories can fix today. A vulnerability view tells you which of your installed packages have a published CVE, including the ones with no fix yet, so that you can decide what to do in the meantime.
Verify
Debian and Ubuntu
apt list --upgradable
Rocky Linux and AlmaLinux
sudo dnf updateinfo list --security
Lists the security advisories that apply to the packages installed on this server.
Roll back
Read-only.
How SecAI sees drift
The agent sends the list of installed packages and versions from dpkg or rpm, and SecAI matches it against OSV.dev every 12 hours using the advisories for your exact release: Debian and Ubuntu by version, Rocky Linux and AlmaLinux by major release. A finding names the fixed version when the advisory gives one, and a package you remove takes its findings with it.

17. Restore a backup once

Why
A backup that has never been restored is a hope. The failures that matter, an empty archive, a missing database dump, a key nobody has, only show up on a restore.
Verify
# Restore last night's backup to a scratch server,
# start the application there, and check the newest record.
Roll back
Nothing to undo. Also check that the backup destination cannot be written or deleted with credentials stored on the server being backed up, or ransomware takes both.
How SecAI sees drift
The agent observes whether a backup job exists, how its last run ended, whether new archives are actually being written, and whether a copy leaves the server, including the backup records that cPanel, Plesk and DirectAdmin keep. It never claims that a backup can be restored: that stays a test only you can do.

18. Confirm the way back in

Why
Every control above assumes that you can still reach the server when something goes wrong. Test that assumption while nothing is wrong.
Verify
# 1. Log in through the provider's web console.
# 2. Log in with a second administrator's key.
# 3. Write down where both are documented.
Roll back
Nothing to undo.
How SecAI sees drift
This matters for SecAI's own blocking too. Your own address and any address you mark as trusted are never blocked, every automatic block expires after 24 hours, and the "Let me in" link in an alert lifts a block that caught you without needing approval.

19. Use the provider's firewall as a second layer

Why
A security group or cloud firewall sits outside the server, so it still works when the host firewall is misconfigured, flushed by a reload, or bypassed by Docker. If your administrators have fixed addresses, it is also the right place to limit SSH to them.
Verify
# From a machine outside your network, and only against your own server:
nc -vz server.example.com 3306
A closed port should time out or be refused. Test the ports from control 4 that are not meant to be public.
Roll back
Provider firewall rules are edited in the provider's panel and take effect without touching the server, so a bad rule can always be fixed from outside.
How SecAI sees drift
The agent runs on the server and cannot see a provider firewall. It reports the host firewall only; keep the two in agreement yourself.

First week

Slower work that pays off over the life of the server.

20. Leave SELinux or AppArmor switched on

Why
Mandatory access control limits what a compromised service can touch. It is enabled by default on all four distributions, so the control is mostly about not switching it off when it gets in the way.
Verify
Rocky Linux and AlmaLinux
getenforce
Expect "Enforcing".
Debian and Ubuntu
sudo aa-status
Expect the module loaded and profiles in enforce mode. aa-status is in the apparmor package.
Roll back
When a service is denied something it needs, find the denial and allow that one thing rather than disabling the system: on the RHEL family "sudo ausearch -m AVC -ts recent" shows recent denials. "sudo setenforce 0" switches SELinux to permissive until the next reboot, which is useful for confirming that SELinux is the cause and should not be left that way.
By distribution
  • Rocky Linux and AlmaLinux. Moving SSH to another port needs the port labelled first: "sudo semanage port -a -t ssh_port_t -p tcp 2222". Without it sshd fails to start on the new port. semanage is in the policycoreutils-python-utils package.
How SecAI sees drift
SecAI does not currently check whether SELinux or AppArmor is enforcing. Verify it yourself, and again after major upgrades.

21. Add brute-force protection for SSH

Why
With password login off, guessing cannot succeed, but the attempts still fill the logs and consume connections. fail2ban or CrowdSec bans the sources that keep trying.
Verify
sudo fail2ban-client status sshd
Zero bans on an internet-facing server usually means the jail is not matching your logs, not that nobody is trying.
Roll back
"sudo fail2ban-client unban ADDRESS" lifts one ban. Add your own addresses to "ignoreip" before you need to.
How SecAI sees drift
SecAI blocks through whichever tool the server already runs, fail2ban first, then CrowdSec, CSF, firewalld or iptables, so the two do not fight. The full walk-through, including the journal-only case on Debian, is in the article on stopping SSH brute force.

22. Write down what normal looks like

Why
You cannot recognise an abnormal process, listener, account or cron job without a record of the normal ones. Captured while the server is known-good and stored somewhere else, that record is the fastest triage tool you will have.
Verify
# The hardening checklist has a ready-made script:
# listeners, processes, setuid files, cron and accounts into one dated file.
Roll back
Nothing to undo. Store the file off the server; a baseline captured after a compromise records the intrusion as normal.
How SecAI sees drift
SecAI builds its own baselines from the server's behaviour: resource use is compared with the server's own history rather than a fixed threshold, outbound destinations are compared with those seen before, and monitored files with their last known state.

23. Know when certificates expire

Why
An expired certificate is an outage, and the workaround people reach for, clicking through the warning, trains exactly the wrong habit.
Verify
echo | openssl s_client -connect www.example.com:443 -servername www.example.com 2>/dev/null | openssl x509 -noout -enddate
Roll back
Read-only.
How SecAI sees drift
The agent reads the certificates the web server is configured to serve, and SecAI checks the certificate visitors actually receive, so an expiring or mismatched certificate is reported before it becomes an outage.

Ongoing

Everything above describes one good day. The server will be changed hundreds of times after it, mostly for good reasons, and hardening only holds if somebody notices when a change undoes it. The habit to build is re-running the verify steps, not the apply steps: the question is never "did we harden this server" but "does it still match what we set".

  • Patch. Let security updates install themselves, read what was installed each week, and reboot when the kernel or a core library changed.
  • Monitor. Logins, new accounts and keys, changed files, new listeners and outbound connections, every day, by something that does not get bored.
  • Audit. Step back from single findings and look at the server as a whole: what is exposed, what is out of date, what has changed since last time.
  • Verify. Re-run the verify commands of the controls you applied; a quarter is a sensible interval for a small team.
  • Review administrative access. Accounts, keys and sudo rules, every quarter and whenever somebody leaves.
  • Review drift. For every change you did not expect, find out who made it and why, then either keep it on purpose or undo it.

How SecAI tracks file and configuration drift

What hardening does not do

Hardening narrows the ways in. It does not close the one most servers are actually compromised through: a vulnerability in the application they run. A perfectly hardened server running a vulnerable plugin is still compromised through the plugin, and the attacker still leaves a web shell, a scheduled task or a key behind.

It also does nothing about valid credentials in the wrong hands. A key stolen from a laptop authenticates correctly and produces a normal login line. What catches both cases is watching behaviour after the login or the exploit: new files in web directories, processes running from temporary directories, new outbound destinations, privilege changes that no administrator session explains.

Linux threat detection

How SecAI fits

SecAI does not harden a server for you, and this guide does not need it. What it adds is the part that people stop doing: checking, every day, that the controls are still in place, and telling you when they are not.

The agent runs a fixed set of hardening checks, SSH password and root login, MySQL and PostgreSQL exposure, and nginx and Apache basics, and reads firewall state, pending updates, sudo grants, exposed services, certificates, time synchronisation and backups as part of each server's posture. Where a check fails and a safe fix exists, SecAI proposes it and waits for approval. An approved fix is applied with a snapshot, validated, and restored automatically if the service does not come back healthy.

Several of the verify commands above are the ones the agent itself runs on every server it monitors: "sshd -T", the simulated apt upgrade, "dnf check-update --security", "needs-restarting -r", "ufw status", "firewall-cmd", "timedatectl" and the list of failed units. They were chosen because they are read-only and mean the same thing on every supported release.

What SecAI does not check today: whether SELinux or AppArmor is enforcing, kernel parameters, the provider's firewall, and whether a backup restores. Those stay on your list.

Run the free, read-only server assessment

Related

Questions people ask

Sources

Every distribution-specific statement and command on this page was checked against the project's own documentation or manual pages, listed below. The read-only verify commands for Debian and Ubuntu were also run on a production Ubuntu 24.04 server. What SecAI does was read from the agent and platform source code. Releases differ in small ways, and newer ones change defaults, Ubuntu 26.04 among them; apply changes on a test server first.

Find out what your server looks like today

The free assessment is read-only and changes nothing. Continuous monitoring starts with a 14-day trial and no card.