The CIS RHEL9 hardening playbook ran clean on two dev servers. Same playbook, same roles, same variables. Then it hit the Tenable server and locked mid-run on the AIDE init task. No error message that pointed anywhere useful. Just a process that wouldn't finish.
This is what happened, why the Tenable server specifically caused it, and the fix that handles it cleanly.
When Ansible runs aide --init, AIDE walks the entire filesystem, checksums every file it's configured to monitor, and writes the result to /var/lib/aide/aide.db.new.gz. On a clean dev server with a minimal footprint that takes 2–5 minutes. On a server running Nessus it can take 30 minutes or more — and that's when the lock conflict probability spikes.
AIDE and Nessus are competing for the same file handles. Nessus continuously reads plugin files, writes scan databases, and updates log files. AIDE is trying to open and checksum those same files. The lock isn't always fatal — sometimes the playbook just hangs waiting for a file handle that never frees. Sometimes the aide --init process errors with a lock file conflict. Either way the task stalls.
The dev servers had no database, no active scanning agent, and no files being continuously written. AIDE init ran against a static filesystem. The Tenable server had:
That's significantly more filesystem activity than a vanilla RHEL server. The original Ansible task only asked one question before running aide --init:
when: not aide_db.stat.exists
Does aide.db.gz exist? No? Run init. That logic works fine when nothing else is touching the filesystem. It fails when Nessus is running because it doesn't account for a partially completed init, a currently running AIDE process, or files locked by another process.
- name: Initialize AIDE database
ansible.builtin.command: aide --init
when: not aide_db.stat.exists
async: 3600
poll: 30
One condition. No awareness of whether AIDE was already running. No check for aide.db.new.gz which is the intermediate file AIDE writes before the final database exists. If a previous run started aide --init and got interrupted, aide.db.new.gz exists but aide.db.gz doesn't — and the original task would fire again, creating a second AIDE process trying to init against the same filesystem at the same time.
That's the exact scenario that produces the lock conflict on the Tenable server.
The fix replaces the single task with five tasks that cover every scenario:
- name: "1.3.1 | Check if AIDE database exists"
ansible.builtin.stat:
path: /var/lib/aide/aide.db.gz
register: aide_db
- name: "1.3.1 | Check if AIDE init already in progress"
ansible.builtin.stat:
path: /var/lib/aide/aide.db.new.gz
register: aide_db_new
- name: "1.3.1 | Wait for any running AIDE process to complete"
ansible.builtin.shell: |
timeout=120
while pgrep -x aide > /dev/null && [ $timeout -gt 0 ]; do
sleep 5
timeout=$((timeout - 5))
done
pgrep -x aide > /dev/null && echo "running" || echo "clear"
register: aide_wait
changed_when: false
when:
- not aide_db.stat.exists
- not aide_db_new.stat.exists
- name: "1.3.1 | Initialize AIDE database"
ansible.builtin.command: aide --init
async: 3600
poll: 30
when:
- not aide_db.stat.exists
- not aide_db_new.stat.exists
- aide_wait.stdout is defined
- aide_wait.stdout == 'clear'
- name: "1.3.1 | Move AIDE database into place"
ansible.builtin.command: >
mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
when: >
not aide_db.stat.exists and
(aide_db_new.stat.exists or
(aide_wait.stdout is defined and aide_wait.stdout == 'clear'))
The original task asked one question. This asks three before running init:
Does aide.db.gz exist? If yes — done, skip everything.
Does aide.db.new.gz exist? If yes — a previous init completed or is in progress, skip init and go straight to the move task.
Is AIDE currently running? If yes — wait up to 120 seconds for it to finish, then check again.
Only when all three conditions are clear does init fire.
Fixing the init conflict surfaces a second issue specific to Tenable servers. Once AIDE initializes successfully, the daily aide --check cron will generate significant noise because Nessus plugin files update constantly. Every plugin update, every scan result write, every log rotation shows as a file integrity change.
The fix is excluding Nessus directories from AIDE monitoring entirely:
- name: "1.3.1 | Configure AIDE exclusions for Nessus"
ansible.builtin.blockinfile:
path: /etc/aide.conf
block: |
!/opt/nessus/.*
!/var/opt/nessus/.*
marker: "# {mark} AIDE NESSUS EXCLUSIONS"
when: ansible_hostname is search('nessus|tenable|scanner')
Run this exclusion task before aide --init. If you add exclusions after initialization, the existing database reflects the full filesystem including Nessus files. Your next aide --check will flag every Nessus file as changed.
The exclusion targets the Nessus installation and data directories. AIDE still monitors everything that matters for compliance — OS binaries, system configs, PAM files, sudoers, SSH configuration. It just stops chasing Nessus plugin updates that will never stop changing.
The same conflict can occur on any server running a tool that continuously reads or writes large numbers of files. DTEX endpoint agent, Microsoft Defender, CrowdStrike Falcon, and other EDR agents all have similar filesystem activity patterns. Before running AIDE init on any server running a security agent:
Check what's running:
ps aux | grep -E "nessus|falcon|dtex|defender|sentinel"
Check what the agent's data directory looks like:
du -sh /opt/nessus /var/opt/nessus 2>/dev/null
find /opt/nessus -newer /etc/hostname -type f 2>/dev/null | wc -l
If the agent directory has thousands of recently modified files, add exclusions before init.
Run the playbook with the updated AIDE tasks:
ansible-playbook cis-rhel9-l1.yml --tags aide -l tenable_servers
Confirm the database initialized correctly:
ls -lh /var/lib/aide/aide.db.gz
aide --check | head -20
Verify Nessus exclusions are in the AIDE config:
grep -A3 "NESSUS" /etc/aide.conf
Confirm no duplicate AIDE processes:
pgrep -x aide
The Tenable server isn't a problem. It just required the hardening playbook to be aware of what else is running on it. Same CIS controls, same compliance target — the implementation just needs to account for the environment it's running against.
These posts come from real production environments — not labs, not documentation. If that's the kind of writing you want more of, follow Root & Secure on Medium.