100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Linux & Shell Scripting
50 minbeginner

Files Practice — Server File Audit

What You'll Build

In this exercise you will build `cricket_file_audit.sh` — a comprehensive file system audit script that a security engineer or DevOps engineer can run on a CricketPulse server to verify that file ownership, permissions, and structure match the expected deployment baseline. The script combines file operations, I/O redirection, links, wildcards, find, and permissions into a single production-quality tool.

The audit covers four areas: verifying that all required FHS-correct directories exist with correct ownership, checking that sensitive configuration files have restrictive permissions, identifying any world-readable secrets or world-writable application files, and confirming that the release symlink points to a valid versioned directory. Each check produces a structured pass/fail result that can be consumed by a CI/CD pipeline.

Real security audits at organisations like Netflix, Stripe, and Cloudflare run exactly this kind of automated filesystem check as part of their deployment verification pipelines. Building one from scratch cements the permission model, find syntax, and symlink mechanics covered in this module into practical, reusable knowledge that applies directly to every server you will ever provision or deploy to.

Analogy🏏Cricket
🏏 Think of it like cricket: This script is like the pre-match ground inspection conducted by the match referee, pitch curator, and captains before a Test match begins. Before Rohit Sharma and the opposition captain walk out for the toss, the curator has measured pitch moisture, taken grass length readings, and documented the surface condition — creating a baseline against which any afternoon deterioration can be measured.Just as this structured inspection prevents surprises and creates a documented record, the inventory script creates a documented baseline for a server against which future anomalies can be compared. Just as a ground inspection without a checklist might miss a drainage issue that affects the afternoon session, a server assessment without a structured script might miss a nearly-full disk that causes a midnight deployment failure.The insight is that the value of a structured inspection is not just the current findings but the reproducible method — the same script run tomorrow highlights exactly what changed.

Prerequisites

  • Completion of Lessons 05–07: file operations and I/O redirection, links/wildcards/find, and the full permission model.
  • A Linux system where you have sudo access — the audit checks system directories that require elevated privileges to inspect fully.
  • The CricketPulse directory structure from previous exercises or a simulated version created using the setup commands in this lesson.
  • Familiarity with exit codes — the audit script uses non-zero exit codes to signal failures to a calling CI/CD pipeline.
  • Basic understanding of bash arrays and loops — the audit uses arrays to define the expected state and loops to check against it.

Setup & Project Structure

Before writing the audit script, set up the directory structure that the script will audit. This creates a realistic target environment with both correct and intentionally misconfigured permissions, allowing you to verify that the audit script correctly identifies both passing and failing checks. Run the setup as root or with sudo — creating system directories and service users requires elevated privileges.

Analogy🏏Cricket
🏏 Think of it like cricket: building `cricket_inventory.sh` incrementally across four steps is like a batter building an innings session by session rather than swinging for six off the first ball. Just as you only need a bat and a net to start practising — no fancy equipment — this exercise needs only a text editor and a shell, nothing installed. Just as a batter grooves one shot at a time, the cover drive before the pull, each step adds exactly one report section and introduces only the commands needed to gather that piece of information. Just as marking your guard and taking centre sets a repeatable stance, creating a dedicated exercise directory and making the file executable gives you a clean, runnable base to return to. And just as a solid start makes the big total possible, getting the script structure right first means the later sections drop in without collapse. The payoff: incremental practice builds a working system-inventory script the same disciplined way a batter builds a match-winning score.

The setup creates the full CricketPulse directory structure from Module 2's permission model: a system service user, the FHS-correct directories with appropriate ownership, placeholder configuration and log files, the versioned release structure with a `current` symlink, and two intentional misconfigurations for the audit script to detect — a world-readable secret file and a world-writable application directory.

bash
#!/bin/bash
# setup_audit_target.sh — creates CricketPulse structure to audit
set -euo pipefail

# Create service user if it does not exist
id cricketpulse &>/dev/null || \
    useradd --system --no-create-home --shell /usr/sbin/nologin cricketpulse

# Create directory structure
mkdir -p /etc/cricketpulse
mkdir -p /var/log/cricketpulse
mkdir -p /opt/cricketpulse/releases/v2.1.0
mkdir -p /opt/cricketpulse/bin

# Set correct permissions
chown -R cricketpulse:cricketpulse /opt/cricketpulse /var/log/cricketpulse
chown root:cricketpulse /etc/cricketpulse
chmod 750 /etc/cricketpulse /var/log/cricketpulse
chmod 2775 /opt/cricketpulse/releases

# Create config files with correct permissions
echo 'app: cricketpulse' > /etc/cricketpulse/config.yaml
chmod 640 /etc/cricketpulse/config.yaml
chown root:cricketpulse /etc/cricketpulse/config.yaml

