Verify a state, do not rebuild it
Initial VPS hardening happens once, at delivery: SSH keys, firewall, fail2ban, automatic updates. Our hardening checklist already covers it. Here we assume all of that exists and ask whether it still holds. A package left pending, an account created for a contractor and never removed, a port opened for a test, a certificate whose renewal fails silently: none of it shows unless you look. The monthly audit is therefore a verification routine, not a configuration one. Each check produces an output that you archive, and a green, orange or red status in the client's report. A deviation is recorded, never fixed without agreement.
Set the scope before the first audit
Before the first command, write down who is responsible for what. Does the VPS belong to the client or to the agency? Who holds the root account, who receives alerts, who decides on a reboot? Without answers, the first deviation you find becomes a dispute: was the agency allowed to fix it, had the client been told? Take five points into your contract or order form: the VPS covered, the audit frequency, the read-only access granted, the response time for a red deviation, and the list of actions the agency may take alone, such as applying a security update, as opposed to those that need client approval, such as a reboot or an account removal.
To prepare once, before auditing
- A dedicated audit account — a named user with
sudolimited to reading logs and inspecting the network, rather than a shared root. - An expected list per VPS — ports that should listen, authorized accounts, domains served, backup tool and repository. Without a reference, a deviation cannot be told apart from normal behaviour.
- A read-only collection script — it chains the commands of the checklist and writes everything to a dated file, so each command is typed only once.
- An archive folder per client — one subfolder per month, kept outside the audited VPS, so months can be compared.
- A report template — the same layout for every client, filled in from the collected outputs.
- A calendar — a fixed day of the month per client, noted in the team agenda, otherwise the audit is skipped in the first busy month.
The seven checks, with their time budget
Pending updates (6 min)
sudo apt updateapt list --upgradable 2>/dev/nullapt list --upgradable 2>/dev/null | grep -i security
Isolate packages whose origin ends in-security: they go first. The others go through human validation, because a major version jump can break an application. On AlmaLinux or RHEL,sudo dnf check-update --securityplays the same role. Even with automatic updates enabled, this check shows what remains blocked.Kernel and pending reboot (4 min)
uname -rls /boot/vmlinuz-*ls /var/run/reboot-requiredsudo needrestart -b -r l
If the newest installed kernel differs fromuname -r, or if/var/run/reboot-requiredexists, a reboot is due. In the needrestart output, theNEEDRESTART-KSTAline tells whether the running kernel is current, and the list shows services still using old libraries. The reboot is never done alone: it is scheduled with the client.Accounts and SSH keys (8 min)
awk -F: '$3>=1000' /etc/passwdgetent group sudosudo ssh-keygen -lf /home/<account>/.ssh/authorized_keyslastlog | grep -v 'Never logged in'last -n 20
Every human account must match a person known to the client or the agency. Every key fingerprint must belong to someone still working on the project. An account that has not logged in for months, or an unknown key, turns orange and goes to the client for validation.Open ports (5 min)
sudo ss -tlnpsudo ss -ulnpsudo ufw status verbose
Compare the result with the list expected for this VPS. A database listening on all interfaces instead of the loopback, a forgotten test port, an unknown service: each deviation is recorded with the process that opens it, without closing it immediately, because a service may depend on that port.Certificates (4 min)
openssl s_client -connect your-domain.com:443 -servername your-domain.com </dev/null 2>/dev/null | openssl x509 -noout -enddate
Read the expiry date on the service actually exposed, not just the presence of a renewal timer: an active timer can fail every night. If the domain goes through a CDN, this command reads the CDN certificate. For the origin, replace the host in-connectwith the VPS IP address and keep-servername.Restored backups (10 min)
restic snapshotsrestic restore latest --target /tmp/restore-test --include /etc/nginx
Note the date of the latest restore point, then actually restore a file or a database into a temporary directory and open it. A backup that was never restored is not proof, it is a hypothesis. Delete the temporary directory afterwards. If the client uses another tool, the logic is the same: list the points, partial restore, check the content.Authentication logs (8 min)
sudo journalctl -u ssh --since '30 days ago' --no-pager | grep -c 'Failed password'sudo journalctl -u ssh --since '30 days ago' --no-pager | grep 'Accepted'sudo journalctl _COMM=sudo --since '30 days ago' --no-pager
Count the failures, list the successful logins and look for those from an unknown address. Reviewsudousage. On RHEL and derivatives, the unit is calledsshd, notssh.
The 45-minute budget per VPS
Scroll the table
| Check | Key command | Time |
|---|---|---|
| Pending updates | `apt list --upgradable` | 6 min |
| Kernel and reboot | `needrestart -b -r l` | 4 min |
| Accounts and SSH keys | `ssh-keygen -lf authorized_keys` | 8 min |
| Open ports | `ss -tlnp` | 5 min |
| Certificates | `openssl s_client` | 4 min |
| Restored backups | `restic restore` | 10 min |
| Authentication logs | `journalctl -u ssh` | 8 min |
A collection script instead of seven manual sessions
Most of the 45 minutes is reading, not typing, provided you do not retype the commands. Write an audit-vps script that runs the commands of checks one to seven and redirects each output to a file named after the client and the date, for example ~/audits/client-a/YYYY-MM/raw-output.txt. It contains only read commands: no apt upgrade, no systemctl restart, no rm. Run it over SSH from your workstation, fetch the file, then read it calmly. The backup restore stays the only manual step, because it requires judging whether the restored content is consistent.
Archive and compare: that is where drift shows
A single audit gives a snapshot, two consecutive audits give a trend. Keep the raw output of each month, then compare it with the previous one using diff -u previous-audit.txt current-audit.txt. A port that appears, an extra account, a number of failed logins multiplied by ten, a restore point that no longer moves: all these signals stand out in a few lines of difference, while they are invisible in an output read alone. Also archive the report handed to the client. The day the client asks since when an account existed or when a fix was applied, the answer is a dated file, not a memory.
The one-page report handed to the client
The header
Client name, VPS identifier, IP address, audit date, name of the person who ran it.
The table of the seven checks
One line per check with a status: green if nothing to report, orange if a deviation needs a client decision, red if urgent action is needed.
The deviations found
Each deviation in one factual sentence, with the evidence: the output line, the port, the account concerned.
The actions taken
What the agency applied alone, within the agreed scope, for example a security update.
The actions for the client to approve
Reboot, account removal, closing a port, with the risk of doing nothing.
The next due date
The date of the next audit and, for each orange or red deviation, a deadline for handling it.
Record the deviation, do not fix it on the spot
The reflex to close the port or delete the suspicious account during the audit is the most expensive one: a client service goes down, and the agency decided it alone. The role of the audit is to make the deviation visible and dated. The fix follows the approval path agreed at the start, even for what looks obvious.
When an output does not say what it seems to say
needrestartnot found — the package is not installed by default everywhere. Install it once with the client's agreement, or compareuname -rand the kernels in/bootby hand.- No SSH failures in the logs — check the unit name before concluding. Depending on the distribution, authentication is logged under
ssh,sshdor, on recent OpenSSH versions,sshd-session. Trysudo journalctl _COMM=sshd-session --since '30 days ago'. ss -tlnpdoes not show processes — withoutsudo, the process column stays empty for other users' services.- A valid certificate while the site shows an error — you read the one from a CDN or another vhost. Point at the VPS address and pass the right
-servername. resticasks for a password — the repository is encrypted. Get the secret from the client before the audit, not during it, otherwise the check is skipped.
The time objection: 45 minutes per client, every month
Twenty clients make fifteen hours a month, and the objection is fair. Three levers reduce it. First, time is measured per VPS, not per client: a client with a single server costs 45 minutes, and less from the second month, because the expected references are in place. Second, the script collects everything, you only read, and the report is filled in from a template. Finally, compare with the cost of an undetected incident: a forgotten access or an unusable backup is discovered on the day of the outage, with an unhappy client and a recovery bill. The routine costs less than a single avoidable incident.
When the portfolio grows
At ten VPS, the checklist fits in a day. At fifty, the difficulty becomes organisational: knowing which client has what, which due dates fall this month, who has been audited. The ServOrbit.com agency space gathers each client's services and due dates in a single interface, which avoids maintaining a parallel spreadsheet. If the agency prefers not to carry the system base itself, the VPS administration option exists and is looked at separately. This guide is provided as is, without support: running the audit and the responsibility towards the client remain yours.
Going further
- Set up the base — /blog/linux-hardening-vps-checklist describes the initial hardening that this audit verifies.
- Automate fixes — /blog/securite-vps-mises-a-jour-automatiques-debian-ubuntu explains unattended-upgrades, whose leftovers the first check measures.
- Back up properly — /blog/backup-vps-restic-s3-3-2-1 covers setting up Restic, whose restore the sixth check verifies.
- Monitor continuously — /blog/alertes-disque-ram-vps-monitoring-proactif complements the monthly audit with alerts between two passes.
- Inventory PHP versions — /blog/inventaire-versions-php-parc-sites-clients-agence applies the same discipline to the site fleet.
- Respond to a vulnerability — the fix guides in the Security and monitoring category of the blog show what an up-to-date inventory lets you do on day one.
The official documentation
apt options: https://manpages.debian.org/stable/apt/apt.8.en.html
needrestart project: https://github.com/liske/needrestart
Automatic updates on Debian: https://wiki.debian.org/UnattendedUpgrades
ss command: https://man7.org/linux/man-pages/man8/ss.8.html
openssl s_client command: https://docs.openssl.org/master/man1/openssl-s_client/
journalctl command: https://www.freedesktop.org/software/systemd/man/latest/journalctl.html
sshd daemon: https://man.openbsd.org/sshd.8
last command: https://man7.org/linux/man-pages/man1/last.1.html