Skip to content

feat(engine): emit transaction-phase records into auditd (Phase 5, PR 2/2)#113

Merged
remyluslosius merged 2 commits into
mainfrom
feat/engine-audit-emission
Jun 20, 2026
Merged

feat(engine): emit transaction-phase records into auditd (Phase 5, PR 2/2)#113
remyluslosius merged 2 commits into
mainfrom
feat/engine-audit-emission

Conversation

@remyluslosius

Copy link
Copy Markdown
Contributor

Phase 5, PR 2 of 2 — completes Phase 5. The transaction coordinator now emits an AUDIT_USER (type 1005) record into the local auditd at each transaction-phase boundary, so a SIEM ingesting auditd sees Kensa's transaction lifecycle without additional plumbing — the observability the FedRAMP reviewer flagged. The engine runs on the controller, so events land in the controller (operator) host's auditd.

Best-effort by construction — can never affect a transaction

  • auditnl.Emitter sends via SendNoWait (no ACK wait → non-blocking) and swallows all errors.
  • NewEmitter never errors: a failed socket open (no CAP_AUDIT_WRITE) → silent no-op emitter.
  • The engine's default emitter is a no-op; an engine with none wired runs identically (unit-tested).

What

  • internal/agent/auditnl: Emitter (one socket; EmitPhase via SendNoWait), NewEmitter (no-op on open failure), formatPhaseMessage.
  • internal/engine: PhaseEmitter interface defined here (so the core doesn't import the netlink/go-libaudit stack — auditnl.Emitter satisfies it structurally), no-op default, WithAuditEmitter option, and emit calls in Run/finalize/errored: started → capture → apply → validate → committed|rolled_back|partially_applied, and errored on the error path.
  • pkg/kensa: wires WithAuditEmitter(auditnl.NewEmitter()) so emission is on by default where privilege allows.
  • spec engine-audit-emission (Tier 1, 4 ACs) + engine emit-sequence tests (committed + rolled-back orders, no-op-default safety) + auditnl emit tests. No new dependency (go-libaudit landed with feat(handlers): audit_rule_set loads rules via AUDIT netlink (Phase 5, PR 1/2) #112).

Verification

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

Failure-mode analysis

  1. Wrong in prod? Touching the atomicity-critical coordinator. Mitigated by construction: the only change is added emitter.EmitPhase(...) calls that cannot block (SendNoWait) or error (return ignored; no-op default), sitting AFTER each phase's existing publishPhaseCompleted — never gating phase logic. A test asserts an engine with no emitter commits identically.
  2. Captured-state sufficiency: N/A — observability emission, not a capturable handler.
  3. Edge case / gated: needs CAP_AUDIT_WRITE (root); a non-privileged host emits nothing (no-op). Records land in the controller's auditd (where the engine runs), not the target's — documented. ⚠️ LIVE validation that records reach auditd needs root + a real audit subsystem (CI non-root → only the no-op path runs); the engine change carries the standard atomicity two-human review (CONTRIBUTING) as the founder's gate.

🤖 Generated with Claude Code

remyluslosius and others added 2 commits June 19, 2026 23:20
Phase 5, PR 2 of 2 — completes Phase 5. The transaction coordinator now
emits an AUDIT_USER (type 1005) record into the local auditd at each
transaction-phase boundary, so a SIEM ingesting auditd sees Kensa's
transaction lifecycle without additional plumbing — the observability the
FedRAMP reviewer flagged as non-negotiable. The engine runs on the
controller, so events land in the controller (operator) host's auditd.

- internal/agent/auditnl: Emitter (holds one netlink socket; EmitPhase
  sends an AUDIT_USER record via SendNoWait — no ACK wait, fully
  non-blocking; swallows all errors). NewEmitter NEVER errors: a failed
  socket open (no CAP_AUDIT_WRITE) yields a silent no-op emitter.
  formatPhaseMessage renders the auditd-style key=value body.
- internal/engine: PhaseEmitter interface (defined HERE so the engine core
  does not import the netlink/go-libaudit stack — auditnl.Emitter
  satisfies it structurally), no-op default, WithAuditEmitter option, and
  emit calls at each boundary in Run / finalize / errored: started →
  capture → apply → validate → committed|rolled_back|partially_applied,
  and "errored" on the error path.
- pkg/kensa: wires engine.WithAuditEmitter(auditnl.NewEmitter()) into the
  production engine, so emission is on by default wherever privilege
  allows.
- spec engine-audit-emission (Tier 1, 4 ACs); engine emit-sequence tests
  (committed + rolled-back orders, no-op-default safety) + auditnl emit
  tests (message format, no-op/zero-value safety). No new dependency
  (go-libaudit landed with PR 5a).

STRICTLY best-effort and non-blocking by construction: SendNoWait,
all-errors-swallowed, no-op default. An audit-log failure can NEVER fail
or delay a transaction.

Failure-mode analysis:
1. What could this do wrong in production? Touching the atomicity-critical
   coordinator risks affecting transaction outcomes. Mitigated by
   construction: the only engine change is added emitter.EmitPhase(...)
   calls whose implementation cannot block (SendNoWait) or error (return
   value ignored; no-op default). A unit test asserts an engine with NO
   emitter wired commits identically; the emit calls sit AFTER each
   phase's existing publishPhaseCompleted, never gating phase logic.
2. Captured-state sufficiency: N/A — this is observability emission, not a
   capturable handler; no capture/rollback path is touched.
3. Edge case not safe for / gated: emission needs CAP_AUDIT_WRITE (root);
   a non-privileged operator host silently emits nothing (no-op emitter) —
   acceptable, audit logging is inherently privileged. The emitted records
   land in the controller's auditd (where the engine runs), not the
   target's — documented in the spec. LIVE validation that records reach
   auditd needs root + a real audit subsystem (CI is non-root → only the
   no-op path runs); the engine change carries the standard atomicity
   two-human review (CONTRIBUTING) as the founder's gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
comment-lint forbids planning labels in new Go comments; restate the
emitter/option comments without the phase number.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@remyluslosius remyluslosius merged commit a9cc0d7 into main Jun 20, 2026
18 checks passed
@remyluslosius remyluslosius deleted the feat/engine-audit-emission branch June 20, 2026 03:27
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