# ---- Intentional misconfigurations for the audit to detect ----
echo 'db_password=secret123' > /etc/cricketpulse/db.key
chmod 644 /etc/cricketpulse/db.key    # ❌ World-readable secret
chmod 777 /opt/cricketpulse/bin       # ❌ World-writable directory

# Create active release symlink
ln -sfn /opt/cricketpulse/releases/v2.1.0 /opt/cricketpulse/current
touch /var/log/cricketpulse/app.log
echo 'Setup complete — run cricket_file_audit.sh'

Step 1 — Foundation: Audit Framework

Step 1 establishes the audit script's framework: the exit code tracking, the pass/fail reporting functions, and the summary output. Unlike the inventory script from Module 1, this script must communicate a binary result to its caller — a CI/CD pipeline that runs this script needs to know whether the audit passed or failed via the exit code, not just by reading the output. Exit code 0 means all checks passed; any non-zero exit code means at least one check failed.

The framework uses a global counter to track failures. Each check function increments the counter when it finds a violation, and the script exits with the counter as its exit code. This design allows a CI pipeline to fail the build immediately on first violation (with `set -e`) or collect all violations and report them together — the choice depends on the pipeline's configuration.

Analogy🏏Cricket
🏏 Think of it like cricket: The exit code tracking is like the umpire's decision system in DRS. Each delivery produces a decision (pass/fail per check), and the over's outcome (audit result) is the accumulation of individual decisions. The batting team (CI pipeline) receives a clear signal at the end — total wickets lost (failure count) — not just a commentary description of each ball.Just as a DRS review that overturns a decision changes the over's official record, each failed check modifies the audit's official outcome. Just as the scoreboard shows 0 for dot balls and non-zero for wickets regardless of how dramatic the delivery was, the exit code is 0 for a clean audit and non-zero for any failure regardless of severity.The insight is that scripts communicating with pipelines must speak in exit codes — the universal language of process success and failure in Linux.
bash
#!/bin/bash
# cricket_file_audit.sh — Step 1: Audit Framework
set -euo pipefail

FAILURES=0
REPORT="/tmp/cricket_audit_$(date +%Y%m%d_%H%M%S).txt"

pass()  { echo "  [PASS] $1" | tee -a "$REPORT"; }
fail()  { echo "  [FAIL] $1" | tee -a "$REPORT"; ((FAILURES++)) || true; }
section() { echo; echo "=== $1 ==" | tee -a "$REPORT"; }

{
  echo "CricketPulse File Audit — $(date)"
  echo "Auditing host: $(hostname)"
} | tee "$REPORT"

# ---- All checks go here (Steps 2-4 expand this section) ----

# Summary and exit
echo | tee -a "$REPORT"
if [ "$FAILURES" -eq 0 ]; then
    echo "RESULT: PASSED — all checks clean" | tee -a "$REPORT"
    exit 0
else
    echo "RESULT: FAILED — $FAILURES violation(s) found" | tee -a "$REPORT"
    exit "$FAILURES"
fi

Step 2 — Core Logic: Directory and Ownership Checks

Step 2 adds the directory existence and ownership verification checks. Each directory in the CricketPulse deployment has an expected owner, group, and permission mode. The check function reads the actual values using `stat` and compares them against the expected values, calling `pass()` or `fail()` based on the result. This pattern — define expected state, read actual state, compare — is the same pattern used by configuration management tools like Ansible's `file` module and Chef's `directory` resource.

The `stat -c` format string is the key tool here. `stat -c '%a'` returns numeric permissions, `stat -c '%U'` returns the owner username, and `stat -c '%G'` returns the group name. Combining these into a single `stat -c '%a %U %G'` call reads all three values in one filesystem operation, reducing the number of `stat` system calls and making the check function efficient enough to run on dozens of directories without noticeable overhead.

Analogy🏏Cricket
🏏 Think of it like cricket: Directory checks are like verifying each area of the cricket ground against the ICC facility specification. The specification defines: pitch dimensions must be 22 yards (expected mode), pitch preparation must be done by the home ground curator (expected owner), boundary rope must be placed by ground staff (expected group). The verification reads the actual state and compares against the specification.Just as a ground that passes the facility specification is approved without the teams needing to re-verify each specification item themselves, a server directory that passes the ownership and permission checks can be trusted by the application without defensive permission checking in application code. The audit makes the application's trust well-founded rather than assumed.The insight is that infrastructure verification codifies institutional knowledge — the expected state becomes an executable specification rather than tribal knowledge held by a single senior engineer.
bash
# Step 2: Directory and ownership checks — add inside the report block

