Skip to main content
RunBook Academy

LinuxLXXIV · Configuration DriftRemediation

Drift remediation - bringing hosts back to desired state

Intermediate⏱ ~10 minansiblepcs

What you'll learn

  • Capture drift evidence and investigate the cause before reverting
  • Remediate drift to the desired state, scoped to the affected host
  • Update CM to include intentional changes
  • Prevent future drift with a scoped, jittered, locked reapply
  • Document the remediation

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.

Drift remediation brings hosts back to the desired state and prevents the drift from recurring. This lesson covers the runbook.

The order of the runbook is the whole lesson: capture, investigate, decide, remediate. Reverting first feels efficient and is the single most common way an automated drift programme re-opens a resolved incident.

Step 1: Capture the evidence

Before you touch anything, take a copy and find out who changed it:

# Snapshot the drifted file with its metadata intact
sudo cp -a /etc/nginx/nginx.conf \
  /var/tmp/drift-$(date +%FT%H%M)-nginx.conf

# Who changed it, and when (requires an auditd watch on the path)
sudo ausearch -f /etc/nginx/nginx.conf -ts recent

# What exactly differs from desired state - reports only, changes nothing
ansible-playbook -i prod site.yml --check --diff --limit web01

--check --diff is the read-only form: it prints the diff the CM run would apply without applying it. Run it scoped to the single affected host with --limit.

Step 2: Investigate

Why did the drift happen?

  • Was it a manual change by an operator?
  • Was it a package update that rewrote a config?
  • Was it an emergency change?
  • Was it a test that was not cleaned up?

The decisive question is the third one. Check the incident record and the change log for this host before you decide anything. If a change was made during an incident, confirm the incident is closed and that the mitigation is no longer load-bearing.

Document the cause in the runbook. The same drift should not recur.

Step 3: Decide

FindingAction
Intentional and still wantedAdd it to CM, then re-apply so it becomes desired state
UnintentionalRevert it via CM
Mitigation for an open incidentDo not revert. Escalate to the incident owner
Cause unknownDo not revert yet. Keep investigating; an unexplained config change on a production host is a security question, not a tidiness question

Step 4: Remediate

Only now apply the desired state, and scope it to the host you investigated:

# Apply the CM to the affected host only
ansible-playbook -i prod site.yml --limit web01

# Verify drift is gone
ansible-playbook -i prod site.yml --check --diff --limit web01

The output should be empty (or near-empty for known non-managed files).

The --limit is not optional. Without it this command drives a run across every host in the prod inventory - a fleet-wide production change, triggered by a single host’s drift, with no batching and no canary.

Update CM to include intentional changes

If the drift is intentional (e.g. an operator made a needed change):

  1. Add the change to the CM.
  2. Re-apply CM; the change is now desired state.
  3. Drift detection no longer flags it.

If the drift is unintentional, do not add it to CM. Revert the change.

Prevent future drift

The prevention patterns:

  • No manual changes: all changes through CM.
  • Ad-hoc SSH alerting: detect manual changes via SSH sessions.
  • Configuration file monitoring: alert on changes to /etc outside CM.
  • Periodic reapply: run CM on a schedule to revert drift automatically.

Periodic reapply is the one that needs care. Automatic remediation is itself a production change, so it has to obey the same controls as any other production change.

Consider what this apparently harmless crontab line does:

# WRONG - do not deploy this
0 4 * * * root ansible-playbook -i /etc/ansible/prod /etc/ansible/site.yml

There is no --limit, so the inventory is the whole fleet. Every host that runs this line drives its own fleet-wide run. On 200 hosts that is 200 concurrent full-fleet runs starting in the same minute, all hammering the inventory source and the secrets backend, and overlapping on each other’s package state. Any task that notifies a service restart restarts that service on every host at once - a total outage of that service, with no gradual signal that anything is wrong.

The correct pattern is a pull model: each host remediates only itself, on a jittered schedule, with a lock so runs cannot overlap.

# /etc/systemd/system/config-pull.service
[Service]
Type=oneshot
ExecStart=/usr/bin/flock -n /run/config-pull.lock \
  /usr/bin/ansible-pull -U https://git.example.com/ops/cm.git -C main \
    --only-if-changed local.yml
# /etc/systemd/system/config-pull.timer - spread the fleet out
[Timer]
OnCalendar=*-*-* 04:00:00
RandomizedDelaySec=1800

flock -n makes a second run exit immediately rather than stack on the first. RandomizedDelaySec=1800 spreads the fleet across half an hour so a bad commit does not land everywhere at the same instant. --only-if-changed skips the run entirely when the repository has not moved.

If you must keep a push model, scope it to the local host and batch it:

ansible-playbook -i /etc/ansible/prod /etc/ansible/site.yml \
  --limit "$(hostname -s)"

and set an abort threshold in the play itself:

- hosts: web
  serial: "5%"
  max_fail_percentage: 0

serial: "5%" runs the fleet in waves. max_fail_percentage: 0 stops the whole play the moment a wave fails, so a bad commit reaches one wave rather than all of production.

Document the remediation

For each drift remediation:

DRIFT REMEDIATION
=================
Host: web01
Date: 2026-08-09
Drift: /etc/nginx/nginx.conf was manually modified
Evidence: /var/tmp/drift-2026-08-09T0912-nginx.conf (copy taken before any change)
Cause: Operator edit during INC-4471 on 2026-08-08 - added
       limit_req to shed load from a scraper
Incident state: INC-4471 closed 2026-08-08 21:40; upstream rate
       limiting now handled at the edge, so the local limit is
       no longer load-bearing (confirmed with incident owner)
Decision: safe to revert
Action: ansible-playbook -i prod site.yml --limit web01
Prevention: config-pull.timer on web01, jittered, host-scoped
Followup: none

Read the Incident state line. That line is the reason this revert was safe. Without it, the correct action was to leave the file alone and escalate.

The documentation is the audit trail. The next drift investigation can look back and see what happened.

Knowledge check

Knowledge check · 5 questions

  1. Q1. What is the first step in drift remediation?

  2. Q2. Drift remediation is a one-time fix.

  3. Q3. Which of the following are valid drift remediation steps? Select all that apply.

  4. Q4. Drift detection flags a changed nginx.conf on web01 at 02:00. Git blame is no help - the file is not in CM. The on-call engineer edited it four hours ago and the incident is still open. What do you do?

  5. Q5. You put `0 4 * * * root ansible-playbook -i /etc/ansible/prod /etc/ansible/site.yml` on all 200 production hosts. What is the blast radius at 04:00?

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