A services audit reviews the running daemons, scheduled jobs, listening processes and kernel hardening settings on a Linux server, to verify that only the services you chose are active and that the OS itself is configured to limit the damage when something goes wrong. This page documents the 9 checks in the CtrlOps Services & Processes audit, in the order the audit runs them.
Every threshold below is transcribed from the audit script itself, not from general advice. Where the script passes a value, this page says it passes; where the script warns rather than fails, this page says so and explains why the distinction matters.
Key takeaways
Nine checks cover four layers: what is running, what is listening, what is scheduled, and what the kernel does when something gets compromised. A host that passes all nine has a small, reviewed attack surface with mandatory access control and kernel exploit mitigations in place.
- One check is rated HIGH, suspicious cron jobs. A download piped straight into a shell is the most common persistence mechanism after a compromise, and this check hunts for exactly that pattern.
- Only two need root: mapping external listeners to process names, and reading user crontabs under
/var/spool/cron. The other seven run as any user, which makes this the least privilege-hungry audit in the set. - The whole audit takes about 11 seconds when CtrlOps runs it over your existing SSH connection, against roughly 20 to 25 minutes by hand.
- More than 40 running services is a warning. A single-purpose VPS rarely needs that many, and most of the excess was installed by something else as a dependency.
| # | Check | Severity | What it catches | Root needed |
|---|---|---|---|---|
| 1 | Unneeded services | LOW | cups, avahi, bluetooth, ModemManager on a server | No |
| 2 | Running service count | LOW | More than 40 active systemd services | No |
| 3 | External listener processes | MEDIUM | Root-owned processes on public-facing ports | Yes |
| 4 | Suspicious cron jobs | HIGH | Download-and-execute persistence in crontabs | Yes |
| 5 | Systemd timers | LOW | More than 30 scheduled timers | No |
| 6 | Time synchronisation | LOW | Missing or failing NTP sync | No |
| 7 | Audit daemon | MEDIUM | auditd missing or stopped | No |
| 8 | AppArmor / SELinux | MEDIUM | No mandatory access control active | No |
| 9 | Kernel hardening | MEDIUM | Partial ASLR, unrestricted dmesg, ICMP redirects | No |
Check 1: Are unnecessary services running?
Desktop services such as CUPS (printing), Avahi (mDNS discovery), Bluetooth and ModemManager have no business running on a production VPS. Each adds code that listens for input, parses untrusted data, and carries its own CVE history. They are installed by default on many distributions and enabled automatically, which is exactly why nobody notices them.
The audit asks systemctl is-active about those four names specifically. It is a deliberately short list of things that are never right on a headless server, rather than an attempt to judge your whole service inventory.
How to check manually
systemctl is-active cups avahi-daemon bluetooth ModemManager| Result | When | What it means |
|---|---|---|
| PASS | None of cups, avahi-daemon, bluetooth or ModemManager is active | No commonly unneeded desktop services are running on this host. |
| WARN | One or more of the four is active | The running ones are named. Stop, disable and mask each that you do not use. |
| SKIP | systemctl is not available | This is not a systemd host, so the check has nothing to query. |
How to fix it
# Stop and disable in one step
sudo systemctl disable --now cups avahi-daemon bluetooth ModemManager
# Then mask, so nothing can start them again
sudo systemctl mask cups avahi-daemon bluetooth ModemManager/dev/null so no start succeeds by any route. systemctl unmask reverses it if you ever genuinely need the service.Check 2: How many services are running?
The number of active systemd services is a proxy for attack surface. A fresh Ubuntu server runs roughly 20 to 25. If yours shows 45 or 50, either something installed software you did not choose or a package you did install pulled in daemons you never configured.
The audit counts running .service units and warns above 40. A count on its own could only ever pass, so the threshold is what turns this from inventory into a finding.
How to check manually
systemctl list-units --type=service --state=running --no-legend | grep -c '\.service'| Result | When | What it means |
|---|---|---|
| PASS | 40 or fewer services running | Normal range for a single-purpose host. The exact count is reported, and every one of them is still attack surface worth reviewing. |
| WARN | More than 40 services running | Well beyond what a single-purpose VPS needs. Disable and mask everything unused. |
| SKIP | systemctl is not available | This is not a systemd host. |
How to fix it
# Read the list before changing anything
systemctl list-units --type=service --state=running --no-pager
# Find out what a unit actually does
systemctl cat <service-name>
# Then stop and disable the ones you do not need
sudo systemctl disable --now <service-name>systemd-resolved, dbus, polkit, systemd-journald and systemd-logind are load-bearing, and disabling them breaks the host in ways that are not obvious until the next reboot. Read systemctl cat on anything you do not recognise before you touch it. The count is a leading indicator, not a to-do list.Check 3: What processes listen on external interfaces?
A process listening on 0.0.0.0 or :: accepts connections from the public internet. That is expected for SSH, nginx and your application server. It is not expected for a database, a debug endpoint or a metrics agent that only ever needed loopback.
The audit runs ss -tlnp as root to map every externally listening socket to its owning process, then does the part that matters: it resolves each PID back to its owner and flags any root-owned process outside a small expected set (sshd, systemd, nginx, apache2, httpd, dockerd, containerd). Listing process names alone could only ever pass. A root-owned process on a public socket is the real finding, because a pre-authentication bug in it is instant host root with no privilege escalation step in between.
How to check manually
# Every external listener, with the owning process
sudo ss -tlnp | grep -vE '127\.0\.0\.1:|\[::1\]:'
# Which of those run as root
sudo ss -tlnp | awk 'NR>1' | grep -vE '127\.0\.0\.1:|\[::1\]:' | while read -r line; do
pid=$(printf '%s' "$line" | grep -oE 'pid=[0-9]+' | head -1 | cut -d= -f2)
[ -n "$pid" ] && printf '%s %s\n' "$(sudo stat -c '%U' "/proc/$pid" 2>/dev/null)" \
"$(printf '%s' "$line" | grep -oE '"[^"]+"' | head -1 | tr -d '"')"
done | sort -u| Result | When | What it means |
|---|---|---|
| PASS | External listeners exist with no unexpected root-owned process, or nothing is listening externally at all | The listening process names are reported. Nothing runs as root beyond the expected service set. |
| WARN | A root-owned process outside the expected set is listening externally | The process names are listed. A pre-auth bug in any of them is instant host root. Drop privileges or bind to 127.0.0.1. |
| SKIP | Root was unavailable, or ss is not installed | Port-to-process mapping needs both. Re-run with sudo, or install iproute2. |
Note what a PASS with nothing listening means: it is reported as a pass because there is no exposure, but on a server that is supposed to be serving something, it is worth a second look. An empty listener list on a web host means the web server is not running.
How to fix it
# Bind to loopback in the service config (Redis example)
# /etc/redis/redis.conf
bind 127.0.0.1 ::1
# Or keep it reachable but drop it off uid 0
# /etc/systemd/system/myapp.service
[Service]
User=appuser
Group=appgroup
sudo systemctl daemon-reload
sudo systemctl restart redis myapp0.0.0.0. Binding to loopback is a one-line change that removes the entire attack class, and it composes with the firewall and Docker port checks rather than duplicating them: this check tells you which process and which user, that one tells you whether the firewall or a container is what exposed it.Check 4: Are there suspicious cron jobs?
Download-and-execute is the most common persistence mechanism after a server compromise, and the shape is always the same: curl or wget piped directly into bash, sh, dash or zsh. A legitimate job downloads a file and then runs it as a separate step. A malicious one pipes, because piping keeps the payload off disk where a file scanner could find it.
The audit reads /etc/crontab and /etc/cron.d, the four periodic directories, and, with root, the user crontabs under /var/spool/cron. It matches the pipe specifically. An earlier version accepted && and ; as well, which flagged ordinary deployment hooks like curl -f ... && systemctl reload nginx as persistence, so the pattern was narrowed to the thing that actually distinguishes a backdoor.
How to check manually
# System crontabs and periodic directories (no root needed, these are 0644/0755)
grep -rhE '(curl|wget)[^|]*\|[[:space:]]*(/bin/|/usr/bin/)?(ba|da|z|k)?sh([[:space:]]|$)' \
/etc/crontab /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly \
2>/dev/null | grep -v '^[[:space:]]*#'
# User crontabs are mode 1730 and do need root
sudo grep -rhE '(curl|wget)[^|]*\|[[:space:]]*(/bin/|/usr/bin/)?(ba|da|z|k)?sh([[:space:]]|$)' \
/var/spool/cron /var/spool/cron/crontabs 2>/dev/null | grep -v '^[[:space:]]*#'
# Total cron entries, excluding comments and environment assignments
grep -rh '^[^#]' /etc/crontab /etc/cron.d 2>/dev/null \
| grep -vE '^(SHELL|PATH|MAILTO|HOME)=' | grep -cE '^[0-9*@]'| Result | When | What it means |
|---|---|---|
| PASS | No download-and-execute pattern was found in any readable crontab | The total cron entry count is reported. No pipe-to-shell persistence detected, which is not the same as no backdoor. Review each entry anyway. |
| FAIL | The pattern matched anywhere | The first matching line is shown, truncated. Classic persistence. Treat the host as compromised and investigate before you delete anything. |
Two details about how the result reads. There is no SKIP row: without root the check still runs across the system crontabs and reports its verdict with a "system tabs only" note attached, because a backdoor in /etc/cron.d is worth finding even from an unprivileged session. And on a FAIL it reports only the first match, shortened to 90 characters, which is enough to recognise the pattern and not enough to be the whole investigation.
How to fix it
# Read a user crontab
sudo crontab -l -u <username>
# Edit it to remove the entry
sudo crontab -e -u <username>
# What was touched in the last week?
sudo find /etc/cron.d /var/spool/cron -type f -mtime -7 -lsCheck 5: How many systemd timers are scheduled?
Systemd timers are the modern replacement for cron, and they are just as good a persistence mechanism. A stock server carries roughly 5 to 10, for log rotation, man-db updates, fstrim and similar. Past 30, something created timers nobody reviewed.
The audit counts every timer, active and inactive, via systemctl list-timers --all. Inactive ones count deliberately: a timer that is scheduled but not currently running is still scheduled.
How to check manually
systemctl list-timers --all --no-legend | grep -c '\.timer'| Result | When | What it means |
|---|---|---|
| PASS | 30 or fewer timers scheduled | Normal range. The count is reported. Investigate any name you do not recognise. |
| WARN | More than 30 timers scheduled | More than a stock install carries. Check systemctl list-timers for anything you did not create. |
| SKIP | systemctl is not available | This is not a systemd host. |
How to fix it
# Every timer, with the unit it triggers
systemctl list-timers --all --no-pager
# What does this one actually run?
systemctl cat <timer-name>.timer
# Turn one off
sudo systemctl disable --now <timer-name>.timer/etc/systemd/system, /usr/lib/systemd/system and /run/systemd/system, so there is no single file to read. The count is the cheap sanity check: if it moved since your last review and you did not install anything, find out what created the difference.Check 6: Is the clock synchronized?
Every timestamp in every log this server writes depends on the system clock. When it drifts, log correlation across hosts stops working, incident timelines become guesses, and TLS handshakes can fail outright when the server time falls outside a certificate validity window. NTP sync is free and needs no attention once enabled.
The audit looks for an active time service among chronyd, chrony, systemd-timesyncd, ntpd, ntpsec and openntpd, falls back to checking for a running chronyd or ntpd process by name, and then reads timedatectl show -p NTPSynchronized.
How to check manually
# Is any sync service running?
systemctl is-active chronyd chrony systemd-timesyncd ntpd ntpsec openntpd 2>/dev/null
# Does the system consider the clock synchronised?
timedatectl show -p NTPSynchronized --value| Result | When | What it means |
|---|---|---|
| PASS | A time service is running and the clock is not reported as unsynchronised | The service providing sync is named in the result. Timestamps can be trusted. |
| WARN | No time service was found and timedatectl returned nothing | Nothing is keeping this clock accurate. Enable systemd-timesyncd or install chrony. |
| WARN | NTPSynchronized reports no | A service is present but is not reaching its upstream servers. Check egress rules for UDP 123. |
There is no SKIP here, and the PASS is slightly weaker than it looks: the check passes when a service is running and the clock is not explicitly reported as unsynchronised, which includes hosts where timedatectl is unavailable to answer either way. On those, treat the pass as "a sync service is running" rather than "the clock is provably correct".
How to fix it
# Simplest: use the built-in
sudo timedatectl set-ntp true
# Or install chrony for more control over sources
sudo apt install chrony # Debian/Ubuntu
sudo dnf install chrony # RHEL/Rocky/Alma
sudo systemctl enable --now chronydCheck 7: Is auditd running?
Application logs record what your software did. auditd records what the operating system did: syscalls, file access, privilege changes, module loads. Without it, a compromise leaves no trail beyond whatever the application chose to write down, which is rarely the part you need. Most distributions ship it and do not enable it.
The audit checks for the binary, then whether the service is running, treating "not installed" and "installed but stopped" as different findings.
How to check manually
# Installed?
command -v auditctl
# Running?
systemctl is-active auditd| Result | When | What it means |
|---|---|---|
| PASS | auditd is installed and running | A syscall-level trail is being recorded. Keep a rule set loaded, because an idle auditd records almost nothing. |
| FAIL | auditd is installed but not running | It crashed or was never enabled. Run systemctl enable --now auditd. |
| WARN | auditd is not installed at all | No syscall-level recording. A compromise leaves no forensic trail beyond application logs. |
The severity ordering here is deliberate and worth noticing: installed-but-stopped fails while not-installed only warns. A host that never had auditd is a decision. A host where auditd is installed and dead is a promise that is not being kept, and somebody probably believes it is running.
How to fix it
# Install
sudo apt install auditd # Debian/Ubuntu
sudo dnf install audit # RHEL/Rocky/Alma
sudo systemctl enable --now auditd
# Give it rules, or it watches almost nothing
sudo cp /usr/share/audit/sample-rules/30-stig.rules /etc/audit/rules.d/
sudo augenrules --loadCheck 8: Is AppArmor or SELinux enforcing?
Unix permissions say who may read or write a file. Mandatory access control adds a second, independent layer: even a process running as root can only touch what its policy allows. Without it, a compromised nginx worker can read /etc/shadow, write anywhere and execute anything. With it enforcing, the same compromised process is stuck inside its declared profile.
The audit checks SELinux first via getenforce, then falls back to AppArmor by looking for /sys/kernel/security/apparmor.
How to check manually
# SELinux
getenforce
# AppArmor: is it present, and how many profiles are loaded?
ls /sys/kernel/security/apparmor/policy/profiles 2>/dev/null | wc -l
sudo aa-status| Result | When | What it means |
|---|---|---|
| PASS | SELinux reports Enforcing, or AppArmor is present on the host | For AppArmor the loaded profile count is reported. Confirm your network-facing services are actually among the confined ones. |
| WARN | SELinux is present but reports Permissive or Disabled | The current mode is named. A service exploit is bounded only by unix permissions. Set it to enforcing. |
| WARN | Neither AppArmor nor SELinux is active | Nothing confines a compromised service beyond file permissions. Install and enable one. |
Read the AppArmor pass carefully. It triggers on AppArmor being present, and reports the profile count as information rather than as a condition. An Ubuntu host with AppArmor loaded and zero meaningful profiles passes this check. That is the honest limit of a cheap read-only probe, and it is why the result text tells you to confirm your network-facing services are confined: run sudo aa-status and look for nginx, your app server and your database in the enforce list, not just a non-zero number.
How to fix it
# Debian/Ubuntu: usually pre-installed
sudo systemctl enable --now apparmor
sudo aa-status
# RHEL/Rocky/Alma
sudo setenforce 1
sudo sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/configaa-complain for AppArmor and setenforce 0 for SELinux. Run for a week, review denials with ausearch -m AVC, fix them, and only then enforce.Check 9: Are kernel hardening settings applied?
Four kernel parameters decide whether basic exploit mitigations are on. Full ASLR randomises memory layout so an overflow cannot jump to a predictable address. dmesg_restrict stops unprivileged users reading kernel ring buffer messages that leak those addresses. Reverse-path filtering drops spoofed source addresses. Refusing ICMP redirects prevents routing table manipulation.
The audit reads all four straight out of /proc/sys and warns if any is below baseline.
How to check manually
cat /proc/sys/kernel/randomize_va_space # want exactly 2
cat /proc/sys/kernel/dmesg_restrict # want exactly 1
cat /proc/sys/net/ipv4/conf/all/rp_filter # anything but 0
cat /proc/sys/net/ipv4/conf/all/accept_redirects # anything but 1| Result | When | What it means |
|---|---|---|
| PASS | ASLR is full, dmesg is restricted, reverse-path filtering is on and ICMP redirects are refused | All four checked mitigations are at or above baseline. |
| WARN | One or more settings are below baseline | The specific parameter names are listed. Set them in /etc/sysctl.d and apply with sysctl --system. |
| SKIP | /proc/sys could not be read | Kernel settings are not inspectable here, which usually means a container rather than a host. |
The four are not tested identically, and the asymmetry is intentional. ASLR and dmesg_restrict must equal their target value, so a parameter that cannot be read counts against you: those two exist on every Linux kernel, and an unreadable one is a problem in itself. Reverse-path filtering and ICMP redirects are only flagged on the exact bad value (0 and 1 respectively), so a missing parameter does not count against you and rp_filter=2, the looser but still valid mode used on asymmetrically routed hosts, passes rather than warning.
How to fix it
sudo tee /etc/sysctl.d/99-hardening.conf > /dev/null << 'EOF'
kernel.randomize_va_space = 2
kernel.dmesg_restrict = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
EOF
# Apply now, persists across reboots
sudo sysctl --systemnet.ipv4.ip_forward = 1 is required for container networking, and the audit never counts it as a finding. When it sees ip_forward enabled on a host with Docker installed, it appends a note saying so to the result, on a pass as well as a warning, so nobody spends an afternoon "fixing" a setting that has to stay exactly where it is.What this audit does not cover
Being explicit about scope is part of the point of publishing thresholds. This audit reads what is running, what is listening, what is scheduled and how the kernel is configured, on the host itself.
- Container internals. Services and kernel settings are read from the host. What runs inside Docker containers, Kubernetes pods or LXC instances is invisible here, and needs Falco, Sysdig or Trivy instead.
- Application vulnerabilities. A service can pass all nine checks and still carry a SQL injection or an authentication bypass. That is SAST and DAST territory, a separate discipline.
- File integrity. Check 4 finds one specific cron pattern. It does not tell you whether a binary changed since last week. AIDE, Tripwire or OSSEC cover that, and they are what catches the backdoor this check is blind to.
- Network traffic. Knowing what listens is not knowing what talks. An IDS such as Suricata or Snort watches the traffic itself; check 3 only reads the socket table.
- Behavioural analytics. auditd records syscalls; turning those into "this account did something unusual" needs a SIEM on top.
- Whether auditd or MAC is configured usefully. Checks 7 and 8 confirm the mechanism is present and running. Neither can tell you the rule set or the profile set is worth anything, and both say so in their result text.
The manual audit problem at scale
Running all 9 checks by hand takes roughly 20 to 25 minutes per server: listing and reading services, parsing ss output and resolving PIDs to owners, grepping four cron locations plus the user tabs, counting timers, checking sync and auditd, then reading MAC status and four sysctls. It is the longest audit in the VPS set, and most of the time goes on the two checks that need correlation rather than a single command.
| Task | Manual, per server | CtrlOps |
|---|---|---|
| List and count services | About 3 min | Included |
| Map external listeners to owners | About 3 min | Included |
| Grep every crontab for persistence | About 4 min | Included |
| Count and review systemd timers | About 2 min | Included |
| Check time sync and auditd | About 3 min | Included |
| Read MAC status and sysctls | About 5 min | Included |
| Total | About 20 to 25 min | About 11 seconds |
| Scored report and PDF export | Not available | One click |
Multiply by a fleet and the arithmetic stops working. Eight servers is three hours; twenty is most of a week of afternoons.
Four of the nine checks are counts, and a count only means something next to the previous one. 34 running services is fine. 34 when it was 26 last month means something installed eight daemons and nobody noticed. The same is true of the timer count and the cron entry count: read once they are trivia, read in sequence they are the earliest warning you get that a host changed underneath you. That is the server management drift problem in its purest form, and it is the thing a manual audit is structurally worst at, because nobody keeps last month's numbers in a spreadsheet.
How CtrlOps runs all 9 checks in one click
Instead of running these commands on every server by hand, CtrlOps runs the whole Services & Processes audit over the SSH connection you already have open. Nothing is installed on the server: no agent, no daemon, no package, no new credentials. The checks are read-only POSIX shell that executes, streams its results back, and leaves nothing behind.
Watch it run
The recording below walks through the SSH & Access audit rather than this one, but the flow is identical for every audit in the catalog: pick it, watch the checks stream in, read the score, send the failures to the AI Terminal. The written steps that follow cover the same ground if you would rather read than watch.
Step 1: Open the Audit tab
Connect to your server in CtrlOps and open the Audit tab in the left sidebar.
Step 2: Select Services & Processes
Choose the Services & Processes audit from the catalog. All 9 checks are listed with their descriptions, severity levels and the estimated run time (about 11 seconds in total). Toggle off any check you want to skip.
Step 3: Run it
The audit runs over your existing SSH session. Results stream in as they complete, so you watch each check pass, warn or fail rather than waiting on a final report.
Step 4: Read the report
You get a summary containing:
- A hardening score out of 100
- A severity breakdown: how many HIGH, MEDIUM and LOW findings
- Pass, warning, failed and skipped counts
- A findings table you can sort and filter
Step 5: Fix what failed
Select the failed findings and send them to the AI Terminal. Each proposed fix command appears with an explanation of what it changes. Nothing runs until you review and approve it.
Step 6: Re-run and compare
Run the same audit again after applying the fixes and watch the hardening score move. On this audit the stored history earns its keep twice over, because four of the nine checks are counts and a count is only meaningful against the last one.
Credentials stay on your machine, stored locally, never synced to the cloud. Every command the AI Terminal proposes is shown before execution and gated behind your approval, never auto-run.
Conclusion
Auditing what runs, what listens, what is scheduled and how the kernel is configured is how you keep a server's attack surface small enough to reason about. Nine checks is a lot of ground, but only one of them is an emergency: a download-and-execute line in cron means the host is already compromised, and the other eight are the layers that decide how much that costs you.
Checking all nine by hand takes 20 to 25 minutes per server and produces numbers you will not remember next month. The CtrlOps Security Audit runs the set in about 11 seconds per host, agentlessly, and keeps the counts so the trend is visible without anyone maintaining a spreadsheet.
The neighbouring checklists cover the layers around this one: SSH and access control, the firewall and network perimeter, file system permissions, logging and monitoring and the application and database layer.
Frequently asked questions
A services audit reviews every running daemon, listening process, scheduled job and kernel hardening setting on a Linux server, to verify that only intentionally enabled software is active. The goal is to shrink attack surface by finding unneeded services, root-owned listeners on public ports, suspicious cron entries, and missing defences such as auditd, AppArmor or SELinux and the kernel exploit mitigations.
Run systemctl list-units --type=service --state=running. A fresh Ubuntu server typically runs 20 to 25 services, and this audit warns above 40. Read systemctl cat <name> on anything you do not recognise before disabling it: several unfamiliar names, systemd-resolved, dbus and polkit among them, are load-bearing and break the host if removed.
It is a cron entry that pipes the output of curl or wget straight into a shell. The payload never touches disk, so a file scanner never sees it, which is why it is the most common persistence mechanism left behind after a compromise. The audit matches the pipe specifically and deliberately ignores curl ... && something, because downloading a file and then acting on it is ordinary deployment behaviour rather than a backdoor.
Because only one of them is evidence that something already happened. A download-and-execute cron entry is not a weakness that could be exploited, it is a backdoor that was installed, so it is rated HIGH. The MEDIUM checks are absent defence layers, which change how bad a future compromise gets rather than indicating a present one. The LOW checks are counts and inventory, worth reviewing rather than fixing tonight.
Run sudo ss -tlnp and filter out loopback addresses. That gives you every socket accepting external connections along with the owning PID and process name. The part worth the effort is the next step: resolve each PID with sudo stat -c '%U' /proc/<pid> to find which run as root. A root-owned process on a public port means a pre-authentication bug in it is instant host root, with no privilege escalation needed.
Only two of the nine checks do: mapping external listeners to process names, which needs root to read /proc/<pid> for processes you do not own, and reading user crontabs under /var/spool/cron, which are mode 1730. The cron check does not skip without root; it scans the system crontabs anyway and labels the result accordingly, because a backdoor in /etc/cron.d is worth finding either way.
It is the only durable record of what happened at the OS level, and application logs cannot substitute for it: they record what your software chose to write down, not which files were read or which privileges changed. Install it, enable it, and load a rule set. An idle auditd with no rules records almost nothing, so a passing check here means the daemon is up, not that it is watching anything useful.
Yes, and use whichever your distribution ships: AppArmor on Ubuntu and Debian, SELinux on RHEL, Rocky and Alma. Both confine a service to a declared policy, so a compromised process cannot reach resources outside its profile even when it runs as root. Move to enforcing gradually rather than in one step, using aa-complain or setenforce 0 to log denials for a week first, because going straight to enforcing on a live host can lock you out.
Four form the baseline: kernel.randomize_va_space at 2 for full ASLR, kernel.dmesg_restrict at 1 to block unprivileged kernel log reads, net.ipv4.conf.all.rp_filter at anything other than 0 for reverse-path filtering, and net.ipv4.conf.all.accept_redirects at anything other than 1 to refuse ICMP redirects. Set them in /etc/sysctl.d/99-hardening.conf and apply with sysctl --system. Leave net.ipv4.ip_forward alone on any host running Docker, where 1 is correct and required.
After every software install, configuration change and OS update, because those are exactly what enable services and timers you did not ask for. Monthly is a reasonable floor otherwise. Four of the nine checks are counts, and counts only mean something in sequence, so the value compounds with regularity in a way a single thorough run cannot match.
It reads the host operating system, so it has limited value if your workloads run entirely in Kubernetes or containers with no meaningful host surface. It also does not cover container internals (use Falco or Trivy), application vulnerabilities (SAST and DAST), or network traffic analysis (Suricata or Snort). It is for servers you administer over SSH, where the services, cron and kernel settings are yours to change.
Yes. All nine checks work on any Linux host reachable over SSH: EC2, DigitalOcean Droplets, Linode, Vultr, Hetzner, bare metal or any other VPS. Three of them depend on systemd and report SKIP on a non-systemd init, and check 9 reports SKIP inside a container where /proc/sys is not readable, which is the correct answer rather than a failure.