check_dir() {
    local path=$1 expected_mode=$2 expected_owner=$3 expected_group=$4

    if [ ! -d "$path" ]; then
        fail "MISSING directory: $path"; return
    fi

    actual_mode=$(stat -c '%a' "$path")
    actual_owner=$(stat -c '%U' "$path")
    actual_group=$(stat -c '%G' "$path")

    [ "$actual_mode"  = "$expected_mode"  ] || fail "$path permissions: got $actual_mode, expected $expected_mode"
    [ "$actual_owner" = "$expected_owner" ] || fail "$path owner: got $actual_owner, expected $expected_owner"
    [ "$actual_group" = "$expected_group" ] || fail "$path group: got $actual_group, expected $expected_group"

    [ "$FAILURES" -eq 0 ] && pass "$path: mode=$actual_mode owner=$actual_owner:$actual_group"
}

section "DIRECTORY OWNERSHIP & PERMISSIONS"
check_dir /etc/cricketpulse             750  root         cricketpulse
check_dir /var/log/cricketpulse         750  cricketpulse cricketpulse
check_dir /opt/cricketpulse             755  cricketpulse cricketpulse
check_dir /opt/cricketpulse/releases    2775 cricketpulse cricketpulse

section "SYMLINK VERIFICATION"
if [ -L /opt/cricketpulse/current ]; then
    target=$(readlink /opt/cricketpulse/current)
    if [ -d "$target" ]; then
        pass "current symlink → $target (valid directory)"
    else
        fail "current symlink → $target (target does not exist)"
    fi
else
    fail "current symlink missing at /opt/cricketpulse/current"
fi

Step 3 — Integration: Security Permission Checks

Step 3 adds the security-focused permission checks that identify the two most dangerous misconfigurations: world-readable files in the configuration directory (which may expose secrets) and world-writable files in the application directory (which may allow unauthorised modification of application code). These checks use `find` with permission criteria, producing output that names every violating file.

The check for world-readable configuration files uses `find /etc/cricketpulse -perm -004 -type f`. The `-004` permission mask matches any file where the 'other read' bit is set, regardless of the other bits. The equivalent check for world-writable application files uses `-perm -002` to match any file where the 'other write' bit is set. Both checks count their results and report a failure if any violations exist.

Analogy🏏Cricket
🏏 Think of it like cricket: Security permission checks are like the ICC anti-corruption unit's pre-series intelligence sweep. Before each major series, the unit checks for any players, staff, or officials with financial connections to known bookmakers — not because they expect violations, but because the check itself deters them and catches them early when they do occur.Just as the sweep focuses on specific indicators of risk (bookmaker connections) rather than auditing every possible corruption vector, the permission check focuses on the two specific permission bits (world-read on secrets, world-write on application code) that represent the highest-impact risks. Just as a clean sweep result is reported to tournament officials as a verified baseline, a clean permission audit result is reported to the deployment pipeline as a security baseline.The insight is that security checks are most valuable when they are automated, routine, and specific — the same qualities that make the anti-corruption sweep effective.
bash
# Step 3: Security permission checks

section "SECURITY — WORLD-READABLE CONFIG FILES"
world_readable=$(find /etc/cricketpulse -perm -004 -type f 2>/dev/null)
if [ -z "$world_readable" ]; then
    pass "No world-readable files in /etc/cricketpulse"
else
    while IFS= read -r f; do
        perm=$(stat -c '%a' "$f")
        fail "World-readable: $f (mode: $perm)"
    done <<< "$world_readable"
fi

section "SECURITY — WORLD-WRITABLE APP FILES"
world_writable=$(find /opt/cricketpulse -perm -002 -type f 2>/dev/null)
world_writable_dirs=$(find /opt/cricketpulse -perm -002 -type d 2>/dev/null)
if [ -z "$world_writable" ] && [ -z "$world_writable_dirs" ]; then
    pass "No world-writable files or dirs in /opt/cricketpulse"
else
    for f in $world_writable $world_writable_dirs; do
        perm=$(stat -c '%a' "$f")
        fail "World-writable: $f (mode: $perm)"
    done
fi

section "LOG DIRECTORY CHECK"
log_dir="/var/log/cricketpulse"
if [ -d "$log_dir" ]; then
    log_count=$(find "$log_dir" -name '*.log' -type f | wc -l)
    pass "Log directory exists with $log_count log file(s)"
else
    fail "Log directory missing: $log_dir"
fi

Step 4 — Testing & Verification

