Permission Audit
File Permission Security and Special Permissions already showed the individual find commands for spotting world-writable files, unexpected setuid binaries, and files with no known owner. Run once, during a specific investigation, each of those commands is a diagnostic. Run on a schedule, combined with the sudoers and account checks covered elsewhere in this module, and compared against what was true last time, they become something different: a permission audit — a repeatable process for answering "does access on this server still match what's actually needed," rather than a one-off check triggered by a specific incident.
This article doesn't re-teach the individual find invocations — it assembles them, and the sudoers/account checks from earlier in this module, into a single repeatable script, and covers the part none of the individual techniques address on their own: knowing whether a given finding is new, or has been there, unremarked, for two years.
What You Will Learn
By the end of this article you will be able to:
- assemble the individual permission checks scattered across this course into one audit script;
- distinguish a one-off permission check from a repeatable audit process;
- compare an audit's current output against a previous baseline to surface only what's changed;
- decide what to actually do with an audit finding, rather than treating every result as equally urgent;
- schedule a recurring audit and route its output somewhere it will actually get reviewed.
What Belongs in a Permission Audit
A useful audit doesn't reinvent detection techniques — it collects the ones already covered across the course into a single pass:
| Check | Technique | Covered in |
|---|---|---|
| World-writable files and directories | find / -xdev -type f -perm -0002 |
File Permission Security |
| Unexpected setuid/setgid binaries | find / -xdev -perm -4000 -o -perm -2000 |
Special Permissions |
| Files with no valid owner (deleted account left orphaned files) | find / -xdev -nouser -o -nogroup |
Special Permissions |
| Overly broad sudoers grants | Reviewing /etc/sudoers.d/ against active accounts |
sudo Security |
ACLs granting access ls -l doesn't show |
getfacl -R on sensitive trees |
File Permission Security |
| Accounts that can still log in but shouldn't | getent passwd cross-checked against active staff |
This article, below |
Assembling the Checks into One Script
#!/bin/bash
set -euo pipefail
REPORT_DIR="/var/log/permission-audit"
TIMESTAMP=$(date '+%Y%m%d-%H%M%S')
REPORT="${REPORT_DIR}/audit-${TIMESTAMP}.txt"
mkdir -p "$REPORT_DIR"
{
echo "=== Permission Audit: $(date '+%Y-%m-%d %H:%M:%S') ==="
echo -e "\n--- World-writable files (excluding /proc, /sys) ---"
find / -xdev \( -path /proc -o -path /sys \) -prune -o \
-type f -perm -0002 -print 2>/dev/null
echo -e "\n--- setuid/setgid binaries ---"
find / -xdev \( -perm -4000 -o -perm -2000 \) -type f -print 2>/dev/null
echo -e "\n--- Files with no valid owner or group ---"
find / -xdev \( -nouser -o -nogroup \) -print 2>/dev/null
echo -e "\n--- sudoers.d contents ---"
grep -rH '' /etc/sudoers.d/ 2>/dev/null | grep -v '^[^:]*:#'
echo -e "\n--- Accounts with a login shell and UID 0 ---"
awk -F: '($3 == 0) {print $1}' /etc/passwd
echo -e "\n--- Accounts with a valid login shell (candidates to review) ---"
grep -E '/(bash|sh|zsh)$' /etc/passwd | cut -d: -f1
} > "$REPORT"
echo "Report written to $REPORT"
This script deliberately does nothing the individual techniques above haven't already covered in depth — its only job is to run all of them together, on a schedule, and land the combined result in one dated file that can be diffed against the last one.
A full-filesystem find is read-only but not free
Every command above only reads metadata — nothing is modified — but a find / -xdev traversal across a large filesystem takes real time and I/O. -xdev keeps the search within a single filesystem and prevents it from wandering into /proc, network mounts, or other filesystems mounted under the search path, which both speeds it up and avoids reporting on filesystems this specific audit isn't meant to cover. Running it during low-traffic hours (a natural fit for the scheduling covered in cron Basics) avoids competing with production I/O.
Comparing Against a Baseline
The genuinely new capability here — the reason a scheduled audit is worth more than the sum of its individual find commands — is knowing what's changed since last time, rather than staring at the same 40-line list every week and re-deciding it's fine.
diff <(sort /var/log/permission-audit/audit-20260722-020000.txt) \
<(sort /var/log/permission-audit/audit-20260729-020000.txt)
A single new line in a diff — one new setuid binary that wasn't present in last week's report — is far more actionable, and far more likely to actually get investigated, than the same binary sitting unremarked in a 40-line report someone skims and files away. Storing each run's report with a timestamp, as the script above does, is what makes this comparison possible at all; overwriting the same file every run destroys the history needed to tell "new" from "always been there."
comm -13 is a slightly more surgical version of the same idea: it prints only lines present in the second file but not the first — exactly the "what's new" list, without the noise of lines that disappeared or stayed the same.
Deciding What to Do With a Finding
Not every audit result is equally urgent, and treating them all identically either causes alert fatigue (everything gets a low-priority "eventually" label) or unnecessary panic (a totally expected setuid binary like /usr/bin/sudo itself triggers the same alarm as a genuinely suspicious one). A rough triage:
| Finding | Typical urgency | Typical next step |
|---|---|---|
A well-known setuid binary (/usr/bin/sudo, /usr/bin/passwd) |
None — expected | Confirm it's the distribution's own, unmodified binary; ignore |
| A new setuid binary from a package install | Low-to-medium | Confirm the package and version that installed it, check if setuid is actually required for its function |
| A new setuid binary with no known origin | High | Treat as a potential compromise indicator; investigate immediately, don't wait for the next scheduled review |
| A sudoers entry for an account that no longer exists or is disabled | Medium | Remove; see sudo Security |
| A world-writable file inside an application's own working directory | Low-to-medium, context-dependent | Check whether the application genuinely requires it; narrow if not |
| A world-writable file outside any expected application path | High | Investigate immediately |
The distinguishing question for "how urgent is this" is almost always "can I explain, right now, exactly why this exists" — a finding with an immediate, verifiable explanation is a low-priority housekeeping item; one that requires digging just to understand what created it is the one that needs attention first.
Practical Scenario: Building a Weekly Audit Routine
Problem. A team wants a recurring permission audit, with only meaningful changes surfaced — not a wall of unchanged output every week.
Step 1 — install the audit script and confirm it runs cleanly by hand:
Step 2 — schedule it, following the pattern from cron Basics:
Step 3 — add a comparison step that only reports a difference from the previous run:
#!/bin/bash
set -euo pipefail
REPORT_DIR="/var/log/permission-audit"
LATEST=$(ls -1t "${REPORT_DIR}"/audit-*.txt | head -n 1)
PREVIOUS=$(ls -1t "${REPORT_DIR}"/audit-*.txt | sed -n 2p)
if [ -z "$PREVIOUS" ]; then
echo "No previous report to compare against yet."
exit 0
fi
DIFF_OUTPUT=$(diff "$PREVIOUS" "$LATEST" || true)
if [ -n "$DIFF_OUTPUT" ]; then
echo "Changes since $(basename "$PREVIOUS"):"
echo "$DIFF_OUTPUT"
else
echo "No changes since $(basename "$PREVIOUS")."
fi
Verify with two runs a week apart:
Conclusion. The weekly cron job now produces a full historical record, but the diff script is what a human should actually read each week — an empty diff confirms nothing changed and takes seconds to review; a non-empty one points directly at what's new, which is the only thing that actually warrants attention.
Common Mistakes
- Running an audit once and considering the job done. A single snapshot only tells you the state at that moment; the security value comes from comparing snapshots over time and catching drift.
- Overwriting the previous report instead of keeping history. Without a timestamped archive, there's nothing to diff against, and every run degenerates back into "read the whole list and eyeball it."
- Treating every finding as equally urgent. As covered above, this either causes alert fatigue or unnecessary panic; a finding with an immediate, verifiable explanation is not the same as one that requires investigation.
- Running the full-filesystem scan during peak traffic hours. A
find / -xdevtraversal is read-only but not free — scheduling it for a quiet period avoids competing with production I/O. - Auditing files and sudoers, but never cross-checking against active accounts. A world-writable file audit says nothing about a sudoers grant left behind for a departed contractor — both need to be part of the same routine, as shown in the script above.
Exercises
- Adapt the audit script above to also flag any account in
/etc/passwdwith UID 0 other thanroot, and explain why that specific finding deserves immediate attention regardless of any other context. - Run the audit script twice, a day apart, on a lab system where you deliberately create one new setuid file in between. Confirm the diff step correctly flags only that one change.
- For a finding of "a world-writable directory inside
/opt/myapp/uploads," describe what additional information you'd need before deciding whether it's expected or a problem. - Explain why storing audit reports with a timestamp in the filename is a prerequisite for meaningful trend analysis, not just a naming convention.
Summary
A permission audit doesn't require new detection techniques beyond what File Permission Security, Special Permissions, and sudo Security already cover — it requires running them together, on a schedule, and comparing each run against the last one so that a genuinely new finding stands out instead of being buried in an unchanging wall of output. The real value is in the repeatability and the diff, not in any single check. The next article, Security Log Analysis, applies the same "look for what changed or stands out" discipline to authentication and audit logs instead of file metadata.