LinuxLXXIV · Configuration DriftCoverage
The limits of drift detection - measuring what nothing manages
What you'll learn
- Explain why a clean drift report is a statement about coverage, not about the host
- Measure how much of a host is owned by a package or declared by configuration management
- Classify unmanaged files into must-manage, deliberately-unmanaged and unknown
- Report coverage alongside drift so a green report cannot be misread
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-11
Every drift control in this part reports the same thing when everything is fine: nothing. And “nothing” carries two completely different meanings that no report distinguishes.
- Nothing has drifted from what we declared.
- Nothing we declared has drifted, and we declared very little.
A configuration management tool reporting “no changes” has made a precise, narrow statement: it compared reality against its own declarations and found agreement. It has said nothing whatsoever about the parts of the host it was never told about — and in most environments that is the larger part.
The unmanaged file is where drift lives, because it is the only place drift can live undetected.
Three states, not two
Every file on a host is in one of three states, and drift detection distinguishes only the first two:
| State | Detected by | Drifts silently? |
|---|---|---|
| Declared by configuration management | CM dry-run | No |
| Owned by a package | dpkg --verify, rpm -V | No |
| Neither | Nothing, by definition | Yes, indefinitely |
The third row is not a small residue. It contains everything
configuration management wrote and later stopped managing,
everything an operator created, every file an application writes
into /etc, every leftover from a deleted playbook task, and
every .dpkg-old backup of a customisation somebody used to care
about.
Measuring coverage
Coverage is a number you can compute today, on any host, with tools that are already installed. Start with the package-owned set, because it is the easy half:
# Every path under /etc that some installed package owns
export LC_ALL=C
dpkg-query -W -f='${binary:Package}\n' 2>/dev/null \
| xargs dpkg -L 2>/dev/null \
| grep '^/etc/' | sort -u > /var/tmp/etc-package-owned.txt
# Every regular file actually present under /etc
find /etc -xdev -type f 2>/dev/null | sort -u > /var/tmp/etc-present.txt
# The difference is the set nothing in the package system owns
comm -23 /var/tmp/etc-present.txt /var/tmp/etc-package-owned.txt \
> /var/tmp/etc-unowned.txt
wc -l /var/tmp/etc-present.txt /var/tmp/etc-unowned.txt
$ wc -l /var/tmp/etc-present.txt /var/tmp/etc-unowned.txt 897 /var/tmp/etc-present.txt
139 /var/tmp/etc-unowned.txt
1036 totalIllustrative output
Those 139 files are not automatically a problem. They are the candidate set: the files that package verification can never report on, so they are covered only if configuration management declares them, and you now have a concrete list to check that against.
For the configuration-management half, the reliable technique is to mark what the tool manages so it can be counted later. A header on every templated file works and costs nothing:
# Files declared by CM, if your templates carry a marker line
grep -rl 'MANAGED BY ANSIBLE' /etc 2>/dev/null | sort -u \
> /var/tmp/etc-cm-managed.txt
# Unowned by a package AND not marked as managed: the real gap
export LC_ALL=C
comm -23 /var/tmp/etc-unowned.txt /var/tmp/etc-cm-managed.txt
An extended attribute is the tidier version, and does not require
the file format to tolerate comments — linux-extended-and-immutable-attributes
covers user.origin, which several configuration management
tools already set for exactly this purpose:
# Substitute your own values before running:
F=/etc/nginx/conf.d/upstream.conf
getfattr -d -m 'user\.' "$F" 2>/dev/null
Classify the gap, do not just measure it
A list of 139 unmanaged files is not actionable and will be ignored. Sorting them into three buckets is, and the bucket assignment is a decision that gets recorded:
| Bucket | Meaning | Action |
|---|---|---|
| Must be managed | Security-relevant or service-defining: firewall, PAM, sudo, SSH, TLS material, anything the compliance baseline names | Declare it in CM, this quarter |
| Deliberately unmanaged | Genuinely host-specific or application-owned state that belongs on the host: machine ID, generated keys, local caches | Record the decision, exclude explicitly |
| Unknown | Nobody knows what wrote it or whether it matters | The backlog. This bucket is the real finding |
Two filters make the initial triage fast. Modification time groups files by the event that created them:
# Unowned files changed in the last 90 days - recent activity first
export LC_ALL=C
while IFS= read -r f; do
[ -f "$f" ] && printf '%s %s\n' "$(stat -c '%y' "$f" | cut -d' ' -f1)" "$f"
done < /var/tmp/etc-unowned.txt | sort -r | head -20
# Backup and leftover files: each one marks a decision nobody reviewed
grep -E '\.(dpkg-old|dpkg-dist|dpkg-new|rpmsave|rpmnew|ucf-old|bak|orig|save|[0-9]{8})$' \
/var/tmp/etc-unowned.txt
That second filter is consistently productive. sshd_config.bak,
sudoers.orig, nginx.conf.20250114 — each is a snapshot
somebody took before a change, left in place, and forgot. They
are unmanaged by definition, they frequently contain credentials
that were rotated afterwards, and in the case of sudoers.d and
sshd_config.d drop-in directories a stray backup file may still
be read by the service.
Reporting coverage next to drift
The fix for a misreadable green report is to publish the denominator with it:
DRIFT REPORT - role: web - 2026-08-11
Hosts: 12
Drift findings: 0 (CM dry-run, all hosts)
Package verification: 2 findings (web03: modified binary in nginx)
Coverage:
/etc files present, median per host 897
owned by a package 758 (84%)
declared by CM but not package-owned 61
neither - UNMANAGED 78
of which: classified deliberately 44
MUST be managed, open tickets 7
UNKNOWN 27
The zero on the drift line and the 27 on the unknown line have to be read together. Alone, the zero says the fleet is clean. Next to the 27, it says what it actually means: nothing has drifted from the 84% we can see, and there are 27 files per host that nothing has ever checked.
Knowledge check
Knowledge check · 5 questions
Q1. A configuration management dry-run reports "no changes" across the fleet. What has been established?
Q2. A file that no package owns is necessarily a drift finding that needs to be brought under management.
Q3. Which files are invisible to BOTH `dpkg --verify` and a CM dry-run? Select all that apply.
Q4. Why must `LC_ALL=C` be set before sorting the inputs to a `comm` comparison?
Q5. Which figure is the most useful thing to track over time in a drift programme?
Passing score: 75%. Answers are checked in this browser.