Run the complete audit script against the prepared target environment. The intentional misconfigurations created in the setup — the world-readable `db.key` and the world-writable `bin/` directory — should cause the script to exit with a non-zero code and clearly identify the violations. Verify that the script correctly counts failures and that the exit code equals the number of violations found.

Analogy🏏Cricket
🏏 Think of it like cricket: running the full script and cross-checking its numbers against `nproc`, `free -h`, and `df -h` is like a scorer confirming the big-screen total against the umpire's notebook and the third umpire's feed before it goes in the record. Just as a total that only appears on the scoreboard is untrusted until verified against an independent source, the report's CPU, memory, and disk figures are only trustworthy once they match the raw commands. Just as the score must show correctly to both the crowd in the ground and in the official book, the output should appear simultaneously in the terminal and in the timestamped file. Just as a good scorer confirms every figure comes from the right source rather than a stale note, you confirm the script reads from the correct system sources. The payoff: cross-checking against independent commands proves the inventory report is accurate, not just plausible-looking.

After verifying that failures are correctly detected, fix the two intentional misconfigurations with `chmod 640 /etc/cricketpulse/db.key` and `chmod 755 /opt/cricketpulse/bin`, then rerun the audit. The audit should now exit with code 0 and all checks should report PASS. This verify-fix-reverify cycle is the core workflow for using the audit in a deployment pipeline.

bash
# Run the audit — expect failures from the intentional misconfigurations
sudo ./cricket_file_audit.sh
echo "Exit code: $?"   # Should be non-zero (2 violations)

# Inspect the report
cat /tmp/cricket_audit_*.txt | grep -E '\[PASS\]|\[FAIL\]|RESULT'

# Fix the intentional misconfigurations
sudo chmod 640 /etc/cricketpulse/db.key
sudo chmod 755 /opt/cricketpulse/bin

# Rerun — should now exit 0
sudo ./cricket_file_audit.sh
echo "Exit code: $?"   # Should be 0

# Test in CI mode: fail fast on first violation
sudo chmod 644 /etc/cricketpulse/db.key   # Re-introduce one misconfiguration
sudo ./cricket_file_audit.sh && echo 'AUDIT PASSED' || echo "AUDIT FAILED: $? violations"

# Expected final output:
# CricketPulse File Audit — 2026-06-19 10:30:00
# === DIRECTORY OWNERSHIP & PERMISSIONS ==
#   [PASS] /etc/cricketpulse: mode=750 owner=root:cricketpulse
#   [PASS] /var/log/cricketpulse: mode=750 owner=cricketpulse:cricketpulse
# ...
# === SECURITY — WORLD-READABLE CONFIG FILES ==
#   [FAIL] World-readable: /etc/cricketpulse/db.key (mode: 644)
# RESULT: FAILED — 1 violation(s) found

Warning: The `((FAILURES++)) || true` pattern is required because `set -e` treats any command that returns non-zero as an error. In Bash, `((expression))` returns exit code 1 when the expression evaluates to zero — and `((FAILURES++))` evaluates to zero when FAILURES is 0 before the increment. Without `|| true`, the script exits on the very first failure increment when FAILURES is 0, preventing subsequent checks from running. The `|| true` ensures the overall expression always succeeds, letting the failure count accumulate correctly.

Extension Challenge: Add three additional audit sections to `cricket_file_audit.sh`. First, add a STALE BACKUPS check using `find /etc/cricketpulse -name '*.bak' -mtime +7` to identify backup files older than one week that should be rotated. Second, add a LOG ROTATION check that verifies `/etc/logrotate.d/cricketpulse` exists and contains the string 'compress' — confirming log compression is configured. Third, add a DISK SPACE check that fails if any filesystem used by the application is above 85% — using `df` output parsed with `awk` to extract the usage percentage for specific mount points.

  • Audit scripts must communicate results via exit codes (0 for pass, non-zero for fail) so CI/CD pipelines can act on them without parsing text output.
  • Use `stat -c '%a %U %G'` to read permissions and ownership in a single system call — combining all three into one `stat` invocation is more efficient than calling `stat` three times.
  • `find -perm -004` matches files where the world-read bit is set; `-perm -002` matches world-writable — these are the two permission bits most commonly misconfigured in deployments.
  • The `((FAILURES++)) || true` pattern is necessary with `set -e` because Bash arithmetic expressions return exit code 1 when the result is zero, which would trigger early script exit.
  • The verify-fix-reverify cycle — run audit, identify violations, fix them, rerun audit — is the core workflow for integrating automated security checks into a deployment process.
  • Automated filesystem audits codify security requirements as executable specifications, replacing tribal knowledge with verifiable, repeatable checks that work consistently across all deployments.
Lesson 8 of 40
0% complete