Skip to main content
RunBook Academy

LinuxXXXIV · Configuration ManagementDrift detection

Config drift detection - keeping reality aligned with declared state

Intermediate⏱ ~10 minansiblecronmonitoring

What you'll learn

  • Detect configuration drift
  • Schedule drift checks
  • Alert on critical drift
  • Remediate automatically

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09

Not yet marked complete on this device.

Configuration drift is the silent killer of infrastructure. Hosts that started identical end up different; nobody knows when or why. Drift detection catches this.

What is drift?

Drift is any change to a host’s configuration that is not recorded in the desired state:

  • A user manually edits /etc/ssh/sshd_config.
  • A package is installed ad-hoc.
  • A service is enabled but should not be.
  • A file’s permissions change.

Drift is dangerous because the desired state is no longer true, but the CM system thinks it is.

Detect drift with Ansible

# Check mode: report what would change without making changes
ansible-playbook -i prod site.yml --check --diff

Output:

TASK [Configure nginx]
--- before
+++ after
@@ -1,3 +1,3 @@
 worker_processes 1;
-listen 80;
+listen 8080;

changed: [web01]

The diff shows exactly what is drifted.

Schedule drift checks

# /etc/cron.d/ansible-drift-check
0 2 * * * root install -m 0600 -o root -g root /dev/null /var/log/drift-check.log && /usr/bin/ansible-playbook -i /etc/ansible/prod /etc/ansible/site.yml --check --diff >> /var/log/drift-check.log 2>&1

Daily checks. Review the log weekly.

The install -m 0600 step is not decoration. Root cron runs with umask 022, so a shell redirection creates the log mode 0644 - readable by every local account on the host. The next section explains why that matters.

Add the file to logrotate with the same mode, or the first rotation undoes the fix:

# /etc/logrotate.d/drift-check
/var/log/drift-check.log {
    weekly
    rotate 8
    create 0600 root root
    missingok
}

Alert on drift

Run the check from the Ansible control node, and drive the alert off the changed count:

#!/usr/bin/env bash
# /usr/local/sbin/drift-check - runs on the control node
set -uo pipefail

out=$(ANSIBLE_STDOUT_CALLBACK=json ansible-playbook \
        -i /etc/ansible/prod /etc/ansible/site.yml --check 2>&1)
rc=$?

if [ "$rc" -ne 0 ]; then
    echo "drift-check FAILED rc=$rc"   # a broken check, not drift
    exit 2
fi

changed=$(printf '%s' "$out" | jq '[.stats[].changed] | add')
if [ "${changed:-0}" -gt 0 ]; then
    echo "DRIFT: $changed resource(s) differ from desired state"
    exit 1        # this is the signal monitoring alerts on
fi
exit 0

Exit 1 means drift, exit 2 means the detector itself is broken. Alert on both - a check that cannot run is not a clean check.

Without the JSON callback, parse the play recap instead:

drifted=$(printf '%s\n' "$out" | awk '/PLAY RECAP/,0' \
  | grep -oP 'changed=\K[0-9]+' | awk '{s+=$1} END{print s+0}')
[ "$drifted" -gt 0 ] && logger -t drift "drift on $drifted host-task(s)"

Remediate

For critical drift (e.g. SSH config change), auto-remediate:

# Run the playbook for real, not in check mode
ansible-playbook -i prod site.yml --limit web01

For non-critical drift (e.g. cosmetic), log and review: manual decision.

Track drift over time

Store drift reports in a git repo or SIEM:

day=/var/log/drift-history/$(date +%Y%m%d)
install -d -m 0700 -o root -g root "$day"
install -m 0600 -o root -g root /dev/null "$day/$(date +%H%M).log"
ansible-playbook -i /etc/ansible/prod /etc/ansible/site.yml \
  --check --diff >> "$day/$(date +%H%M).log" 2>&1

The permissions carry more weight here than in the nightly job. A history directory keeps every diff indefinitely, so a single unredacted secret stays on disk - and in every host backup taken since - until someone notices. Prefer storing the drift summary (host, task, resource changed) rather than the raw diff, and keep the raw diff only where the secrets have been suppressed with no_log.

Trend analysis: a host with increasing drift over time suggests it is being managed manually, not by CM.

What to check

For each host, check:

  • Critical config files (/etc/ssh/sshd_config, /etc/pam.d/*, /etc/sudoers).
  • Installed packages (vs the desired set).
  • Running services (vs the desired set).
  • File permissions on sensitive files.
  • User accounts (no ad-hoc additions).
  • Sudo rules (no ad-hoc additions).

Knowledge check

Knowledge check · 5 questions

  1. Q1. What Ansible flag runs a playbook in check mode?

  2. Q2. Drift detection should run weekly.

  3. Q3. Which of the following are typical drift check targets? Select all that apply.

  4. Q4. Your nightly drift check runs ansible-playbook --check and alerts when the exit code is non-zero. It has reported clean for six months. What is the most likely explanation?

  5. Q5. Redirecting ansible-playbook --check --diff into /var/log/drift-check.log is safe, because secret files are deployed with mode 0600.

Passing score: 75%. Answers are checked in this browser.