AIDE on Debian 13: File Integrity Monitoring

12 min read

A file integrity checker tells you when files changed that should not have. Examples are a new SUID binary in /usr/local/bin, a cron job nobody wrote or a modified sshd. On Debian 13, the tool for this is AIDE (Advanced Intrusion Detection Environment). Getting it installed is easy. Keeping it useful is harder: on a default setup it either buries you in noise after every apt upgrade, or it trusts a baseline database that any attacker with root can rewrite.

This article covers AIDE file integrity monitoring on Debian 13 using the packaged defaults. It explains how the aide.conf.d layout and the dailyaidecheck timer work, how to reduce noise, how to re-baseline after upgrades without hiding tampering, how to keep the baseline out of an attacker's reach, and how to triage a real alert with dpkg -V and debsums.

What AIDE is for, and when to use it

AIDE builds a database of file attributes, including permissions, owner, inode, size, mtime/ctime, extended attributes and several hashes, for every path matched by its rules. Later runs compare the live filesystem against that database. AIDE finds changes after they happen. It does not block anything, and it does not record who made a change.

It's a good fit for:

  • Internet-facing servers where you want an independent answer to the question "did anything change in /etc, /usr or /boot that I didn't do?"
  • Compliance baselines that require file integrity monitoring.
  • Post-incident scoping, if you have a baseline you can trust.

It's a poor fit for hosts where large parts of the filesystem change all the time, such as build machines or container hosts with a busy /var/lib/docker, unless you exclude those parts deliberately.

Installing AIDE on Debian 13

Trixie ships aide and aide-common version 0.19.1-2+deb13u2. Upstream is at 0.19.4, released 2026-08-30. That version has reached testing but not stable. Make sure you are on at least 0.19.1-2+deb13u1: DSA-5977-1 (2025-08-14) fixed CVE-2025-54389 and CVE-2025-54409. Those flaws let a local attacker hide an added or removed file from the report, tamper with log output, or crash AIDE while it prints a report. For a tool whose whole job is reporting, that matters.

apt update
apt install aide aide-common
dpkg-query -W aide aide-common
systemctl list-timers dailyaidecheck.timer

The Debian package does more than install the binary:

  • /etc/aide/aide.conf sets database_in=file:/var/lib/aide/aide.db and database_out=file:/var/lib/aide/aide.db.new, with gzip_dbout=yes.
  • Rules live as snippets in /etc/aide/aide.conf.d, pulled in via @@x_include /etc/aide/aide.conf.d ^[a-zA-Z0-9_-]+$. If a snippet is executable, AIDE runs it and uses its output as configuration. That's how snippets such as 31_aide_apt generate rules for the apt lists, apt state files and logs.
  • A dailyaidecheck.timer runs at 01:50 with RandomizedDelaySec=2h and Persistent=true.
  • The matching service runs as the unprivileged _aide user with AmbientCapabilities=CAP_DAC_READ_SEARCH CAP_AUDIT_WRITE. It deliberately uses no systemd sandboxing, because sandboxing would hide parts of /run. If you've read how systemd service sandboxing works, that's why systemd-analyze security scores this unit badly.

Build the first baseline with aideinit. The -y flag overwrites aide.db.new without asking, and -f replaces the existing aide.db. On a fresh install there's nothing to lose, but on a host that already has a database, -f throws away your current reference.

aideinit -y -f
ls -l /var/lib/aide/
sha256sum /var/lib/aide/aide.db

Write the hash down somewhere outside this machine now. You'll need it later.

Real usage: checks, the timer and a planted change

Run a manual check against the reference database:

aide -c /etc/aide/aide.conf --check
echo "aide exit code: $?"

The exit code is a bit field: 1 means new files, 2 removed files, 4 changed files. The values add up, so 5 means files were both added and changed. Codes 14 and above are errors: 17 is a configuration error, 18 an I/O error, 24 a database error. That makes AIDE easy to wire into monitoring.

To run the scheduled job immediately and read its output:

systemctl start dailyaidecheck.service
journalctl -u dailyaidecheck.service -n 50 --no-pager

Mail delivery has one Debian-specific catch. The README notes that the unprivileged service can't use a setuid sendmail. The script prefers s-nail, which sends the report by SMTP to localhost, so it needs a working MTA listening there. Without s-nail and a local MTA, you will probably get no AIDE mail. If you never get AIDE mail, check this first.

Prove it works: plant a change

Don't trust a monitor you haven't tested. The test below drops a harmless, comment-only file into /etc/cron.d, a common persistence spot, and confirms AIDE catches it. First, confirm the path is covered by a rule with --path-check:

aide -c /etc/aide/aide.conf -p f:/etc/cron.d/aide-canary
printf '# aide canary, safe to delete\n' > /etc/cron.d/aide-canary
aide -c /etc/aide/aide.conf --check -l '^/etc/cron.d'
echo "aide exit code: $?"
rm /etc/cron.d/aide-canary

