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 upgradeRocky Linux and AlmaLinuxsudo 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 AlmaLinuxsudo dnf check-update --security sudo needs-restarting -rcheck-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 aliceRocky Linux and AlmaLinuxsudo useradd -m alice sudo passwd alice sudo usermod -aG wheel alice - Verify
id alice sudo -l -U aliceThe 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 -tulpnRead 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 enableAllow 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 --reloadThe 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 verboseExpect "Status: active" and "Default: deny (incoming)".Rocky Linux and AlmaLinuxsudo 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 trueRocky Linux and AlmaLinuxsudo systemctl enable --now chronyd chronyc tracking - Verify
timedatectlExpect "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 -tsshd 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 Ubuntusudo systemctl reload sshRocky Linux and AlmaLinuxsudo 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-upgradesRocky Linux and AlmaLinuxsudo dnf install dnf-automatic sudoedit /etc/dnf/automatic.conf sudo systemctl enable --now dnf-automatic.timerIn 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-runExpect 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 AlmaLinuxsystemctl 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.serviceDisable 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.1On 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.PostgreSQLlisten_addresses = 'localhost'This is the default in postgresql.conf; check that nobody changed it to '*'.Redisbind 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 -cAlways 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".ApacheServerTokens Prod ServerSignature OffOn 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/nullSave 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 --flushWith 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 | headMore 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 -VRocky Linux and AlmaLinuxsudo rpm -VaBoth 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 --upgradableRocky Linux and AlmaLinuxsudo dnf updateinfo list --securityLists 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 3306A 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
getenforceExpect "Enforcing".Debian and Ubuntusudo aa-statusExpect 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 sshdZero 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.
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.
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.