Skip to content

feat(handlers): audit_rule_set loads rules via AUDIT netlink (Phase 5, PR 1/2)#112

Merged
remyluslosius merged 1 commit into
mainfrom
feat/auditnl-rule-set
Jun 20, 2026
Merged

feat(handlers): audit_rule_set loads rules via AUDIT netlink (Phase 5, PR 1/2)#112
remyluslosius merged 1 commit into
mainfrom
feat/auditnl-rule-set

Conversation

@remyluslosius

Copy link
Copy Markdown
Contributor

Phase 5, PR 1 of 2 (rule handler; the engine transaction-phase event emission follows). audit_rule_set now loads each rule line into the running kernel via AUDIT_ADD_RULE (go-libaudit) and writes the /etc/audit/rules.d drop-in atomically (fsatomic), instead of shelling out to augenrules, in agent mode.

New dependency

github.com/elastic/go-libaudit/v2 v2.6.2 (pure-Go netlink + the auditctl-grammar rule parser; go-shellquote pulled in // indirect for its parser). No depguard in .golangci.yml → no allowlist change; go mod tidy verified idempotent (CI drift check passes).

What

  • internal/agent/auditnl: BuildRule (flags.Parse + rule.Build → wire format — same grammar as auditctl, not reimplemented) + RuleLines; AuditClient interface + Open (ErrAuditUnavailable on open failure); AuditTransport capability (FileTransport + AuditClient()); FakeAuditTransport.
  • local.Transport implements AuditTransport.
  • audit_rule_set Apply/Capture/Rollback gain a netlink branch via transport.(auditnl.AuditTransport); ErrAuditUnavailable (no privilege / immutable audit) or a missing capability → augenrules shell fallback (host without netlink behaves exactly as before).

⚠️ Additive-per-rule model (documented divergence)

The netlink path loads this rule's lines into the kernel's flat list — it does not replicate augenrules' whole-rules.d compile-load. To keep rollback safe, Capture records added_rules = the lines NOT already loaded (wire-equality vs AUDIT_LIST_RULES), and Rollback unloads only those — so a rule another drop-in owns survives rollback (unit-tested).

spec auditnl-rule-set (Tier 1, 5 ACs) + parser tests + handler round-trip tests (load+persist, bad-rule→failed-step, unload-what-we-added, keep-preexisting, both fallback paths).

Verification

go test ./... green; go build ./... clean; golangci-lint 0; comment-lint clean; specter sync all pass; go mod tidy idempotent.

Failure-mode analysis

  1. Wrong in prod? Unloading an audit rule another drop-in/operator owns. Mitigated: added_rules (wire-equality vs the kernel's current list) → rollback deletes only what this apply added; ENOENT on delete is a no-op.
  2. Captured-state sufficiency: rollback consumes file_existed + prior_content (persist) and added_rules (runtime). Capture failure → ErrCaptureIncomplete.
  3. Edge case / gated: immutable audit (enabled=2) rejects ADD/DEL until reboot → failed step (or shell fallback, same limitation); malformed rule → failed step (nothing loaded/persisted). ⚠️ LIVE netlink validation needs root + a real audit subsystem (CI is non-root → only the shell-fallback path runs); kensa-fuzz + two-human rollback review (CONTRIBUTING) are the founder's gate.

🤖 Generated with Claude Code

Phase 5, PR 1 of 2 (rule handler; the engine transaction-phase event
emission follows). audit_rule_set now loads each rule line into the
running kernel via AUDIT_ADD_RULE (github.com/elastic/go-libaudit) and
writes the /etc/audit/rules.d drop-in atomically (fsatomic), instead of
shelling out to augenrules, when running in agent mode.

New dependency: github.com/elastic/go-libaudit/v2 v2.6.2 (pure-Go netlink
+ rule parser; go-shellquote pulled in as indirect for its parser). No
depguard in .golangci.yml, so no allowlist change; go mod tidy verified
idempotent (CI drift check passes).

New internal/agent/auditnl package:
- BuildRule (rule/flags.Parse + rule.Build → kernel wire format; the same
  grammar auditctl implements, so we don't reimplement it) + RuleLines.
- AuditClient interface (AddRule/DeleteRule/GetRules/Close) + Open
  (ErrAuditUnavailable when the netlink socket can't be opened).
- AuditTransport capability (kernelio.FileTransport + AuditClient()).
- FakeAuditTransport (embeds kernelio.FakeSysctlTransport) for tests.

local.Transport implements AuditTransport (AuditClient → auditnl.Open).
audit_rule_set Apply/Capture/Rollback gain a netlink branch selected by
transport.(auditnl.AuditTransport); ErrAuditUnavailable (no privilege /
immutable audit) OR a missing capability falls back to the augenrules
shell path — so a host without netlink behaves exactly as before.

The netlink model is ADDITIVE per-rule (it loads this rule's lines into
the kernel's flat list; it does NOT replicate augenrules' whole-rules.d
compile-load). Capture records added_rules = the lines NOT already loaded
(by wire-format equality vs AUDIT_LIST_RULES); Rollback unloads ONLY
those, so a rule another drop-in owns survives rollback. spec
auditnl-rule-set (Tier 1, 5 ACs); auditnl parser tests + handler
round-trip tests (load+persist, bad-rule→failed-step, unload-what-we-added,
keep-preexisting, both fallback paths).

Failure-mode analysis:
1. What could this do wrong in production? Unloading an audit rule another
   drop-in/operator owns (a compliance regression). Mitigated: Capture
   computes added_rules by wire-equality against the kernel's CURRENT rule
   list, and Rollback deletes ONLY added_rules — a rule already present at
   capture is never removed (unit-tested). AUDIT_DEL of an absent rule
   (ENOENT) is a no-op.
2. Captured-state sufficiency: rollback consumes file_existed +
   prior_content (persist) AND added_rules (runtime) — the full prior
   state on both layers. A capture list/read failure surfaces
   ErrCaptureIncomplete, never an empty (wrong) prior.
3. Edge case not safe for / gated: immutable audit config (enabled=2)
   rejects AUDIT_ADD/DEL until reboot — surfaced as a failed step (or, on
   socket-open failure, the shell fallback, which has the same limitation).
   A malformed rule line is a failed step (nothing loaded/persisted), never
   a silent success. The additive-vs-augenrules semantic difference is
   documented in the spec. LIVE netlink validation needs root + a real
   audit subsystem (CI runs as non-root → shell fallback path only);
   kensa-fuzz atomicity + the two-human rollback-handler review
   (CONTRIBUTING) remain the founder's gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@remyluslosius remyluslosius merged commit a54101f into main Jun 20, 2026
18 checks passed
@remyluslosius remyluslosius deleted the feat/auditnl-rule-set branch June 20, 2026 03:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant