Add signed-package verification and anti-rootkit cross-view (v0.7)

Closes the tamper-evidence loop with two trust anchors the attacker can't
forge from userland:

- pkgdb Layer 2: verify on-disk files against the .MTREE in the *signed*
  cache package, surviving a rewritten local checksum DB. Rotating-sample
  cadence keeps it affordable; flags pkg_signature_mismatch (sid 100027)
  and SigLevel downgrades (sid 100026). Fixes parse_mtree, which required
  type=file on every line and so matched nothing on real pacman MTREEs
  (which use a /set type=file default with bare file entries).
- rootcheck: anti-rootkit cross-view — hidden processes, modules, ports,
  and promiscuous interfaces, each caught by diffing two views of the
  same state (sids 100022-100025).

Wired both through config, the daemon (off-loop slow cadence), and CLI
(pkgdb-verify, rootcheck). 14 new tests (95 total). Docs + version bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-01 05:42:57 -07:00
parent 34bc09041a
commit 1de5e86fed
10 changed files with 766 additions and 5 deletions

View file

@ -75,6 +75,9 @@ enodia_sentinel/
├── web.py read-only dashboard: stdlib http server + JSON API + auth
├── static/ the self-contained dashboard SPA
├── fim.py file integrity monitoring (hash baseline + pacman verify)
├── pkgdb.py package-DB integrity + signed-package verification
├── rootcheck.py anti-rootkit cross-view (hidden procs/modules/ports)
├── selfprotect.py self-integrity footprint + dead-man's-switch heartbeat
├── triage.py false-positive classification
├── provenance.py package-ownership lookups (pacman/dpkg/rpm)
├── detectors/ poll detectors — one module per signature, each a pure
@ -196,6 +199,10 @@ enodia-sentinel.service`. Every key is optional. Highlights:
| `suid_scan_extra_dirs` | /tmp … | writable mounts always scanned (tmpfs-safe) |
| `capture_execve_bpftrace` | false | add a bpftrace execve trace to snapshots |
| `notify_users` | [] | desktop notify-send targets |
| `pkgdb_pkgverify` | false | verify on-disk files against signed cache packages |
| `pkgdb_pkgverify_sample` | 40 | packages verified per pass (rotates over time) |
| `rootcheck_enabled` | true | run the anti-rootkit cross-view sweep |
| `rootcheck_interval` | 300 | seconds between cross-view sweeps |
## Web dashboard
@ -273,9 +280,10 @@ enodia-sentinel watchdog --url http://100.x.x.x:8787 --token <T> --max-age 120
1. **Immutability**`chattr +i` Sentinel's binaries, config, baselines, and
the pacman hook so even root must visibly clear the flag first.
2. **Cryptographic anchor (next)** — verify files against the *signed* package
in the cache rather than the mutable DB; a maintainer's PGP signature is a
root of trust the attacker doesn't hold.
2. ✅ **Cryptographic anchor (done)** — verify on-disk files against the *signed*
package in the cache rather than the mutable DB; a maintainer's PGP signature
is a root of trust the attacker doesn't hold. See *Signed-package verification*
below.
3. **External anchor (next)** — mirror the DB/FIM fingerprints off-box so a
local attacker can't refresh the anchor to cover their tracks.
@ -285,6 +293,61 @@ enodia-sentinel watchdog --url http://100.x.x.x:8787 --token <T> --max-age 120
> attacker: signatures, external monitors, and ultimately the kernel (the eBPF
> roadmap) rather than userland the attacker can rewrite.
### Signed-package verification (the independent anchor)
Layer 1 (above) catches an *out-of-band* DB edit. But a root attacker can do the
full job: modify `/usr/bin/sshd`, rewrite its hash in the local DB, **and** run
`fim-update` to re-anchor — defeating both `pacman -Qkk` and the DB-fingerprint
check, because every reference they're checked against is one they can rewrite.
The one reference they *can't* forge is the distro's signing key. Packages in
the cache are signed, and each carries a `.MTREE` manifest of per-file SHA-256
hashes. Layer 2 extracts that manifest from the **cached package** and compares
the on-disk files to it — a reference independent of the local DB:
```bash
enodia-sentinel pkgdb-verify # verify a rotating sample of packages
enodia-sentinel pkgdb-verify --sample 200 # verify more per run
```
A divergence is `pkg_signature_mismatch` (CRITICAL, `sid 100027`) — a trojaned
binary that a rewritten checksum DB would have hidden. It also flags a
`SigLevel` downgrade in `pacman.conf` (`pacman_siglevel_disabled`, CRITICAL,
`sid 100026`), since disabling signature checking is how an attacker would slip
an unsigned package past the anchor in the first place.
Verifying every package each pass is expensive (untar + hash every file), so it
runs on its own slow cadence (`pkgdb_pkgverify_interval`) and checks a **rotating
sample** (`pkgdb_pkgverify_sample`) per pass — over enough passes the whole
installed set is covered, and any single mismatch fires immediately. It's
off by default (needs the package cache populated); enable with `pkgdb_pkgverify
= true`.
## Anti-rootkit (cross-view detection)
A rootkit hides by lying to one view of the system — but the technique that
hides it is also how you catch it: **ask the same question two different ways and
compare the answers.** A discrepancy is the hiding artifact.
| Cross-check | Hidden thing it surfaces | `sid` |
|---|---|---|
| `kill(pid, 0)` for every PID vs the `/proc` listing | a process the kernel schedules but `/proc` omits | 100022 |
| `/sys/module` (initstate=live) vs `/proc/modules` | a loaded LKM hidden from the module list | 100023 |
| `/proc/net/tcp` vs `ss` | a listening port a hooked `ss` won't report | 100024 |
| `/sys/class/net/*/flags` | an interface in promiscuous mode (a sniffer) | 100025 |
```bash
enodia-sentinel rootcheck # one-shot cross-view scan
```
The daemon runs it on a slow background cadence (`rootcheck_interval`) and routes
any finding into the normal alert/snapshot/push pipeline. **Honest limits:** this
runs in user space, so it reliably catches userland (`LD_PRELOAD`) rootkits and
the common `/proc`-hiding LKMs, but a kernel rootkit that hooks *every* path
consistently can still evade it. It raises the bar and catches the common cases;
it is not a guarantee against a bespoke ring-0 implant — pair it with the off-box
dead-man's switch. (Lineage: OSSEC's rootcheck / `chkrootkit`'s cross-view idea.)
## File integrity monitoring (Tripwire-style)
Detects tampering with binaries and critical configs by **content hash**, so it
@ -395,6 +458,13 @@ regression suite for both.
## Project status
v0.7 — closes the tamper-evidence loop with the **independent anchor**:
signed-package verification (compares on-disk files to the `.MTREE` in the signed
cache package, surviving a rewritten checksum DB) plus a `SigLevel`-downgrade
check, and an **anti-rootkit cross-view** layer (hidden processes, modules,
ports, and promiscuous interfaces — each caught by asking the same question two
ways and diffing the answers).
v0.6 — adds **tamper-evidence**: out-of-band package-DB integrity
(catches rewritten checksums), self-integrity of Sentinel's own footprint, and a
dead-man's-switch heartbeat with an external watchdog.