
Automate a Read-Only Security Audit Report for a Linux Server
Before you run this
This guide builds a single bash script that takes a read-only snapshot of a server's security posture — logged-in users, listening sockets, running services, UID 0 accounts, the effective SSH config, pending updates, SUID/SGID binaries, and world-writable files — and writes it to a timestamped text file. It inspects and records; it never edits config, kills processes, changes permissions, or installs anything.
It needs root to be useful. Several of the things it reads — /var/log/btmp, /etc/shadow, the process owner behind each listening socket, and sshd -T — are only visible to root. Run it with sudo. Read the whole script before you run it, and run it first on a test VM or a non-production host, not straight onto a production box, so you can see exactly what it touches.
Because it is read-only there is nothing to roll back on the system itself. The only artifacts it creates are the report files (which contain sensitive detail, so the script keeps them root-only, mode 600) and, once you schedule it, one cron entry. Undoing the whole thing means deleting the report directory and removing that cron line — both shown at the end.
The one destructive-looking line is the find / scan for SUID and world-writable files. It only reads and prints; it changes no permissions. It can be slow and noisy on a busy box, so the first real run is best done in a quiet window.
What I assume
- Ubuntu 22.04 LTS or Debian 12, bash, systemd, and rsyslog writing to
/var/log/auth.log. - On RHEL/Alma/Rocky the ideas are identical but a few names differ: auth logs live in
/var/log/secure, package queries usednf, and the firewall isfirewalldrather thanufw. I note those inline where they matter. - You have
sudorights on the host.
The script
Save this as /usr/local/bin/security-audit.sh.
#!/usr/bin/env bash
# security-audit.sh - read-only security snapshot of this host.
# Writes a timestamped report and changes nothing on the system.
set -euo pipefail
REPORT_DIR="/var/log/security-audit" # change if you prefer another location
STAMP="$(date +%Y%m%d-%H%M%S)"
REPORT="${REPORT_DIR}/audit-${STAMP}.txt"
umask 077 # report is root-only; it holds sensitive detail
mkdir -p "$REPORT_DIR"
section() { printf '\n===== %s =====\n' "$1" >> "$REPORT"; }
{
echo "Security audit for $(hostname -f 2>/dev/null || hostname)"
echo "Generated: $(date)"
echo "Kernel: $(uname -srmo)"
} > "$REPORT"
section "Uptime and load"
uptime >> "$REPORT"
section "Logged-in users now"
who >> "$REPORT"
section "Last 15 logins"
last -n 15 >> "$REPORT"
section "Last 15 failed login attempts (reads /var/log/btmp)"
lastb -n 15 2>/dev/null >> "$REPORT" || echo "lastb unavailable" >> "$REPORT"
section "Accounts with UID 0 (should be ONLY root)"
awk -F: '($3 == 0) {print $1}' /etc/passwd >> "$REPORT"
section "Accounts with an empty password field"
awk -F: '($2 == "") {print $1}' /etc/shadow >> "$REPORT" || \
echo "could not read /etc/shadow" >> "$REPORT"
section "Listening TCP/UDP sockets"
ss -tulpn >> "$REPORT"
section "Running services"
systemctl list-units --type=service --state=running --no-pager --no-legend >> "$REPORT"
section "Effective SSH server config"
sshd -T 2>/dev/null | sort >> "$REPORT" || echo "sshd -T unavailable" >> "$REPORT"
section "Firewall status"
if command -v ufw >/dev/null; then
ufw status verbose >> "$REPORT"
else
echo "ufw not installed (on RHEL check: firewall-cmd --list-all)" >> "$REPORT"
fi
section "Pending package updates"
# -s = simulate; queries only, installs nothing.
apt-get -s upgrade 2>/dev/null | grep -E '^Inst' >> "$REPORT" \
|| echo "no pending updates, or apt query failed" >> "$REPORT"
section "SUID/SGID binaries"
find / -xdev \( -perm -4000 -o -perm -2000 \) -type f -print 2>/dev/null >> "$REPORT" || true
section "World-writable files (local filesystems, excluding symlinks)"
find / -xdev -type f -perm -0002 ! -type l -print 2>/dev/null >> "$REPORT" || true
chmod 600 "$REPORT"
echo "Report written to $REPORT"
A few notes on the non-obvious lines. umask 077 before the first write means the file is created root-only from the start, not tightened afterwards. apt-get -s is a simulation — it lists what would upgrade and installs nothing. The -xdev flag on find keeps the scan on the local filesystem so it doesn't wander into NFS mounts or /proc. The || true on the two find lines stops set -e aborting the script when find hits a directory it can't read.
Make it executable:
sudo chmod 750 /usr/local/bin/security-audit.sh
Running it
sudo /usr/local/bin/security-audit.sh
It prints the report path when done. Read it with less:
sudo less /var/log/security-audit/audit-*.txt
Reading the report
The value is in a handful of sections:
- UID 0 accounts — anything other than
roothere is a red flag worth investigating immediately. - Empty password field — should be empty output. Any account listed can log in with no password.
- Listening sockets — cross-check against what you expect to be exposed. A service bound to
0.0.0.0that you thought was localhost-only is the kind of thing this report exists to catch. - Effective SSH config —
sshd -Tdumps what the daemon actually resolved, includes and all, so you're reading the real running policy rather than guessing fromsshd_config. Look atpermitrootloginandpasswordauthentication. - Pending updates — a long list means the box is behind on patches.
- SUID/SGID and world-writable files — build a baseline on a known-good host, then diff future reports against it. New entries appearing over time are what you care about.
This script is deliberately a lightweight, dependency-free snapshot. For a deeper, scored audit, Lynis (from CISOfy) is the mature standard tool and also runs read-only; debsecan cross-references installed Debian packages against known CVEs. Both are worth adding once this baseline is in place — I'm not walking through them here.
Scheduling it
Run it weekly from root's crontab:
sudo crontab -e
Add:
# Weekly security snapshot, Mondays at 06:00
0 6 * * 1 /usr/local/bin/security-audit.sh
A systemd service + timer pair is the tidier alternative if you prefer journald logging and OnCalendar scheduling; the cron entry above is the mainstream path and is enough for one weekly job.
If you keep this running for months, the report directory will grow. Prune old files with a standard find retention sweep — for example a second cron line deleting reports older than 90 days — rather than letting them accumulate forever.
Verify it worked
Confirm the run succeeded and produced a well-formed file:
# Non-zero exit means the script aborted partway
sudo /usr/local/bin/security-audit.sh; echo "exit: $?"
# Newest report should be non-empty and mode 600
sudo ls -l /var/log/security-audit/ | tail
Check the file starts with the header block and ends with the world-writable section:
sudo head -n 4 /var/log/security-audit/audit-*.txt
sudo tail -n 5 "$(sudo ls -1t /var/log/security-audit/audit-*.txt | head -1)"
After scheduling, confirm cron actually picked it up:
sudo crontab -l
Undo / cleanup
Nothing on the system was modified, so cleanup is just removing the artifacts:
# Remove the scheduled job: run crontab -e and delete the audit line
sudo crontab -e
# Remove the script and all reports
sudo rm /usr/local/bin/security-audit.sh
sudo rm -rf /var/log/security-audit
That returns the host to exactly where it started.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
Audit World-Writable Files and SUID Binaries with Bash
This script reads your filesystem and reports two classes of risky files: world-writable files and directories (anyone on the box can modify them) and SUID/SGID binaries (they run with the owner's or group's privileges, often root). It writes a timestamped report and, optionally, a baseline you can diff against later. It does not change any permissions, delete anything, or modify a single file — it only runs find and writes a text report to a directory you choose.
Detect and Report Failed SSH Login Attempts with a Log-Parsing Script
This guide builds a read-only bash script that parses your SSH log, counts failed password attempts, and prints a summary of the busiest source IP addresses and the usernames they tried. Its purpose is visibility — spotting brute-force patterns — not blocking. It does not change firewall rules, ban anyone, edit config, or delete anything. Running it and re-running it leaves your system exactly as it was.
Harden a Fresh Ubuntu Server with a First-Boot Bash Script
This script applies a standard first-boot baseline to a fresh Ubuntu server: it creates a non-root sudo user with your SSH key, turns on the UFW firewall (allowing only SSH), disables direct root login and SSH password authentication, enables automatic security updates, and installs fail2ban to throttle SSH brute-forcing. Its purpose is to take a default cloud or VM image from "wide open with a root password" to a sane, keys-only baseline in one pass.
Automate TLS Cipher and Protocol Scanning of Your Own Services
This guide sets up an automated, repeatable scan of the TLS protocols and cipher suites your own services advertise — the sort of thing you'd otherwise do by hand before a PCI review or after a config change. It uses testssl.sh , a widely-used bash script that connects to a service and negotiates handshakes to see what's on offer (TLS 1.0–1.3, weak ciphers, known issues like ROBOT/BEAST reporting, cert details).




