Documentation
¶
Overview ¶
Package audit is the unified cloud binary's compliance-grade audit trail — tamper-evident, append-only, and complete over the security-relevant request surface (FedRAMP AU-* / SOC 2 CC-* controls).
THE CONTROL, IN ONE SENTENCE. Every security-relevant action against this binary is captured as a structured Record, hash-chained to its predecessor so any later deletion or modification is detectable, and written INLINE (never dropped) to an append-only store the application can only INSERT into.
THREE PIECES, EACH IN ITS LANE (orthogonal, per the Zen of Hanzo):
- record.go — the event model + the hash-chain math (what a record IS and how it links to the one before it). Pure, no I/O.
- store.go — the append-only sink (SQLite primary, INSERT-only; a best-effort OLAP mirror) and the serialized Recorder that owns the chain head. All persistence.
- redact.go — the secret-stripping allowlist/denylist for any structured before/after an explicit emit point supplies. No secret ever reaches a record.
The HTTP middleware (Middleware, in the cloud package) and the query/verify endpoints (in clients/admin) are thin callers of this package. This package holds the security logic; it has zero knowledge of routes.
Index ¶
- func Redact(raw json.RawMessage) json.RawMessage
- type Actor
- type AuthContext
- type Checkpoint
- type CheckpointFunc
- type CheckpointSink
- type Filter
- type Integrity
- type Mirror
- type Outcome
- type Record
- type Recorder
- func (r *Recorder) Append(ctx context.Context, rec Record) (Record, error)
- func (r *Recorder) Close() error
- func (r *Recorder) Head() (count uint64, headHash string)
- func (r *Recorder) Query(ctx context.Context, f Filter) (rows []Record, total int, err error)
- func (r *Recorder) StartCheckpoints(every time.Duration, logFn CheckpointFunc)
- func (r *Recorder) Verify(ctx context.Context) (Integrity, error)
- type Resource
- type Wire
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Redact ¶
func Redact(raw json.RawMessage) json.RawMessage
Redact returns a copy of the JSON value with every secret-keyed value replaced by the redaction marker, recursively through objects and arrays. Non-JSON or empty input yields nil (nothing to record). On a JSON parse error the input is dropped (returns the marker as a JSON string) rather than passed through — fail closed.
Use it at any explicit emit point that supplies before/after:
audit.Emit(ctx, rec.WithChange(audit.Redact(before), audit.Redact(after)))
Types ¶
type Actor ¶
type Actor struct {
// Org is the tenant the action was taken IN — the EFFECTIVE org (X-Org-Id).
// For all but one caller this is also the actor's own org. Empty for an
// unauthenticated request.
Org string `json:"org"`
// Sub is the user id (IAM `sub`/`preferred_username`). Empty for a service
// principal or an anonymous request.
Sub string `json:"sub"`
// Email is the validated user email, when present.
Email string `json:"email,omitempty"`
// Home is the actor's OWN org (the validated home-org claim, X-User-Owner),
// recorded ONLY when it DIFFERS from Org — i.e. ONLY when this action was
// taken by a platform SuperAdmin acting INSIDE ANOTHER TENANT.
//
// Recording it conditionally is what makes the field MEAN something: a
// non-empty Home is, by construction, an impersonation event. Without it an
// admin org-switch is indistinguishable from a native member of the target
// org, because Org alone says "lux" in both cases — the impersonation fact
// was being destroyed at the moment of recording.
//
// HASH COMPATIBILITY (why omitempty is load-bearing, not style): the chain
// hashes the canonical JSON of the record (canonicalBytes). An omitempty
// field that is empty marshals to NOTHING, so every pre-existing record —
// and every ordinary same-org record written from now on — produces byte-
// identical canonical JSON to before this field existed. Existing hashes
// still verify. See TestVerify_HomeFieldIsHashCompatible.
Home string `json:"home,omitempty"`
}
Actor identifies WHO performed the action. It is populated ONLY from a validated principal (the sanitized X-User-* headers SanitizeIdentity mints from a verified IAM JWT), never from a raw client header — so an actor can never be forged by the request that is being audited. A service principal (M2M / no user sub) records Org with an empty Sub.
type AuthContext ¶
AuthContext records HOW the actor authenticated and what authority they held at decision time — the AC-* evidence (was this a SuperAdmin? by what credential?). Method is "jwt" | "api-key" | "none". IsAdmin is the VALIDATED SuperAdmin bit (owner == AdminOrg), never a raw X-User-IsAdmin.
type Checkpoint ¶
type Checkpoint struct {
Time time.Time `json:"time"`
Count uint64 `json:"count"`
Head string `json:"head"`
}
Checkpoint is a periodic, tamper-EVIDENCE digest of the chain head: the record count and the head hash at a moment in time. It is the AU-9 anchor for TAIL-TRUNCATION detection — an internal chain walk cannot notice that the last K records were deleted (the surviving prefix still verifies), but a durable, INDEPENDENT series of head checkpoints can: Count is monotonic, so any decrease between two consecutive checkpoints is deletion, and an attacker cannot forge a higher count without appending records whose hashes the chain walk would reject.
type CheckpointFunc ¶
type CheckpointFunc func(cp Checkpoint)
CheckpointFunc receives a head digest for the structured (o11y) log. It is a plain func so the pure audit package stays free of any concrete logger type; the cloud wiring adapts luxlog to it.
type CheckpointSink ¶
type CheckpointSink interface {
Checkpoint(ctx context.Context, cp Checkpoint) error
}
CheckpointSink is an optional capability a Mirror may implement to persist the head digest series to an INDEPENDENT store (so truncating the local SQLite cannot also rewrite the anchor history). A Mirror that does not implement it still gets its records; checkpoints then flow only to the structured log.
type Filter ¶
type Filter struct {
Org string // actor_org exact match (the org acted IN)
Sub string // actor_sub exact match (a specific user)
Home string // actor_home exact match — the org the actor came FROM.
// Impersonated restricts to CROSS-ORG actions only (actor_home <> ”), which
// is the question this control exists to answer: "show me every time a
// platform admin acted inside a tenant that was not their own." Without it an
// auditor would have to scan the whole trail to find the events that matter
// most.
Impersonated bool
Action string // action exact match
Resource string // res_type exact match
ResourceID string // res_id exact match (a specific resource instance)
Result string // outcome result: success|deny|error
Since time.Time // ts >= Since (UTC)
Until time.Time // ts <= Until (UTC)
Limit int // max rows (default 100, cap 1000)
Offset int // pagination offset
}
Filter narrows a Query. Zero-value fields are ignored (no constraint), so an empty Filter returns the most-recent Limit records. Time bounds are inclusive and compared against the RFC3339Nano ts column lexicographically (RFC3339 is order-preserving as text, so a string range is a correct time range).
type Integrity ¶
type Integrity struct {
// OK is true iff every record's stored hash equals the recomputed hash AND the
// chain links are continuous (each PrevHash == the prior record's Hash, seqs
// gapless from 0).
OK bool `json:"ok"`
// Count is the number of records walked.
Count uint64 `json:"count"`
// HeadHash is the hash of the last record (or the genesis anchor for an empty
// chain). Pin this externally over time to detect tail-truncation.
HeadHash string `json:"headHash"`
// BrokenAt is the seq of the FIRST record that failed verification, or -1 when
// OK. Reason describes the break (recomputed-hash mismatch, prev-hash
// discontinuity, or a seq gap).
BrokenAt int64 `json:"brokenAt"`
Reason string `json:"reason,omitempty"`
}
Integrity is the result of a Verify walk — the AU-9 evidence that the trail has not been tampered with.
type Mirror ¶
type Mirror interface {
// Append writes one sealed record to the projection. A returned error is
// logged and dropped by the Recorder — the mirror never gates a request.
Append(ctx context.Context, r Record) error
}
Mirror is the optional OLAP projection sink (the datastore/datastore). It is deliberately a tiny interface, not a concrete client, so the Recorder has no compile-time dependency on datastore and tests can supply a fake. Append is called best-effort, asynchronously, off the request path.
type Outcome ¶
type Outcome struct {
Result string `json:"result"`
Status int `json:"status"`
Reason string `json:"reason,omitempty"`
}
Outcome is the result of the action: whether it was allowed and what happened. Result is "success" | "deny" | "error". Status is the HTTP status. Reason is a short, non-sensitive explanation for a deny/error (e.g. "SuperAdmin required", "insufficient_balance") — never a secret, never a raw upstream error body.
type Record ¶
type Record struct {
// Seq is the strictly-increasing chain position (0-based). It is assigned by
// the Recorder under its lock, so it is a true total order with no gaps.
Seq uint64 `json:"seq"`
// Time is the UTC event timestamp (RFC3339Nano).
Time time.Time `json:"time"`
// Actor / Action / Resource / Auth / Outcome — the AU-3 "content of audit
// records" core: who, what, on what, how-authenticated, with what result.
Actor Actor `json:"actor"`
Action string `json:"action"`
Resource Resource `json:"resource"`
Auth AuthContext `json:"auth"`
Outcome Outcome `json:"outcome"`
// SourceIP + UserAgent — the AU-3 "source of the event" fields.
SourceIP string `json:"sourceIp,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
// RequestID correlates the record to the request-line log and any downstream
// trace (the X-Request-Id the pipeline mints).
RequestID string `json:"requestId,omitempty"`
// Method + Path are the HTTP verb and route for a request-sourced event.
Method string `json:"method,omitempty"`
Path string `json:"path,omitempty"`
// Before / After capture a mutation's prior and resulting state for the
// AU-required "before/after" on config-affecting changes. They are populated
// ONLY by explicit emit points and ONLY after Redact has stripped secrets —
// the HTTP middleware never sets them (it never reads bodies), so a secret in
// a request body can never leak here. Raw JSON so any shape round-trips.
Before json.RawMessage `json:"before,omitempty"`
After json.RawMessage `json:"after,omitempty"`
// PrevHash is the hash of record Seq-1 (hex). For the genesis record (Seq 0)
// it is genesisPrevHash. Hash is this record's hash. Neither participates in
// its own hash computation (both are zeroed in canonicalBytes).
PrevHash string `json:"prevHash"`
Hash string `json:"hash"`
}
Record is one audit event. The JSON tags ARE the on-disk and on-wire contract.
Field order in the struct is deliberate but IRRELEVANT to the hash: the chain hashes the CANONICAL (sorted-key) JSON of the record with Hash/PrevHash zeroed (see canonicalBytes), so re-ordering fields or adding an omitempty field can never change an existing record's hash.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder is the single serialized writer that owns the audit chain head and the append-only store. Every Record flows through Append, which under one lock assigns the next Seq, links PrevHash to the current head, seals (hashes), and synchronously persists to SQLite before returning. Concurrency is serialized by mu AND by the single-connection SQLite pool, so the on-disk order equals the chain order with no gaps.
func Open ¶
Open opens (creating if needed) the append-only audit chain named by subsystem under dir, and recovers the chain head from it, so a restart continues the SAME chain rather than forking a new one. mirror may be nil.
The trail belongs to the DEPLOYMENT and not to any tenant, so it is keyed under the system namespace. Which chain — one process's — is the subsystem.
MaxOpenConns(1) serializes every statement against the file lock — the same single-writer discipline pricing/provisioning use, here doubling as the chain's serialization guarantee.
func (*Recorder) Append ¶
Append seals r into the next chain position and persists it. It fills Seq, PrevHash, and Hash (the caller sets everything else), advances the in-memory head only AFTER the durable INSERT succeeds, and mirrors best-effort. A persistence error is returned so the caller can fail the request CLOSED — the head is NOT advanced on failure, so the chain never gaps.
The whole critical section (assign seq → seal → INSERT → advance head) holds mu, so two concurrent requests can never claim the same seq or race the head.
func (*Recorder) Close ¶
Close stops the periodic checkpoint emitter, emits a FINAL checkpoint SYNCHRONOUSLY (so the head at shutdown reaches both the o11y log and the independent digest store before the process exits — the AU-9 anchor must be current exactly when an attacker might trigger shutdown then truncate), and closes the underlying database.
func (*Recorder) Head ¶
Head returns the current chain head (count of records, and the head hash). A count of 0 means the genesis (empty) chain, headHash == genesisPrevHash. An external monitor can pin (count, headHash) over time to detect tail-truncation — which an internal chain walk alone cannot catch (a truncated prefix still verifies). This is the anchor point for AU-9 protection against deletion of the most-recent records.
func (*Recorder) Query ¶
Query returns records matching f, newest first, and the total count matching the same predicate (ignoring Limit/Offset) for pagination. All predicates are parameterized — never string-interpolated — so a filter value can never inject SQL. Column names in the WHERE come from a fixed allowlist below, not caller input.
func (*Recorder) StartCheckpoints ¶
func (r *Recorder) StartCheckpoints(every time.Duration, logFn CheckpointFunc)
StartCheckpoints begins periodic head-digest emission every `every` (no ticker if every<=0; the on-Close checkpoint still fires). logFn, when non-nil, receives each digest for the append-only observability log (o11y), and a mirror implementing CheckpointSink also gets it persisted to an INDEPENDENT store — together the AU-9 anchor an external monitor compares to detect tail-truncation (count regression). MUST be called at most once, before any concurrent Append (a second call is ignored); the emitter stops on Close.
func (*Recorder) Verify ¶
Verify walks the entire chain in seq order, recomputing each record's hash from its content + the running prev-hash and checking continuity. It is the tamper-detector: any modification (a changed field re-hashes differently), any deletion or reordering (a seq gap or a broken prev-hash link), or a forged row (its recomputed hash won't match unless the attacker also recomputed the entire suffix — which they cannot do without re-inserting every subsequent record) is reported with the exact seq where the chain first breaks.
Complexity is O(n) over the records; for very large trails this streams row by row (no full materialization). At cloud's audit volume this is fine; if a trail grows past what an on-demand full walk should touch, verify a seq WINDOW (Verify is easily extended with a bound) or rely on the externally-pinned head.
type Resource ¶
Resource identifies WHAT was acted upon: a type (e.g. "org", "role", "secret", "provider-config", "credit") and its id. For a plain HTTP mutation with no finer resource semantics, Type is the route family and ID is empty — the Action verb + path already pin the object.
type Wire ¶ added in v1.786.131
type Wire struct {
Seq uint64 `json:"seq"`
Time string `json:"time"`
Org string `json:"org"`
Sub string `json:"sub"`
Email string `json:"email,omitempty"`
// Home is present ONLY on a cross-org action: the org the actor came FROM,
// while Org is the org they acted IN. A console row carrying `home` is a
// platform-admin impersonation and should be rendered as one.
Home string `json:"home,omitempty"`
Action string `json:"action"`
Resource string `json:"resource"`
ResourceID string `json:"resourceId,omitempty"`
Method string `json:"method,omitempty"`
Path string `json:"path,omitempty"`
Result string `json:"result"`
Status int `json:"status"`
Reason string `json:"reason,omitempty"`
SourceIP string `json:"sourceIp,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
RequestID string `json:"requestId,omitempty"`
IsAdmin bool `json:"isAdmin"`
Auth string `json:"authMethod,omitempty"`
Hash string `json:"hash"`
PrevHash string `json:"prevHash"`
}
Wire is the JSON shape of one audit record on the operator/console contract. It is cloud's OWN record projection — richer than the IAM record it supersedes: it carries the outcome, the validated auth context, and the hash-chain linkage (Hash/PrevHash) so a console can show tamper-evidence per row. The JSON tags ARE the contract; both the admin god-view (/v1/admin/audit) and the org-scoped trail (/v1/audit) serialize this ONE shape so a single console adapter reads either.