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:
parent
34bc09041a
commit
1de5e86fed
10 changed files with 766 additions and 5 deletions
76
README.md
76
README.md
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue