Red Hat Enterprise Linux Security Audit Tool
A single, self-contained script that audits RHEL systems against CIS Benchmarks, DISA STIGs, and a built-in hardening scanner — with full non-root support, auto OS detection, and beautiful HTML + JSON reports. 100% air-gap safe — no internet connection required, zero external dependencies.
Available as two interchangeable engines — pick whichever your host policy allows:
| File | Runs with | Requires |
|---|---|---|
rhelguard.sh |
bash rhelguard.sh |
bash 3.2+ and base RHEL utilities |
rhelguard.py |
python3 rhelguard.py |
Python 3.6+ (standard library only) |
rhelguard.py is a native re-implementation, not a wrapper — it does not
call, embed or extract the shell script. Both engines emit the same check IDs
and the same JSON/CSV/HTML reports, so a baseline taken with one can be diffed
with the other. Use the Python engine where local policy, change control or a
file-transfer gateway will not accept a .sh file.
| Framework | Coverage |
|---|---|
| CIS Benchmarks | RHEL 5, 6, 7, 8, 9, 10 — L1 Server (auto-selected by detected version) |
| DISA STIG | RHEL 6, 7, 8, 9 — CAT I (High) + key CAT II (Medium) findings |
| Posture Checks | Crypto policy, file integrity, account hygiene, network exposure, SUID audit |
| Air-Gap Isolation (new in 2.2) | Egress & bridging, radios (Wi-Fi/BT/WWAN/USB-tether), DMA ports, phone-home services, internet-facing repos, patch age, NTP/syslog/SSH-tunnel exposure |
| Built-in Hardening Scanner | Embedded Lynis-equivalent covering 15 test categories: AUTH BOOT CRYP INSE KRNL LOGG MALW PKGS SCHD SHLL STRG TIME TOOL USERS HRDN — no Lynis install needed, fully air-gap safe |
scp rhelguard.sh user@server:/tmp/
sudo bash /tmp/rhelguard.sh # full scan (recommended)
bash /tmp/rhelguard.sh # non-root partial scanscp rhelguard.py user@server:/tmp/
sudo python3 /tmp/rhelguard.py # full scan (recommended)
python3 /tmp/rhelguard.py # non-root partial scanBoth take the same options. Invoking the interpreter explicitly
(bash … / python3 …) rather than ./rhelguard.* also means the file needs
no execute bit, which matters on a noexec mount such as a hardened /tmp —
under noexec neither file can be run as ./rhelguard.*, but both work
when the interpreter is called directly.
Reports saved to ./rhelguard_reports/ — open the .html file in any browser.
Both engines do not require root to run — it will execute everything it can and cleanly skip checks that require elevated privileges, clearly marking them SKIP (root required) in the report.
| Check Type | Non-Root | Root |
|---|---|---|
| Kernel modules (lsmod, modprobe.d) | ✅ | ✅ |
| Mount options (findmnt) | ✅ | ✅ |
| SELinux status (getenforce, sestatus) | ✅ | ✅ |
| SSH config (sshd -T) | ✅ | ✅ |
| Sysctl network/kernel params | ✅ | ✅ |
| Service status (systemctl) | ✅ | ✅ |
| File permissions (/etc/passwd etc.) | ✅ | ✅ |
| Account UIDs/GIDs (/etc/passwd) | ✅ | ✅ |
| /etc/shadow — empty passwords | ❌ SKIP | ✅ |
| Audit log file permissions | ❌ SKIP | ✅ |
| RPM package integrity (rpm -Va) | ❌ SKIP | ✅ |
| chage (account expiry per-user) | ❌ SKIP | ✅ |
For hosts where a .sh file is not acceptable — local policy forbidding shell
scripts, change control that only approves Python, or a transfer gateway that
strips the extension.
It is a real port, not a wrapper. There is no embedded, encoded or
downloaded copy of rhelguard.sh inside it; every check is implemented in
Python against the same sources (/proc, /sys, /etc, the rpm DB). Nothing
is installed and nothing is fetched.
| Interpreter | Python 3.6+ — RHEL 8 ships 3.6, RHEL 9/10 ship 3.9+ |
| Dependencies | Standard library only. No pip, no venv, no network |
| Privileges | Same as the shell engine: runs as any user, skips privileged checks |
| Output | Same JSON / CSV / HTML filenames and schema |
| Parity | Verified identical to rhelguard.sh on RHEL 9 and RHEL 8, in all five modes, as root and non-root |
python3 rhelguard.py --help
python3 rhelguard.py --version
sudo python3 rhelguard.py -m all -BOn RHEL 7 and older the system Python is 2.7; install python3 (or python36)
or use the shell engine there instead.
Where the two engines deliberately read the system differently, the Python one
is the more portable of the two — it reads /proc/sys instead of calling
sysctl, /proc/self/mountinfo instead of findmnt, and /proc/modules
instead of lsmod, so it needs no procps-ng or util-linux. As of 2.3.0 the
shell engine does the same, which is why their results match exactly.
Reports state which engine produced them:
jq -r '.engine' RHELGuard_*.json # -> "bash" or "python3"sudo bash rhelguard.sh [OPTIONS]
sudo python3 rhelguard.py [OPTIONS]
OPTIONS:
-m, --mode cis | stig | posture | airgap | all (default: all)
-o, --output Output directory (default: ./rhelguard_reports)
-t, --throttle ms delay between checks (default: 50)
-b, --baseline FILE Previous RHELGuard JSON — show drift since that scan
-w, --waivers FILE Accepted deviations, one "CHECK-ID | reason" per line
-a, --max-patch-age Days since last package change before flagging (default: 90)
-B, --bundle Pack reports + MANIFEST + SHA256SUMS into a .tar.gz for transfer
--strict Exit 2 if any FAIL remains (CI / automation)
-s, --skip-lynis No-op (kept for compatibility)
-q, --quiet Suppress per-check output (summary still printed)
-h, --help Show this help
Swap bash rhelguard.sh for python3 rhelguard.py in any of these — the
options are identical.
# Full scan — all frameworks
sudo bash rhelguard.sh
# CIS only, custom output dir
sudo bash rhelguard.sh -m cis -o /var/log/rhelguard
# STIG only, higher throttle for busy production system
sudo bash rhelguard.sh -m stig -t 200
# Non-root partial scan
bash rhelguard.sh
# Quiet mode (no console output, just reports)
sudo bash rhelguard.sh -q
# Air-gap isolation checks only
sudo python3 rhelguard.py -m airgap
# Quarterly enclave audit: compare to last run, apply approved waivers, bundle for transfer
sudo python3 rhelguard.py -b last_quarter.json -w enclave-waivers.txt -B --strict
# A baseline from either engine works with either engine
sudo bash rhelguard.sh -o out1
sudo python3 rhelguard.py -b out1/RHELGuard_*.jsonRHELGuard is built for disconnected enclaves: every check reads local state (/proc, /sys, /etc, rpm DB) — nothing is resolved, fetched or probed, and the HTML report carries a strict Content-Security-Policy so it cannot load or send anything either.
1. Verify before transfer in. Check the script against the published SHA256SUMS on your connected side, and again on the enclave side after media transfer:
sha256sum -c SHA256SUMS # covers rhelguard.sh and rhelguard.pyEach report records the script_sha256 of the build that produced it, so an auditor can prove which version ran.
2. Scan. sudo bash rhelguard.sh -B (or sudo python3 rhelguard.py -B) — works on a minimal install with no repos, no DNS and no default route.
3. Track drift without a SIEM. Keep each JSON and feed it back next time with -b. The report lists every check that is NEW, REGRESSED, FIXED or CHANGED.
4. Record accepted risk. Enclaves always have documented deviations. Put them in a waiver file and they show as WAIVED (with the reason) instead of failures, and are excluded from the score:
# enclave-waivers.txt
AIR-NET-1 | Default route to enclave-only firewall, CAB-2291
CIS-MOD-squashfs | Required by appliance image
Waiving a base ID (e.g. STIG-PKG-tuned) also covers suffixed duplicates.
5. Transfer out. -B produces RHELGuard_<host>_<ts>.tar.gz containing the reports, a MANIFEST.txt (host, operator, script hash, summary) and SHA256SUMS, and prints the bundle hash for your media transfer log.
| ID | Checks |
|---|---|
| AIR-NET-1..4 | Default route (from /proc, works on RHEL 5), multi-homed host, IP forwarding, proxy settings (credentials masked) |
| AIR-DNS-1 | Public/non-private DNS resolvers (DNS-tunnel & leak path) |
| AIR-RF-1..4 | Wi-Fi interfaces/drivers, Bluetooth, cellular/WWAN modems + ModemManager, USB NIC / phone tethering drivers |
| AIR-DMA-1 | Thunderbolt without authorisation, FireWire DMA |
| AIR-USB-1 | udisks2 removable-media automount |
| AIR-SVC-* | rhsmcertd, insights-client, rhcd, dnf-makecache/automatic, yum-cron, PackageKit, avahi, cups-browsed, cloud-init, geoclue, network kdump |
| AIR-RHSM-1 | subscription-manager pointed at the public CDN instead of Satellite |
| AIR-REPO-1 | Enabled repos using public mirrors, mirrorlist or metalink |
| AIR-PATCH-1 | Days since last package change vs --max-patch-age (the real risk of a disconnected host) |
| AIR-PATCH-2 | Running kernel older than newest installed kernel (reboot pending) |
| AIR-TIME-1 | Missing or public NTP sources |
| AIR-LOG-1 | Log forwarding to public destinations |
| AIR-SSH-1 | TCP/agent/stream forwarding, PermitTunnel, GatewayPorts, X11 |
Public destinations are private-range IP checks plus a list of well-known internet domains (AIRGAP_PUBLIC_PATTERNS near the top of the air-gap section — extend it for your environment).
RHELGuard auto-detects your RHEL major version and applies the correct checks:
| Version | CIS Benchmark | DISA STIG | Notes |
|---|---|---|---|
| RHEL 5 | CIS RHEL 5 L1 v2.2.1 | v1r18 | Legacy — basic checks |
| RHEL 6 | CIS RHEL 6 v3.0.0 L1 | v2r2 | EOL — security-critical only |
| RHEL 7 | CIS RHEL 7 v4.0.0 L1 | v3r15 | Full coverage |
| RHEL 8 | CIS RHEL 8 v4.0.0 L1 | v2r6 | Full coverage |
| RHEL 9 | CIS RHEL 9 v2.0.0 L1 | v2r7 | Full coverage + RHEL 9-specific checks |
| RHEL 10 | CIS RHEL 10 v1.0.1 L1 | — | Best-effort (new) |
Also works on compatible derivatives: CentOS, AlmaLinux, Rocky Linux, Fedora (best-effort).
The report is a standalone HTML file — no server required, open in any browser.
- Compliance Score —
pass ÷ (pass + fail + warn); INFO, SKIP and WAIVED don't count against you - Drift panel — changes since the
--baselinescan - Offline-locked — CSP blocks all network loads; all values HTML-escaped
- Progress bar — visual compliance fill bar
- Summary cards — PASS / FAIL / WARN / INFO / SKIP at a glance
- Non-root warning — banner showing how many checks were privilege-skipped
- Filter buttons — click to show only failures, warnings, passes etc.
- Live search — filter by check ID, category, finding text, remediation command
- Remediation commands — every FAIL/WARN shows the exact fix command
rhelguard_reports/ (created with umask 077)
├── RHELGuard_<hostname>_<timestamp>.html ← Human dashboard
├── RHELGuard_<hostname>_<timestamp>.json ← Machine-readable / baseline input
├── RHELGuard_<hostname>_<timestamp>.csv ← Spreadsheet / GRC import
└── RHELGuard_<hostname>_<timestamp>.tar.gz ← with -B: reports + MANIFEST + SHA256SUMS
Exit codes: 0 scan completed · 1 usage/setup error · 2 FAILs present (only with --strict).
| Feature | Detail |
|---|---|
| ✅ Read-only | Zero system modifications made |
| ✅ No service restarts | Nothing interrupted |
| ✅ Configurable throttle | --throttle 200 for I/O-sensitive systems |
| ✅ No network probing | All checks are local only |
| ✅ No port scanning | Network checks inspect local sysctl only |
| ✅ Non-root safe | Runs without sudo, skips privileged checks gracefully |
| ✅ No package installs | Zero dependencies beyond base RHEL |
For very busy production systems:
sudo bash rhelguard.sh -t 500 # 500ms throttle for very busy systems
sudo python3 rhelguard.py -t 500 # same option on the Python engine#!/bin/bash
HOSTS=(web01 db01 app01 bastion01)
for host in "${HOSTS[@]}"; do
echo "Scanning $host..."
scp rhelguard.sh root@${host}:/tmp/
ssh root@${host} "bash /tmp/rhelguard.sh -q -B -o /tmp/rg_out"
mkdir -p collected/${host}
scp "root@${host}:/tmp/rg_out/*.tar.gz" collected/${host}/
done
echo "All done. Reports in ./collected/"# Show all failures with remediation
jq '.results[] | select(.status=="FAIL") | {id, title, remediation}' RHELGuard_*.json
# Compliance summary
jq '.summary' RHELGuard_*.json
# All CAT I equivalent (FAIL) access control issues
jq '.results[] | select(.status=="FAIL" and .category=="ACCESS CONTROL")' RHELGuard_*.json
# Count skipped due to non-root
jq '.summary.priv_skip' RHELGuard_*.json
# What regressed since the baseline?
jq '.drift[] | select(.change=="REGRESSED" or .change=="NEW")' RHELGuard_*.jsonNo jq inside the enclave? Results are one JSON object per line, so plain grep works:
grep '"status":"FAIL"' RHELGuard_*.json| Area | Checks |
|---|---|
| Kernel modules | cramfs, freevxfs, hfs, hfsplus, jffs2, udf, squashfs (8+), firewire, usb-storage, sctp, tipc, atm, can, bluetooth (9+) |
| Mount options | /tmp, /dev/shm, /home, /var, /var/tmp, /var/log, /var/log/audit, /boot (9+) |
| Package management | GPG keys, gpgcheck, localpkg_gpgcheck (9+), pending updates |
| SELinux | Install, bootloader, mode, policy, mcstrans, setroubleshoot |
| Bootloader | Password, config permissions, ownership (9+) |
| Kernel params | ASLR, suid_dump, dmesg_restrict, kptr_restrict, ptrace, hardlinks, symlinks, BPF (8+), user namespaces (9+) |
| Services | 20+ unnecessary service checks, version-conditional |
| Network | 22+ sysctl parameters, firewall |
| Logging | auditd, audit rules (7 keys + extras for 8+), rsyslog, journald |
| SSH | 17 directive checks, version-conditional (GSSAPI, Kerberos, UsePAM) |
| Password policy | login.defs, pwquality, faillock (8+) / pam_tally2 (≤7), TMOUT, umask |
| File permissions | 8 critical /etc files with mode + ownership |
| Account integrity | UID 0 audit, duplicate UIDs/GIDs, empty passwords, inactive lockout |
| Severity | Areas |
|---|---|
| CAT I (High) | FIPS mode, disk encryption, shosts files, dangerous packages, Ctrl-Alt-Delete, UID 0 |
| CAT II (Medium) | SSH banners, crypto policy, sudo NOPASSWD, SSH key permissions, USBGuard (9+), required packages (9+), audit log perms, password aging, NTP, wireless, Bluetooth |
| Area | Checks |
|---|---|
| File integrity | World-writable /etc files, sticky bit, SUID/SGID inventory, AIDE, RPM verify |
| Accounts | Interactive account expiry, system account shells, cron access |
| Crypto | System crypto policy, Secure Boot, NX/XD bit |
| Network | Listening services, promiscuous mode, IPv6 status |
| Logging | Critical log file presence |
| Built-in Hardening | AUTH, BOOT, CRYP, INSE, KRNL, LOGG, MALW, PKGS, SCHD, SHLL, STRG, TIME, TOOL, USERS, HRDN |
- CIS Red Hat Enterprise Linux Benchmarks
- DISA STIG for RHEL 8
- DISA STIG for RHEL 9
- Tenable CIS/STIG Audit Files
- CISOfy Lynis (inspiration for hardening categories — not required or downloaded)
- NIST 800-53
MIT — see LICENSE