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.lockdown=confidentiality at boot, which disables the audit subsystem even though the Fedora kernel has CONFIG_AUDIT compiled in. These rules cannot function on stock secureblue. They are documented here only for users running custom secureblue builds without lockdown.lockdown=confidentiality.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 instantlylockdown=confidentiality disables real-time audit at runtime, making offline integrity tools the only viable detection layer/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.
lockdown=confidentiality at boot, which disables the audit subsystem even though the Fedora kernel has CONFIG_AUDIT compiled in. This means real-time audit rules, canary trips, and privilege escalation detection cannot function on stock secureblue. AIDE and the SUID scanner are unaffected and remain fully functional.
# Verify the audit subsystem is disabled by lockdown
ls /sys/kernel/security/audit/ 2>/dev/null && echo "AUDIT AVAILABLE" || echo "AUDIT DISABLED"
# Check if lockdown=confidentiality is in your boot params
cat /proc/cmdline | grep lockdown
# auditctl will return nothing even though auditd is running
/usr/sbin/auditctl -s 2>/dev/null && echo "AUDITCTL WORKS" || echo "AUDITCTL DISABLED"
CONFIG_AUDIT is already compiled in. The lockdown parameter disables it at runtime. Skip all audit-based components (Phase 2 rules, canary files) and use only AIDE (Phase 3) and the SUID scanner (Phase 4). The guide is designed to work as an offline integrity monitoring stack on stock secureblue.
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 will fail.
Core stack: No packages are required. AIDE is built from source in Phase 0, and the SUID scanner and daily review are bash scripts. No RPM installation is needed for the offline integrity monitoring stack.
Optional install (only for custom builds without lockdown):
# Install audit framework for real-time monitoring
# Only useful if you are running a custom secureblue build without lockdown=confidentiality
rpm-ostree install audit
# REBOOT NOW
systemctl reboot
/sys/kernel/security/audit/ does not exist. On stock secureblue, this is expected because lockdown=confidentiality disables the audit subsystem at runtime. Proceed directly to Phase 3 (AIDE). These rules are documented only for users running custom secureblue builds without lockdown.
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.
run0 tee ... << 'EOF' can corrupt if your terminal emulator mishandles clipboard paste or if the heredoc delimiter is accidentally modified. After every file creation, verify the first few lines with run0 head -n 5 /path/to/file. If you see [paste: garbage or other corruption, delete the file and recreate it using a local temp file: tee ~/file.tmp << 'EOF' ... EOF then run0 mv ~/file.tmp /path/to/file.
systemctl and journalctl are polkit-aware and do not need a run0 prefix. Running systemctl enable ... or journalctl -u ... directly will prompt for authentication via polkit when required. Only use run0 for commands that lack native polkit integration, such as tee, mkdir, mv, auditctl, aide, and ausearch.
$USER variable in the canary path below expands to your username before run0 executes. Verify the output matches your username by running run0 /usr/bin/grep canary /etc/audit/rules.d/99-secureblue-custom.rules after creation.
# 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
run0 /usr/bin/head -n 5 /etc/audit/rules.d/99-secureblue-custom.rules
/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
run0 /usr/bin/head -n 5 /etc/audit/rules.d/98-privesc.rules
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
systemctl daemon-reload
# Enable & start the timer (the service runs immediately, then daily thereafter)
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
run0 /usr/bin/head -n 5 /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
systemctl daemon-reload
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.
lockdown=confidentiality prevents the audit subsystem from generating events. The daily review script will report zero active rules and empty audit categories because no audit events are being generated. This is expected. The guide provides an AIDE-only version that works on all secureblue deployments.
wheel, adm, or systemd-journal groups. As a result, journalctl -u audit-review.service will show "No entries" from your primary account. Additionally, run0 -u admin journalctl and run0 journalctl both fail to display oneshot service output properly on secureblue. The only reliable method is file-based output. The guide below configures this by default.
Storage=auto with MaxFileSec=1month, which is adequate. If /etc/systemd/journald.conf does not exist, check whether /var/log/journal/ exists. If it does, persistent storage is already active via systemd defaults. If your system has Storage=volatile or aggressive vacuuming configured, audit events may be lost before the daily review runs. 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
run0 /usr/bin/head -n 5 /usr/local/bin/audit-review
ausearch hangs, auditd may be down or the script may be corrupted.
# Test manually before enabling the timer
run0 timeout 30 /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
systemctl daemon-reload
systemctl enable --now audit-review.timer
secureblue's user/admin split means your primary user cannot read system journals. Choose based on your security/convenience preference.
Log out of your primary user and log in as your admin account, then read the journal. This is secureblue's intended model: the HIDS output stays behind the privilege boundary.
# From the admin account
run0 -u admin journalctl -u audit-review.service -n 200
# Or since last boot
run0 -u admin journalctl -u audit-review.service -b
Use run0 to read the journal from your primary user. This prompts for the admin password each time but requires no configuration changes and keeps output inside the journal.
# Read from primary user account (will prompt for admin password)
run0 journalctl -u audit-review.service -n 200
# Or since last boot
run0 journalctl -u audit-review.service -b
Reconfigure the service to append output to a file in /var/log with broad read permissions:
# Reconfigure the service to write to a file
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=append:/var/log/audit-review.log
EOF
# Create the log file with read permissions for all users
run0 /usr/bin/touch /var/log/audit-review.log
run0 /usr/bin/chmod 644 /var/log/audit-review.log
# Reload and restart
systemctl daemon-reload
systemctl restart audit-review.service
# Now your primary user can read it directly
cat /var/log/audit-review.log
# 1. Verify core timers are active & scheduled correctly
# Look for aide-check.timer, suid-scanner.timer & audit-review.timer
systemctl list-timers --no-pager
# Example output:
# 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 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:
# -rw-------. 1 root root 18M May 1 09:30 /var/lib/aide/aide.db.gz
# 3. Run manual review & inspect output for any existing alerts
# This should show "None" for SUID drift and "AIDE timer active" on a clean system
run0 /usr/local/bin/audit-review
# To see the systemd service output, read the log file:
# run0 cat /var/log/audit-review.log
# Example output (stock secureblue, clean):
# === AUDIT SUMMARY ===
# AIDE timer active
#
# SUID drift: None (baseline: 0 mutable SUID binaries)
#
# Note: Real-time audit monitoring is disabled by lockdown=confidentiality.
# Only offline integrity detection (AIDE + SUID scanner) is active.
# 4. 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 (clean system):
# [no output = all files match baseline]
Only if you are running a custom secureblue build that removed lockdown=confidentiality and installed audit rules in Phase 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:
# 44
# Check for SELinux denials if audit tools fail silently
run0 /usr/sbin/ausearch -m AVC -ts recent
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
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.run0 tee ... << 'EOF' fails with [paste: command not found or contains unexpected text.run0:
tee ~/audit-review.tmp << 'EOF'
#!/bin/bash
[your script contents]
EOF
run0 mv ~/audit-review.tmp /usr/local/bin/audit-review
run0 chmod +x /usr/local/bin/audit-review
run0 head -n 5 /usr/local/bin/audit-review
Then verify the first few lines look correct before proceeding.run0 /usr/sbin/auditctl -l returns 0, or run0 /usr/sbin/auditctl -s returns nothing, even after loading rules.lockdown=confidentiality at boot, which disables the audit subsystem at runtime. The Fedora kernel has CONFIG_AUDIT compiled in, but lockdown prevents it from functioning. This is an intentional security hardening choice, not a missing kernel feature.cat /proc/cmdline | grep lockdown. If you see lockdown=confidentiality, the audit subsystem is intentionally disabled. Do not attempt to rebuild the kernel; this will not help. Options: (1) Use a custom secureblue build without lockdown (not recommended, as it weakens other protections). (2) Accept that only offline integrity tools (AIDE, SUID scanner) are available on stock secureblue. This is the intended design.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.