# ADR-0087 — AIDE file-integrity monitoring (M42)

**Status:** Accepted
**Date:** 2026-04-30
**Related:** ADR-0072 (malware stack — LMD covers user docroots),
ADR-0086 (AppArmor confinement), ADR-0085 (auditd exec audit).

## Context

After M33 (LMD inotify on user docroots) + M39 (auditd narrow exec
audit) + M40 (AppArmor daemon confinement), we still had no
detection layer for system-file tamper. A successful root-level
exploit can:
- Modify `/etc/passwd`, `/etc/shadow`, `/etc/sudoers`.
- Replace `/usr/local/bin/jabali-*` binaries with trojaned versions.
- Edit cron files, sshd config, systemd unit files outside
  `/etc/jabali/`.
- Drop a backdoor SSH key into `/root/.ssh/authorized_keys`.

LMD doesn't watch system paths; AppArmor enforces but doesn't
detect; auditd records execve but not file-content drift.

## Decision

**AIDE** (Advanced Intrusion Detection Environment, mainline since
1999), single-binary FIM, daily check via systemd timer.

### Watch list

```
/bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin
/lib /lib64 /usr/lib
/etc
/boot
/root
```

Hash rule: `p+i+n+u+g+s+m+c+sha256` (perms, inode, links, uid, gid,
size, mtime, ctime, sha256). atime explicitly NOT included — it
churns on every read.

### Exclude list

Paths jabali or its dependencies write to are excluded — including
them = daily noise. Categories:

- `/etc/jabali/` (panel writes config + secrets here).
- `/etc/letsencrypt/{live,archive,csr,keys,renewal,accounts}/`
  (cert renewal rotates files).
- Reconciler-managed config trees:
  `/etc/nftables.d/jabali-*`, `/etc/audit/rules.d/jabali-*`,
  `/etc/nginx/sites-{available,enabled}/jabali-*`,
  `/etc/php/*/fpm/pool.d/jabali-*`, `/etc/systemd/system/jabali-*`,
  `/etc/systemd/system/user-*.slice.d/`,
  `/etc/cron.d/jabali-*`,
  `/etc/apparmor.d/usr.local.bin.jabali-*`.
- AIDE rotation artefacts: `/etc/group-?`, `/etc/passwd-?`,
  `/etc/shadow-?`, `/etc/gshadow-?`.
- Volatile: `/etc/mtab`, `/etc/resolv.conf`, `/etc/adjtime`,
  `/etc/machine-id`.
- SSH host keys (regenerated by package post-install): `/etc/ssh/ssh_host_*_key*`.
- Whole-tree exclusions: `/var`, `/run`, `/proc`, `/sys`, `/tmp`,
  `/home`, `/dev`, `/mnt`, `/media`, `/lost+found`.

### Daily timer

`jabali-aide-check.timer` fires at `04:30 UTC` with 15-min jitter
(spreads fleet load). Service runs `aide --check`, appends to
`/var/log/aide/aide.report.log`. Hardened: `ProtectSystem=strict`,
`ProtectHome=read-only`, `ReadWritePaths=/var/lib/aide /var/log/aide`,
`NoNewPrivileges=yes`.

### M14 event source

`runAideTamper` polls `security.aide.status` every 5 minutes (no
point polling more often — AIDE only updates daily). Fires
`aide.tamper.detected` on first observed diff per check run, with
24h cooldown keyed on the report timestamp.

Severity escalates to `critical` when any of these paths appear in
the diff:
- `/etc/passwd`
- `/etc/shadow`
- `/etc/sudoers`
- `/usr/local/bin/jabali-*`
- `/root/.ssh/authorized_keys`

### `jabali update` re-baseline

Panel binary updates (`/usr/local/bin/jabali-*`) change checksums.
The `jabali aide rebuild --full` CLI re-initialises the DB; operator
runs after a deliberate kernel/binary change. v1 ships full
re-baseline only — partial path-scoped re-baseline is a phase-2
nice-to-have.

## Alternatives considered

### Tripwire

Commercial product; AIDE is the open successor. AIDE's coverage is
identical for our threat model.

### OSSEC / Wazuh

