This is a host-based intrusion detection & integrity monitoring stack designed to detect unauthorized system modifications through two complementary layers of defense.
The stack consists of five components working together:
/etc, /var, /home, /usr/local). On rpm-ostree systems, the baseline is typically zero because all standard SUID binaries reside in the immutable /usr tree; any new appearance is suspicious.Both tools are required because they cover different threat models:
/etc & /usr are overlay filesystems. auditd's inode-based watches can fail to trigger on overlay layers. AIDE hashes the actual file content, bypassing filesystem abstraction entirely.Together, they ensure no persistent tampering goes unnoticed, whether through a live syscall or a quiet file swap while the system sleeps.
I built this because I noticed secureblue stops attacks well but doesn't tell you if and when one gets through.
| secureblue's approach | The gap | What this stack adds |
|---|---|---|
| Preventive hardening | No detection of successful bypasses | Post-exploitation visibility |
Immutable /usr |
Mutable /etc, /var, /home remain exposed |
Integrity monitoring for mutable paths |
| Policy enforcement | No audit trail of policy violations | Tamper-evident logging |
| Boot-time security | No runtime change detection | Continuous monitoring between boots |
/etc/passwd to add a backdoor account: auditd catches the syscall in real-time, AIDE catches it on next scan/home or /var/tmp: SUID scanner alerts on next runrun0: audit rule fires instantly/etc & /usr: AIDE's content hashing bypasses this entirelysecureblue hardens well, but hardening without detection leaves you blind to successful bypasses. If someone edits /etc/passwd or drops a SUID binary in /var/tmp, you need to know. This stack is not a replacement for secureblue's prevention; it is the other half of defense in depth.
Some will call it scope creep. Fair. But I would rather have logs I never read than miss a compromise I could have caught. The tools are cheap; the baseline is zero; the setup is one afternoon. If you install it and ignore the daily review, that is on you. The architecture is sound.
hardened_malloc breaks dynamically linked AIDE. The standard Fedora package segfaults during database operations. Building a statically linked AIDE eliminates the conflict: the binary bundles its own allocator & ignores the system's LD_PRELOAD & malloc hooks. This preserves hardened_malloc for all other processes.
Build complexity: Fedora 44 does not ship static library archives (.a files) for nettle, pcre2, or zlib in standard repositories. The build must compile these dependencies from source, then statically link AIDE against them. This requires only bison & flex from Fedora; all other dependencies are built from upstream source.
Nettle 4.0 compatibility: AIDE 0.19.3 was written for nettle 3.x. Nettle 4.0 changed the nettle_hash_digest_func API from 3 arguments to 2. This guide applies a one-line patch to src/md.c before building. The patch removes the obsolete length parameter from the digest call. If you prefer not to patch AIDE, use nettle 3.10.2 (the last 3.x release) instead; the fallback URL is provided.
# Check latest versions via GitHub API & GNU FTP
# Run these before Phase 0 to verify the URLs below are current
# Latest zlib
/usr/bin/curl -s https://api.github.com/repos/madler/zlib/releases/latest | /usr/bin/grep '"tag_name":'
# Latest pcre2
/usr/bin/curl -s https://api.github.com/repos/PCRE2Project/pcre2/releases/latest | /usr/bin/grep '"tag_name":'
# Latest nettle (manual check; no API)
/usr/bin/curl -s https://ftp.gnu.org/gnu/nettle/ | /usr/bin/grep -o 'nettle-[0-9.]*\.tar\.gz' | /usr/bin/sort -V | /usr/bin/tail -n 1
# Latest AIDE
/usr/bin/curl -s https://api.github.com/repos/aide/aide/releases/latest | /usr/bin/grep '"tag_name":'
# Layer only bison & flex; all other dependencies built from source
# Reboot required before tools are available in active deployment
rpm-ostree install bison flex
systemctl reboot
Download, compile & install static versions of zlib, pcre2 & nettle to a local prefix. Then build AIDE linked against them.
# Set build environment
BUILD_PREFIX="/var/home/$(whoami)/aide-static-deps"
/usr/bin/mkdir -p "${BUILD_PREFIX}"
export LD_PRELOAD=
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
# Build static zlib (latest: 1.3.2)
cd /var/home/$(whoami)
/usr/bin/curl -LO https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz
/usr/bin/tar xzf zlib-1.3.2.tar.gz
cd zlib-1.3.2
./configure --prefix="${BUILD_PREFIX}" --static
/usr/bin/make -j$(/usr/bin/nproc)
/usr/bin/make install
# Build static pcre2 (latest: 10.47)
cd /var/home/$(whoami)
/usr/bin/curl -LO https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.47/pcre2-10.47.tar.gz
/usr/bin/tar xzf pcre2-10.47.tar.gz
cd pcre2-10.47
./configure --prefix="${BUILD_PREFIX}" --disable-shared --enable-static --enable-utf --enable-unicode-properties
/usr/bin/make -j$(/usr/bin/nproc)
/usr/bin/make install
# Build static nettle (latest: 4.0)
# Fallback URL if you prefer not to patch AIDE: https://ftp.gnu.org/gnu/nettle/nettle-3.10.2.tar.gz
cd /var/home/$(whoami)
/usr/bin/curl -LO https://ftp.gnu.org/gnu/nettle/nettle-4.0.tar.gz
/usr/bin/tar xzf nettle-4.0.tar.gz
cd nettle-4.0
./configure --prefix="${BUILD_PREFIX}" --disable-shared --enable-static
/usr/bin/make -j$(/usr/bin/nproc)
/usr/bin/make install
# Build static AIDE linked against custom deps (latest: 0.19.3)
# Apply nettle 4.0 API patch before compiling
cd /var/home/$(whoami)
/usr/bin/mkdir -p aide-build-0.19.3
cd aide-build-0.19.3
/usr/bin/curl -LO https://github.com/aide/aide/releases/download/v0.19.3/aide-0.19.3.tar.gz
/usr/bin/tar xzf aide-0.19.3.tar.gz
cd aide-0.19.3
# Patch src/md.c for nettle 4.0 compatibility
# Nettle 4.0 changed digest() from 3 args to 2; remove the obsolete length parameter
/usr/bin/sed -i 's/nettle_functions\[i\].digest(&md->ctx\[i\].md5, hashsums\[i\].length, hs->hashsums\[i\]);/nettle_functions[i].digest(\&md->ctx[i].md5, hs->hashsums[i]);/' src/md.c
export PKG_CONFIG_PATH="${BUILD_PREFIX}/lib/pkgconfig:${BUILD_PREFIX}/lib64/pkgconfig"
export CFLAGS="-I${BUILD_PREFIX}/include"
export LDFLAGS="-static -L${BUILD_PREFIX}/lib -L${BUILD_PREFIX}/lib64"
./configure --prefix=/usr/local --disable-shared --enable-static
/usr/bin/make -j$(/usr/bin/nproc)
run0 /usr/bin/bash -c "export LD_PRELOAD=; export PATH=/usr/sbin:/usr/bin:/sbin:/bin; cd /var/home/$(whoami)/aide-build-0.19.3/aide-0.19.3; /usr/bin/make install"
# Verify static link & hardened_malloc immunity
/usr/bin/ldd /usr/local/bin/aide
LD_PRELOAD=/usr/lib64/libhardened_malloc.so /usr/local/bin/aide --version
# AIDE requires a configuration file. Create minimal config for secureblue.
# Note: acl, selinux, xattrs omitted because AIDE was built without those libraries.
run0 /usr/bin/tee /usr/local/etc/aide.conf << 'EOF'
database_in=file:/var/lib/aide/aide.db.gz
database_out=file:/var/lib/aide/aide.db.new.gz
gzip_dbout=yes
/boot p+i+n+u+g+s+b+m+sha256
/etc p+i+n+u+g+s+b+m+sha256
/bin p+i+n+u+g+s+b+m+sha256
/sbin p+i+n+u+g+s+b+m+sha256
/lib p+i+n+u+g+s+b+m+sha256
/lib64 p+i+n+u+g+s+b+m+sha256
/opt p+i+n+u+g+s+b+m+sha256
/usr p+i+n+u+g+s+b+m+sha256
/var p+i+n+u+g+s+b+m+sha256
!/var/log/.*
!/var/run/.*
!/var/tmp/.*
!/tmp/.*
!/proc/.*
!/sys/.*
!/dev/.*
!/run/.*
EOF
# Initialize database
run0 /usr/bin/mkdir -p /var/lib/aide
run0 /usr/local/bin/aide --init
run0 /usr/bin/mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
# Verify check works
run0 /usr/local/bin/aide --check
Static linking AIDE does not weaken your system. It isolates a compatibility issue.
The problem: hardened_malloc breaks dynamically linked AIDE during large database operations. The segfault is a compatibility bug, not an AIDE vulnerability.
The fix: The static binary bundles its own allocator and ignores LD_PRELOAD. hardened_malloc never loads into AIDE's address space. Every other process keeps hardened_malloc.
The risk: AIDE uses a standard allocator. But it is non-network-facing, short-lived, and only reads local files that have already passed through kernel and filesystem checks. The alternative is a non-functional tool. I will take a working integrity scanner with an isolated allocator over a broken one every time. The trade-off is documented.
Pre-flight check: Verify you're on a standard secureblue deployment with mutable /var & /etc. If you have custom overlays or read-only /var, AIDE database storage & audit log persistence will fail.
The following packages provide the core monitoring infrastructure. AIDE was built in Phase 0; only audit tools & dependencies are layered here.
# Install audit framework & utilities
rpm-ostree install audit jq
# REBOOT NOW. Do not proceed until the system has restarted.
# Failure to reboot means auditctl & augenrules will not be available.
systemctl reboot
run0 wrapper strips /usr/sbin & /sbin from PATH for security. All audit tools live in /usr/sbin. Therefore, every audit command in this phase must use absolute paths. Copy-pasting standard commands will fail with "command not found." This guide uses absolute paths exclusively; do not abbreviate.
$(whoami) command expands to your actual username at file creation time because the heredoc uses double quotes. Verify the output matches your username.
# Create audit rules defining watched paths & their monitoring keys
# -p wa: watch for writes & attribute changes
# -k <key>: tag events for ausearch filtering
# Unquoted heredoc delimiter allows shell expansion; run0 preserves your original username
run0 /usr/bin/tee /etc/audit/rules.d/99-secureblue-custom.rules << EOF
# Identity & authentication stores
-w /etc/passwd -p wa -k identity_changes
-w /etc/shadow -p wa -k identity_changes
-w /etc/group -p wa -k identity_changes
-w /etc/gshadow -p wa -k identity_changes
# Dynamic linker hijacking
-w /etc/ld.so.preload -p wa -k linker_hijack
-w /etc/ld.so.conf.d/ -p wa -k linker_hijack
# Privilege model (polkit/run0)
-w /etc/polkit-1/rules.d/ -p wa -k privilege_escalation
-w /etc/polkit-1/localauthority/ -p wa -k privilege_escalation
# SSH config
-w /etc/ssh/sshd_config -p wa -k ssh_config
-w /etc/ssh/ssh_config.d/ -p wa -k ssh_config
# Systemd overrides & persistent units
-w /etc/systemd/system/ -p wa -k systemd_changes
-w /etc/systemd/user/ -p wa -k systemd_changes
# Firewall & network
-w /etc/firewalld/ -p wa -k firewall_changes
-w /etc/NetworkManager/system-connections/ -p wa -k network_changes
-w /etc/hosts -p wa -k dns_hijack
-w /etc/resolv.conf -p wa -k dns_hijack
# Time sync
-w /etc/chrony.conf -p wa -k time_sync
# Kernel module overrides
-w /etc/modprobe.d/ -p wa -k module_changes
# Audit config self-protection
-w /etc/audit/rules.d/ -p wa -k audit_config
# Canary files (use /var, not /etc, for reliable triggering on overlayfs)
-w /var/.system_canary -p rwxa -k canary_trip
-w /home/$USER/.user_canary -p rwxa -k canary_trip
EOF
/home/$USER/.user_canary. Creating the file at a different path means the tripwire watches a nonexistent location.
# System-wide canary in /var (not /etc, because /var triggers more reliably on overlayfs)
run0 /usr/bin/tee /var/.system_canary << 'EOF'
# System canary - DO NOT DELETE
# Security monitoring decoy
EOF
run0 /usr/bin/chmod 644 /var/.system_canary
run0 /usr/bin/grep canary /etc/audit/rules.d/99-secureblue-custom.rules
# User canary in your actual home directory
# Verify the path matches Step 1 audit rule: run0 /usr/bin/grep canary /etc/audit/rules.d/99-secureblue-custom.rules
/usr/bin/tee ~/.user_canary << 'EOF'
# User canary - DO NOT DELETE
# Security monitoring decoy
EOF
/usr/bin/chmod 644 ~/.user_canary
# Create dedicated privilege escalation rules file
# These load before 99-secureblue-custom.rules via numeric ordering (98-privesc.rules)
run0 /usr/bin/tee /etc/audit/rules.d/98-privesc.rules << 'EOF'
# Privilege Escalation Detection: secureblue
# Elevation binaries
-w /usr/bin/run0 -p x -k privesc_run0
-w /usr/bin/pkexec -p x -k privesc_pkexec
-w /usr/bin/passwd -p x -k privesc_passwd
# Account management
-w /usr/sbin/useradd -p x -k privesc_account
-w /usr/sbin/usermod -p x -k privesc_account
-w /usr/sbin/userdel -p x -k privesc_account
-w /usr/sbin/groupadd -p x -k privesc_account
-w /usr/sbin/groupmod -p x -k privesc_account
-w /usr/sbin/groupdel -p x -k privesc_account
# Authentication configuration
-w /etc/pam.d/ -p wa -k privesc_pam
-w /etc/security/ -p wa -k privesc_security
# Kernel module & eBPF loading
-a always,exit -F arch=b64 -S init_module -S finit_module -S delete_module -k privesc_kmod
-a always,exit -F arch=b64 -S bpf -k privesc_bpf
# Capability manipulation
-w /usr/sbin/setcap -p x -k privesc_caps
# Persistence vectors
-w /etc/cron.d/ -p wa -k privesc_persist
-w /etc/cron.daily/ -p wa -k privesc_persist
-w /etc/cron.hourly/ -p wa -k privesc_persist
-w /etc/cron.weekly/ -p wa -k privesc_persist
-w /etc/cron.monthly/ -p wa -k privesc_persist
-w /var/spool/cron/ -p wa -k privesc_persist
-w /etc/at.allow -p wa -k privesc_persist
-w /etc/at.deny -p wa -k privesc_persist
EOF
augenrules lives in /usr/sbin. Because run0 strips this from PATH, you must call it with an absolute path. If this fails with "command not found", you forgot to reboot after Phase 1.
# Compile rules from /etc/audit/rules.d/ into the active kernel configuration
# Absolute path required: run0 does not resolve /usr/sbin
run0 /usr/sbin/augenrules --load
systemctl status auditd & /usr/sbin/ausearch -m AVC -ts recent for denials.
# Count active rules. Expected output: ~44
# If 0: auditd is not running or rules failed to compile
# Absolute path required for both auditctl & wc
run0 /usr/sbin/auditctl -l | /usr/bin/wc -l
/usr/sbin/ausearch -m AUDIT_BACKLOG after deployment to tune.
# Increase kernel audit buffer to prevent event loss under load
# Absolute path required: auditctl is in /usr/sbin
run0 /usr/sbin/auditctl -b 8192
/var is writable & persistent. On some rpm-ostree configurations, /var is ephemeral or size-constrained. AIDE databases can grow large; ensure at least 500MB free in /var/lib.
database config option with database_in & summarize_changes with report_summarize_changes. If you have an existing /etc/aide.conf from v0.18, update these directives before initializing the database. This guide assumes a fresh v0.19 install with default config.
# AIDE requires a dedicated directory for its cryptographic database
run0 /usr/bin/mkdir -p /var/lib/aide
# Initialize the AIDE database: scan entire filesystem & create baseline hashes
# This takes several minutes. Progress is silent unless errors occur.
# Uses the statically linked binary from /usr/local/bin; no LD_PRELOAD needed
run0 /usr/local/bin/aide --init
aide --init created /var/lib/aide/aide.db.new.gz. If your AIDE version uses a different default output path, adjust accordingly. Check /usr/local/bin/aide --config-check output or /usr/local/etc/aide.conf for the database_out directive if this step fails. In v0.19, database_in replaces the old database directive.
# Promote the newly initialized database to the active checked database
# If "No such file": check /var/lib/aide/ for aide.db.new.gz.* variants
run0 /usr/bin/mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
Type=oneshot because AIDE is not a daemon; it runs once & exits. StandardOutput=journal ensures results are logged even if the timer fires while no user is logged in.
# Create the one-shot service that runs the integrity check
run0 /usr/bin/tee /etc/systemd/system/aide-check.service << 'EOF'
[Unit]
Description=AIDE integrity check
[Service]
Type=oneshot
# Static binary path; no LD_PRELOAD bypass needed
ExecStart=/usr/local/bin/aide --check
StandardOutput=journal
EOF
# Create the timer that triggers the service daily
run0 /usr/bin/tee /etc/systemd/system/aide-check.timer << 'EOF'
[Unit]
Description=Daily AIDE integrity check
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
EOF
# Reload systemd to recognize the new units
run0 /usr/bin/systemctl daemon-reload
# Enable & start the timer (the service runs immediately, then daily thereafter)
run0 /usr/bin/systemctl enable --now aide-check.timer
/opt or /usr/local, those new SUID binaries will trigger false positive alerts. After installing any such package, re-run run0 /usr/local/bin/suid-scanner to re-establish baseline. The script will overwrite baseline.txt with the new state.
current.txt to baseline.txt. On subsequent runs, it diffs against baseline. The 2>/dev/null suppresses permission denied errors from restricted paths like /var/lib/private. If you need to audit those paths, run with elevated permissions or adjust the find command.
# Create scanner that detects new setuid/setgid binaries in mutable paths
# /usr is excluded because it is immutable on rpm-ostree; any SUID there is vendor-trusted
run0 /usr/bin/tee /usr/local/bin/suid-scanner << 'EOF'
#!/bin/bash
set -euo pipefail
BASELINE="/var/lib/suid-scanner/baseline.txt"
CURRENT="/var/lib/suid-scanner/current.txt"
DIFFOUT="/var/lib/suid-scanner/diff.txt"
# Scan mutable paths only. /boot included because it is not immutable on all deployments.
/usr/bin/find /etc /var /opt /home /usr/local /boot -type f \( -perm -4000 -o -perm -2000 \) 2>/dev/null | /usr/bin/sort > "$CURRENT"
if [[ ! -f "$BASELINE" ]]; then
/usr/bin/cp "$CURRENT" "$BASELINE"
/usr/bin/echo "Baseline established: $(/usr/bin/wc -l < "$CURRENT") mutable SUID/SGID binaries"
exit 0
fi
if ! /usr/bin/diff -q "$BASELINE" "$CURRENT" > /dev/null 2>&1; then
/usr/bin/diff "$BASELINE" "$CURRENT" > "$DIFFOUT" || true
/usr/bin/echo "ALERT: SUID/SGID drift detected"
else
: > "$DIFFOUT"
fi
EOF
run0 /usr/bin/chmod +x /usr/local/bin/suid-scanner
# Service unit for the scanner
run0 /usr/bin/tee /etc/systemd/system/suid-scanner.service << 'EOF'
[Unit]
Description=Scan for new SUID/SGID binaries
[Service]
Type=oneshot
ExecStart=/usr/local/bin/suid-scanner
EOF
# Timer unit: runs daily, persists across missed intervals (e.g., system was off)
run0 /usr/bin/tee /etc/systemd/system/suid-scanner.timer << 'EOF'
[Unit]
Description=Daily SUID/SGID scan
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
EOF
run0 /usr/bin/systemctl daemon-reload
run0 /usr/bin/systemctl enable --now suid-scanner.timer
# First run establishes baseline. Review output carefully before treating as trusted.
run0 /usr/local/bin/suid-scanner
/usr tree. This is normal; the scanner detects tampering in mutable paths.
Storage=auto with MaxFileSec=1month, which is adequate. However, if your system has Storage=volatile or aggressive vacuuming configured, audit events may be lost before the daily review runs. Verify /etc/systemd/journald.conf & ensure /var/log/journal/ exists for persistent storage. The review script cannot summarize events that have already been deleted.
# Create the daily review script that summarizes all security events
run0 /usr/bin/tee /usr/local/bin/audit-review << 'EOF'
#!/bin/bash
# Restore full PATH for utility commands, but audit tools use absolute paths below
PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH
echo "=== AUDIT SUMMARY ==="
# Count active rules using absolute path for auditctl
# If 0, auditd is down or rules were lost.
echo "Active rules: $(/usr/sbin/auditctl -l 2>/dev/null | /usr/bin/wc -l)"
echo ""
echo "Identity changes:"
/usr/sbin/ausearch -k identity_changes -ts recent 2>/dev/null | /usr/bin/grep -E "SYSCALL|PATH" | /usr/bin/tail -n 5 || echo "None"
echo ""
echo "Linker hijack attempts:"
/usr/sbin/ausearch -k linker_hijack -ts recent 2>/dev/null | /usr/bin/grep -E "SYSCALL|PATH" | /usr/bin/tail -n 5 || echo "None"
echo ""
echo "Privilege escalation (polkit):"
/usr/sbin/ausearch -k privilege_escalation -ts recent 2>/dev/null | /usr/bin/grep -E "SYSCALL|PATH" | /usr/bin/tail -n 5 || echo "None"
echo ""
echo "Systemd changes:"
/usr/sbin/ausearch -k systemd_changes -ts recent 2>/dev/null | /usr/bin/grep -E "SYSCALL|PATH" | /usr/bin/tail -n 5 || echo "None"
echo ""
echo "Firewall/network changes:"
/usr/sbin/ausearch -k firewall_changes -ts recent 2>/dev/null | /usr/bin/grep -E "SYSCALL|PATH" | /usr/bin/tail -n 5 || echo "None"
echo ""
echo "Canary trips:"
/usr/sbin/ausearch -k canary_trip -ts recent 2>/dev/null | /usr/bin/grep -E "SYSCALL|PATH" | /usr/bin/tail -n 5 || echo "None"
echo ""
echo "SUID drift:"
if [[ -s /var/lib/suid-scanner/diff.txt ]]; then
/usr/bin/cat /var/lib/suid-scanner/diff.txt
else
echo "None (baseline: $(/usr/bin/wc -l < /var/lib/suid-scanner/baseline.txt 2>/dev/null || echo 0) mutable SUID binaries)"
fi
echo ""
echo "run0 executions:"
/usr/sbin/ausearch -k privesc_run0 -ts recent 2>/dev/null | /usr/bin/grep -E "SYSCALL|PATH" | /usr/bin/tail -n 5 || echo "None"
echo ""
echo "Account management:"
/usr/sbin/ausearch -k privesc_account -ts recent 2>/dev/null | /usr/bin/grep -E "SYSCALL|PATH" | /usr/bin/tail -n 5 || echo "None"
echo ""
echo "SUID bit changes:"
/usr/sbin/ausearch -k privesc_suid -ts recent 2>/dev/null | /usr/bin/grep -E "SYSCALL|PATH" | /usr/bin/tail -n 5 || echo "None"
echo ""
echo "Kernel module loading:"
/usr/sbin/ausearch -k privesc_kmod -ts recent 2>/dev/null | /usr/bin/grep -E "SYSCALL|PATH" | /usr/bin/tail -n 5 || echo "None"
echo ""
echo "eBPF activity:"
/usr/sbin/ausearch -k privesc_bpf -ts recent 2>/dev/null | /usr/bin/grep -E "SYSCALL|PATH" | /usr/bin/tail -n 5 || echo "None"
echo ""
echo "Capability changes:"
/usr/sbin/ausearch -k privesc_caps -ts recent 2>/dev/null | /usr/bin/grep -E "SYSCALL|PATH" | /usr/bin/tail -n 5 || echo "None"
echo ""
echo "Persistence changes:"
/usr/sbin/ausearch -k privesc_persist -ts recent 2>/dev/null | /usr/bin/grep -E "SYSCALL|PATH" | /usr/bin/tail -n 5 || echo "None"
echo ""
echo "AIDE status:"
if /usr/bin/systemctl is-active aide-check.timer >/dev/null 2>&1; then
echo "AIDE timer active"
else
echo "AIDE timer inactive"
fi
EOF
run0 /usr/bin/chmod +x /usr/local/bin/audit-review
# Timer unit for daily review
run0 /usr/bin/tee /etc/systemd/system/audit-review.timer << 'EOF'
[Unit]
Description=Daily audit summary
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
EOF
# Service unit that executes the review script
run0 /usr/bin/tee /etc/systemd/system/audit-review.service << 'EOF'
[Unit]
Description=Daily audit review
[Service]
Type=oneshot
ExecStart=/usr/local/bin/audit-review
StandardOutput=journal
EOF
run0 /usr/bin/systemctl daemon-reload
run0 /usr/bin/systemctl enable --now audit-review.timer
/usr/sbin/ausearch -m AVC -ts recent. secureblue's strict policy may block audit tools from accessing certain contexts. You may need to generate a local policy module with audit2allow if denials persist. This is beyond this guide's scope but may be necessary on hardened deployments.
# 1. Verify all timers are active & scheduled correctly
# Look for aide-check.timer, suid-scanner.timer & audit-review.timer
run0 /usr/bin/systemctl list-timers --no-pager
# Example output from my machine:
# NEXT LEFT LAST PASSED UNIT ACTIVATES
# Sat 2026-05-02 00:00:00 EDT 11h left Fri 2026-05-01 00:00:02 EDT 12h ago aide-check.timer aide-check.service
# Sat 2026-05-02 00:00:00 EDT 11h left Fri 2026-05-01 00:00:02 EDT 12h ago suid-scanner.timer suid-scanner.service
# Sat 2026-05-02 00:00:00 EDT 11h left Fri 2026-05-01 00:00:02 EDT 12h ago audit-review.timer audit-review.service
# 2. Confirm audit rules loaded
# If 0: check systemctl status auditd; check for reboot completion after Phase 1
run0 /usr/sbin/auditctl -l | /usr/bin/wc -l
# Example output from my machine:
# 44
# 3. Confirm AIDE database exists & has reasonable size
# If missing: re-run Phase 3 step 2
run0 /usr/bin/ls -la /var/lib/aide/aide.db.gz
# Example output from my machine:
# -rw-------. 1 root root 18M May 1 09:30 /var/lib/aide/aide.db.gz
# 4. Run manual review & inspect output for any existing alerts
# This should show "None" across all categories on a clean system
run0 /usr/local/bin/audit-review
# Example output from my machine (clean system):
# === AUDIT SUMMARY ===
# Active rules: 44
#
# Identity changes: None
# Linker hijack attempts: None
# Privilege escalation (polkit): None
# ...
# SUID drift: None (baseline: 0 mutable SUID binaries)
# AIDE timer active
# 5. Test AIDE check manually (same command the timer uses)
# Expected: no output if clean, or detailed diff if changes detected
# If segfault: static link failed; rebuild AIDE in Phase 0 & verify ldd output
run0 /usr/local/bin/aide --check
# Example output from my machine (clean system):
# [no output = all files match baseline]
bison & flex) are rpm-ostree layered packages that expand the attack surface. Compilers are prime targets for privilege escalation if an attacker gains local access. Remove them immediately after AIDE is verified working. The static AIDE binary & its database remain fully functional without these tools.
# Remove bison & flex layered via rpm-ostree in Phase 0
# AIDE remains functional because it is statically linked & installed to /usr/local
rpm-ostree uninstall bison flex
# REBOOT NOW to finalize removal & restore minimal attack surface
systemctl reboot
# Remove the local build directory where nettle, pcre2 & zlib were compiled
rm -rf /var/home/$(whoami)/aide-static-deps
# Remove source tarballs & extracted directories if still present
rm -rf /var/home/$(whoami)/nettle-4.0* /var/home/$(whoami)/pcre2-* /var/home/$(whoami)/zlib-* /var/home/$(whoami)/aide-*
# Confirm rpm-ostree build tools are no longer available
/usr/bin/which bison # should return nothing
/usr/bin/which flex # should return nothing
# Confirm local build artifacts are removed
/usr/bin/ls -la /var/home/$(whoami)/aide-static-deps 2>&1 # should report "No such file or directory"
# Confirm static AIDE still functions normally
/usr/local/bin/aide --version
# Confirm all security timers are still active after reboot
run0 /usr/bin/systemctl list-timers --no-pager | /usr/bin/grep -E "aide|suid|audit"
bison & flex via rpm-ostree, rebuild nettle/pcre2/zlib from source to a local prefix, then build AIDE. Verify & remove bison/flex again using this same phase. The cycle is: install bison/flex → build deps from source → build AIDE → verify → uninstall bison/flex → clean local build dir → reboot. Just update the download URLs in the build commands before each rebuild.
nettle-devel, pcre2-devel, zlib-devel, glibc-static), remove them separately with rpm-ostree uninstall. They are not part of the guide's intended procedure.
These are issues I personally encountered while building this stack on my machine. They may not match your exact error, but the fix patterns should transfer.
run0 /usr/local/bin/aide --init or --check crashes with a segfault or memory error.hardened_malloc conflicts with dynamically linked AIDE. The Fedora package bundles glibc malloc hooks that hardened_malloc overrides.LD_PRELOAD and uses its own bundled allocator. Verify with:/usr/bin/ldd /usr/local/bin/aide
# Expected: "not a dynamic executable" or empty output
LD_PRELOAD=/usr/lib64/libhardened_malloc.so /usr/local/bin/aide --version
# Expected: runs normally without crash
run0 /usr/sbin/augenrules --load prints "No rules" followed by warnings.augenrules checks the legacy /etc/audit/audit.rules file first. If it is empty, it reports "No rules" even while successfully loading rules from /etc/audit/rules.d/. The warnings about "Old style watch rules are slower" confirm the rules did load.run0 /usr/sbin/auditctl -l | /usr/bin/wc -l or run0 /usr/sbin/ausearch -k privesc_run0 -ts recent.run0 systemctl restart auditd returns "Operation refused, unit auditd.service may be requested by dependency only."run0 /usr/sbin/auditctl -R /etc/audit/rules.d/99-privesc.rules. The daemon is already running; you only need to refresh the kernel configuration.If you've made it this far, you now know that building a detection layer on an immutable system is awkward. You'll hit friction that standard guides simply don't mention. The issues above are what stopped me personally. If you find another, file it and I'll likely add it. I would offer that the stack is worth the afternoon it takes to set up. Best of luck.