feat(handlers): audit_rule_set loads rules via AUDIT netlink (Phase 5, PR 1/2)#112
Merged
Conversation
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) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 5, PR 1 of 2 (rule handler; the engine transaction-phase event emission follows).
audit_rule_setnow loads each rule line into the running kernel viaAUDIT_ADD_RULE(go-libaudit) and writes the/etc/audit/rules.ddrop-in atomically (fsatomic), instead of shelling out toaugenrules, in agent mode.New dependency
github.com/elastic/go-libaudit/v2 v2.6.2(pure-Go netlink + the auditctl-grammar rule parser;go-shellquotepulled in// indirectfor its parser). No depguard in.golangci.yml→ no allowlist change;go mod tidyverified idempotent (CI drift check passes).What
internal/agent/auditnl:BuildRule(flags.Parse+rule.Build→ wire format — same grammar as auditctl, not reimplemented) +RuleLines;AuditClientinterface +Open(ErrAuditUnavailableon open failure);AuditTransportcapability (FileTransport+AuditClient());FakeAuditTransport.local.TransportimplementsAuditTransport.audit_rule_setApply/Capture/Rollback gain a netlink branch viatransport.(auditnl.AuditTransport);ErrAuditUnavailable(no privilege / immutable audit) or a missing capability → augenrules shell fallback (host without netlink behaves exactly as before).The netlink path loads this rule's lines into the kernel's flat list — it does not replicate
augenrules' whole-rules.dcompile-load. To keep rollback safe, Capture recordsadded_rules= the lines NOT already loaded (wire-equality vsAUDIT_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-lint0;comment-lintclean;specter syncall pass;go mod tidyidempotent.Failure-mode analysis
added_rules(wire-equality vs the kernel's current list) → rollback deletes only what this apply added;ENOENTon delete is a no-op.file_existed+prior_content(persist) andadded_rules(runtime). Capture failure →ErrCaptureIncomplete.enabled=2) rejects ADD/DEL until reboot → failed step (or shell fallback, same limitation); malformed rule → failed step (nothing loaded/persisted).kensa-fuzz+ two-human rollback review (CONTRIBUTING) are the founder's gate.🤖 Generated with Claude Code