You should see something like the following (trimmed, and the layout varies slightly between versions). The exit code should be 1:

AIDE found differences between database and filesystem!!

Summary:
  Added entries:    1
  Removed entries:  0
  Changed entries:  0

Added entries:
f++++++++++++++++: /etc/cron.d/aide-canary

If the exit code is 0, your rules don't cover that path. Fix that before you trust anything else.

AIDE configuration on Debian 13: rules that cut the noise

Debian's snippets already handle most of the churn. 70_aide_var treats /var/log and /var/backups as VarDir, which checks ownership, mode and link count but not content. 31_aide_apt classifies /var/lib/apt/lists, the apt state files and the current and rotated logs in /var/log/apt. It only writes rules for /var/cache/apt/archives if IGNORE_ARCHIVES=yes or IGNORE_FRQCHG=yes is set in 31_aide_apt_settings. Both are empty by default. Several 10_aide_* snippets and 70_aide_run handle /run. The groups used in aide.conf are worth knowing:

GroupDefinitionUse for
FullInodeData+StaticFile (perms, owner, inode, size, mtime, ctime, hashes, xattrs/ACL/caps)Binaries, libraries, config
VarFileOwnerMode+n+l+XFiles whose content changes legitimately
VarDirOwnerMode+n+i+XDirectories with changing contents
LogOwnerMode+n+growing+s+XAppend-only logs; shrinking is flagged

Put your own rules in a new snippet, for example /etc/aide/aide.conf.d/99_aide_local. The filename must match ^[a-zA-Z0-9_-]+$, so a name like 99_local.conf is silently ignored because of the dot. Rule syntax: !regex is a recursive exclude, -regex excludes the match and everything under it without descending, =regex matches only the path itself, and a type letter such as f or d limits a rule to regular files or directories. Regexes are anchored at the start.

# container runtime state churns constantly; watch it with other tools
-/var/lib/docker$
-/var/lib/containerd$
# app cache: content changes, ownership and mode must not
/srv/app/cache$ d VarDir
# deployed code and local tools: full check
/srv/app/releases f Full
/usr/local/sbin f Full

Validate before the next scheduled run:

aide -c /etc/aide/aide.conf --config-check
aide -c /etc/aide/aide.conf -p d:/var/lib/docker

The reporting side is configured in /etc/default/aide. The defaults are COMMAND=update, COPYNEWDB=no, QUIETREPORTS=no, FILTERUPDATES=no, FILTERINSTALLATIONS=no and LINES=1000. Two changes are worth making:

QUIETREPORTS=yes
TRUNCATEDETAILS=yes

QUIETREPORTS stops the "nothing changed" mails, so a mail from AIDE actually means something. TRUNCATEDETAILS shortens the mail but keeps the full detail in the log. Be careful with FILTERUPDATES and FILTERINSTALLATIONS: they drop package-driven changes from the report. An attacker who gets root can install packages too. I leave both off and deal with upgrades by re-baselining instead. COPYNEWDB=yes is worse: every change is reported once, and then it becomes the new baseline automatically.

Re-baselining safely after upgrades

With COMMAND=update, each daily run writes aide.db.new but doesn't promote it. The correct workflow is to upgrade, read the report, check it against what apt did, and then promote the new database yourself. An apt hook can remind you. DPkg::Post-Invoke commands run through /bin/sh, and APT aborts if one fails, so make sure the command can't fail:

DPkg::Post-Invoke { "date -Is > /var/local/aide-rebaseline-pending || true"; };

The promotion script keeps the previous database and copies its owner and mode, so the _aide service can still read the new file:

#!/bin/bash
set -euo pipefail
conf=/etc/aide/aide.conf
db=/var/lib/aide/aide.db
new=/var/lib/aide/aide.db.new
prev="$db.prev-$(date +%F-%H%M)"
rc=0
aide -c "$conf" --update || rc=$?
if [ "$rc" -ge 14 ]; then
    echo "aide failed with exit code $rc" >&2
    exit "$rc"
fi
read -r -p "Reviewed the report above and want to promote it? [y/N] " answer
if [ "$answer" != "y" ]; then
    echo "aborted, $db unchanged"
    exit 1
fi
cp -a "$db" "$prev"
mv "$new" "$db"
chown --reference="$prev" "$db"
chmod --reference="$prev" "$db"
rm -f /var/local/aide-rebaseline-pending
sha256sum "$db"

Ship the printed hash and the database off the host every time you run it.

Protecting the baseline from a root attacker