Heavier daemon model + agent-server deployment. Overkill for a
single-tenant-per-host panel; CrowdSec (M27) already covers the
daemon-watching axis we'd want from OSSEC.

### Samhain

Daemon-mode FIM. Heavier than AIDE for daily-check use case; we
don't need real-time FS event hooks (LMD inotify covers user
docroots; system files don't change often enough to warrant
daemon overhead).

### inotify-only

Doesn't catch offline tamper (single-user-mode boot, mounting the
disk read/write from a rescue ISO). AIDE's hash baseline survives
those scenarios.

## Consequences

### Positive

- **Closes the system-file gap.** A trojaned `/usr/sbin/sshd` or
  `/etc/sudoers` edit fires `aide.tamper.detected` next morning.
- **Mature tooling.** AIDE has been in mainline Linux for ~25
  years; well-understood by sysadmins.
- **Light overhead.** One CPU-bound daily run (~2-5 min on a
  typical host); ~50 MB DB on disk.
- **Daily cadence is correct for FIM.** System-file tamper is rare;
  alerts must be actionable. More-frequent checks burn cycles
  without surfacing more incidents.

### Negative

- **DB-on-same-host limitation.** A root-level attacker can
  `aide --init` after their tamper and rotate the DB. Mitigations:
  0600 perms on aide.db, off-host DB shipping deferred to phase 2,
  M30 backup includes /var/lib/aide so a snapshot exists.
- **Re-baseline on jabali update.** Operator must remember to run
  `jabali aide rebuild --full` after manual config edits — the
  daily check will fire `aide.tamper.detected` until they do.
  Mitigation: severity defaults to `warning` (not `critical`) for
  non-critical paths.
- **AIDE pagefaults under low memory.** Initial `aide --init`
  walks the entire watched tree; on a 1 GB VPS it can OOM. install.sh
  runs init in the background with `nohup` so the install pipeline
  doesn't block; operator gets `aide.tamper.detected` empty
  responses until init completes.

### Risks accepted

- **Off-host DB shipping deferred.** Standard tripwire/AIDE
  practice (sign DB, ship to S3, verify before each check) is
  operationally complex; phase-2.
- **Partial re-baseline deferred.** v1 only supports full
  re-init. After `jabali update`, panel binaries change and the
  next AIDE check fires `aide.tamper.detected` with our binaries
  in the changed list. Operator runs `jabali aide rebuild --full`
  manually. Phase-2 will add `--paths /usr/local/bin/jabali-*`
  for surgical re-baseline.

## Implementation

- `install.sh install_aide()` — apt-installs `aide` + `aide-common`,
  renders `/etc/aide/aide.conf`, async background `aide --init` on
  first install, copies + enables `jabali-aide-check.timer`.
- `panel-agent internal/commands/security_aide.go`:
  - `security.aide.status` — DB age + last report parse + sample.
  - `security.aide.check` — synchronous `aide --check` (10-min
    timeout for the agent context).
- `panel-api /api/v1/admin/security/aide/{status,check}`.
- `panel-ui` — new AIDE sub-tab on `/jabali-admin/security` with
  Statistic row (DB age, last check, summary counts), success/warn
  Alert, sample table, Refresh + Run-check-now buttons.
- `eventsources/aide_tamper.go` — `runAideTamper` (5-min tick,
  24h cooldown, critical-path severity escalation).
- `jabali aide {status,rebuild --full}` cobra subcommand.

### Live verification (target after merge)

On 192.168.100.150:
1. `dpkg -l aide | grep installed` returns the package.
2. `systemctl is-active jabali-aide-check.timer` = active.
3. After initial init: `/var/lib/aide/aide.db` exists, root:root 0600.
4. Manual `aide --check` returns "AIDE found NO differences".
5. Synthetic tamper (`echo x > /usr/local/sbin/test-tamper`) →
   wait one timer tick → AIDE card shows 1 added, M14
   `aide.tamper.detected` fires with severity=warning.
6. `jabali update` (with binary changes) → `jabali aide rebuild
   --full` clears the diff.
7. `apt-get install <package>` (deliberate change) → daily check
   fires `aide.tamper.detected` listing the added/changed files in
   `/usr/bin/`, `/etc/`, etc.