An attacker with root can run aide --update, swap /var/lib/aide/aide.db, edit aide.conf.d to exclude their directory, or replace the aide binary itself. chattr +i slows them down by a few seconds at most. Some countermeasures that actually help, from weakest to strongest:

  1. Keep hashes off-host. After each approved re-baseline, record sha256sum /var/lib/aide/aide.db in your ticket system or config repo. A different machine can pull the file periodically and compare. A mismatch without a matching change ticket is an incident.
  2. Keep database copies off-host in an append-only store. Have the backup host pull the file so the monitored server has no write access to it. The append-only restic setup works well for this, and it also keeps /etc/aide under version history.
  3. Check from trusted media. A kernel-level rootkit can lie to anything running on the host, including your hash checks. For a real investigation, boot a rescue system, mount the disk read-only, and run a known-good aide binary with the off-host database and config.

Also watch for silence. If dailyaidecheck stops producing reports, or starts reporting zero entries, treat that as an alert.

Triage checklist for a real AIDE alert

  1. Read what changed. Was it a hash or size change (content), or p/u/g (permissions or ownership)? Is the file new or removed?
  2. Correlate with apt. grep -E ' (install|upgrade) ' /var/log/dpkg.log | tail -n 20, plus zgrep on rotated logs. If /var/local/aide-rebaseline-pending exists, look at its timestamp.
  3. Find the owning package. Use dpkg -S /path. A new file under /usr or /etc/cron.d that no package owns is suspicious by default.
  4. Verify the package. dpkg -V pkg prints a line only for files that fail. A 5 in the third column means the md5sum doesn't match, and c marks a conffile. debsums -s pkg does the same with quieter output, and debsums -a also checks conffiles.
  5. Don't trust local checksums alone. The md5sums under /var/lib/dpkg/info are writable by root, and the debsums man page itself calls the tool "of limited use as a security tool". Compare against a freshly downloaded package, as shown below.
  6. Decide. If the change is explained, promote the baseline with aide-rebaseline. If it isn't, isolate the host, image the disk, and continue from trusted media.
pkg=openssh-server
ver=$(dpkg-query -W -f='${Version}' "$pkg")
cd "$(mktemp -d)"
apt-get download "$pkg=$ver"
dpkg-deb -x ./*.deb extracted
cmp extracted/usr/sbin/sshd /usr/sbin/sshd && echo "sshd matches the archive"

The dpkg -V output format looks like this:

??5?????? c /etc/ssh/sshd_config
??5??????   /usr/sbin/sshd

A changed conffile you edited yourself is normal. A changed binary with no matching upgrade is not. If the alert involves web content, the checks in web server security basics are the next step.

Pitfalls, limitations and alternatives

  • Detection only, after the fact. AIDE runs once a day by default. An attacker has hours before the next run.
  • Snippet names with dots are silently skipped by the @@x_include regex.
  • Mail as _aide expects s-nail and a working MTA on localhost.
  • Auto-promotion (COPYNEWDB=yes, or a hook that re-baselines blindly) turns AIDE into a changelog instead of a detector.
  • Run time and I/O. A full hash of a large /srv can take a long time. Exclude bulk data that your backups already cover.
ToolWhat it doesTrade-off
dpkg -V / debsumsChecks package files against dpkg's md5sumsNo coverage for unowned files; the checksums sit on the same host
Tripwire (2.4.3.7 in trixie)Rule-based integrity checker; twadmin signs policy, config and key filesSigning helps against tampering, but setup and upkeep are heavier
auditdReal-time syscall and watch events, e.g. auditctl -w /etc/shadow -p wa -k shadowRecords who and when, not full-tree state; the man page warns that watches slow the system
AIDESnapshot comparison of the whole tree with hashes and attributesPeriodic; the baseline must be protected

They work well together: auditd shows who touched /etc/shadow, AIDE shows that something under /usr/lib changed, and dpkg -V tells you whether apt explains it.

AIDE cheat sheet

TaskCommand
Build or replace the baselineaideinit -y -f
Manual checkaide -c /etc/aide/aide.conf --check
Check one subtreeaide -c /etc/aide/aide.conf --check -l '^/etc'
Validate the configaide -c /etc/aide/aide.conf --config-check
Is a path covered?aide -c /etc/aide/aide.conf -p f:/usr/bin/ssh
Run the daily job nowsystemctl start dailyaidecheck.service
Timer statussystemctl list-timers dailyaidecheck.timer
Verify a packagedpkg -V pkg / debsums -s pkg
Who owns a filedpkg -S /path

Takeaways

  • Run aide 0.19.1-2+deb13u1 or newer (DSA-5977-1).
  • Put local rules in an aide.conf.d snippet with no dot in the name, and validate with --config-check and -p.
  • Set QUIETREPORTS=yes. Leave COPYNEWDB and the update filters off.
  • Promote aide.db.new only after reviewing the report. Ship the database and its hash off-host every time.
  • Test detection with a planted canary file, and run the test again after config changes.
  • Triage with dpkg -S, dpkg -V and a fresh .deb. Investigate from trusted media, not from the host that might be compromised.

Sources

Comments