Documentation
¶
Overview ¶
Package audit defines the control plane's audit-log interface (golden rule 5: every state-changing action writes an event).
Phase 0 ships only the interface and two throw-away sinks: MemorySink (tests, in-process assertions) and SlogSink (structured log line per event). Phase 2 replaces both with the SQLite-backed, hash-chained, append-only implementation (plus verify-chain and export) behind the same Sink interface, so callers written against this package need no changes.
Event.Details is free-form context (image reference, hostname, override code, …). It MUST NEVER contain secret values (golden rule 3): the audit log is durable, exported, and shown in the UI. SlogSink defensively redacts values whose key looks secret-bearing (see IsSensitiveKey), but that is a backstop, not a licence — callers put identifiers in Details, never values.
Index ¶
- Constants
- Variables
- func Canonical(e Event) ([]byte, error)
- func CanonicalDetails(details map[string]string) string
- func FormatTime(t time.Time) string
- func HashRow(canonical []byte, prevHash string) string
- func IsSensitiveKey(key string) bool
- func ParseFormat(s string) (string, error)
- type Actor
- type Chain
- func (c *Chain) Export(ctx context.Context, w io.Writer, format string) error
- func (c *Chain) ExportAs(ctx context.Context, actor Actor, w io.Writer, format string) error
- func (c *Chain) List(ctx context.Context, afterSeq int64, limit int64) ([]Row, error)
- func (c *Chain) ListAs(ctx context.Context, actor Actor, afterSeq, limit int64) ([]Row, error)
- func (c *Chain) Record(ctx context.Context, e Event) error
- func (c *Chain) Verify(ctx context.Context) (Report, error)
- func (c *Chain) VerifyAs(ctx context.Context, actor Actor) (Report, error)
- type Event
- type MemorySink
- type Report
- type Row
- type Sink
- type SlogSink
Constants ¶
const ( ResultOK = "ok" ResultDenied = "denied" ResultError = "error" )
Result values for Event.Result.
const ( FormatJSONL = "jsonl" FormatCSV = "csv" )
Export formats.
const GenesisPrevHash = "0000000000000000000000000000000000000000000000000000000000000000"
GenesisPrevHash is the prev_hash of the first row of the chain: 64 zero hex digits, so that the genesis link is as visible in an export as every other one.
const PermAuditRead = "audit.read"
PermAuditRead mirrors auth.PermAuditRead. The auth package imports this one, so the string is duplicated here the same way internal/deploy mirrors its permissions; the httpapi tests pin the two constants to each other.
const Redacted = "[redacted]"
Redacted replaces the value of any Details entry whose key looks secret-bearing.
Variables ¶
var CSVHeader = []string{"seq", "ts", "actor", "action", "resource_type", "resource_id", "request_id", "result", "details", "prev_hash", "hash"}
CSVHeader is the first line of a CSV export.
var ErrForbidden = errors.New("audit: forbidden")
ErrForbidden is returned when the actor lacks PermAuditRead (HTTP 403).
var ErrNoTimestamp = errors.New("audit: canonical: event has no timestamp")
ErrNoTimestamp is returned by Canonical for an event whose Time is zero: the timestamp is part of what is hashed, so it must be fixed before the bytes are.
var ErrUnknownFormat = errors.New("audit: unknown export format (want jsonl or csv)")
ErrUnknownFormat is returned for an export format other than jsonl or csv.
Functions ¶
func Canonical ¶
Canonical returns the pinned, deterministic JSON encoding of e that the hash chain is computed over. The encoding is fixed forever (a change would break verification of every existing log):
- one object, fields in this order and always present: ts, actor, action, resource_type, resource_id, request_id, result, details;
- ts is RFC 3339 with nanoseconds (trailing zeros trimmed, as Go's RFC3339Nano does) in UTC, "Z" suffix;
- details is an object whose keys are sorted byte-wise and whose values are strings — no number, boolean, null, array or nested object can ever appear;
- strings are JSON-quoted without HTML escaping (<, >, & stay literal); control characters are \u00XX-escaped; invalid UTF-8 is replaced by U+FFFD before quoting, so the bytes are stable for any input;
- no whitespace, no trailing newline.
Details values for keys that IsSensitiveKey considers secret-bearing are not encoded — they are dropped, exactly as Chain.Record drops them before storing (golden rule 3), so the hash covers what is stored and nothing else.
func CanonicalDetails ¶
CanonicalDetails encodes details alone (sorted keys, string values, sensitive keys dropped). This is the exact string stored in audit_events.details_json.
func FormatTime ¶
FormatTime renders t exactly as the canonical "ts" field (RFC 3339, nanoseconds, UTC).
func HashRow ¶
HashRow computes the chain hash of one row: hex(SHA-256(canonical || prev_hash)), where prev_hash is the previous row's hex hash as ASCII bytes (GenesisPrevHash for the first row).
func IsSensitiveKey ¶
IsSensitiveKey reports whether a Details key looks like it names a secret value: any token of the key (see keyTokens) is in sensitiveTokens and the last token is not an identifier word such as "name" or "id". It errs on the side of redacting — "db_password", "GITHUB_TOKEN", "clientSecret", "aws_secret_access_key", "deploy_key", "Authorization" and "totp_code" are all sensitive; "image", "tokenizer", "hostname", "secret_name" and "key_id" are not. Sinks that render values redact sensitive keys. This is a backstop for golden rule 3, not a licence to put secret values in Details.
func ParseFormat ¶
ParseFormat normalises a user-supplied format name.
Types ¶
type Actor ¶
type Actor struct {
ID string
Email string
Role string
// Can reports whether the actor holds a permission; nil means "bootstrap: allow all".
Can func(perm string) bool
}
Actor identifies who reads the chain (from the session, API token or bootstrap principal). It mirrors deploy.Actor so the JSON API converts a principal the same way for every service.
type Chain ¶
type Chain struct {
Store *store.Store
// Now is overridable for tests; nil means time.Now.
Now func() time.Time
// Redact, when set, is applied to every free-text field (actor, resource_id, details values)
// before storage. platformd wires the process-wide secrets.Redactor so a secret value that a
// caller mistakenly put in Details can never reach the durable log (golden rule 3 backstop).
Redact func(string) string
// contains filtered or unexported fields
}
Chain is the SQLite-backed, hash-chained, append-only audit log (golden rule 5). It implements Sink; every row's hash is hex(SHA-256(Canonical(event) || prev_hash)) with prev_hash the hex hash of the previous row (GenesisPrevHash for the first). UPDATE and DELETE on audit_events are refused by triggers (migration 00001), and Verify recomputes every hash and link so that an attacker who drops the triggers or edits the file directly is still detected.
Timestamps: an event whose Time is zero is stamped with Now() at insert time. A Time set by the caller is kept as-is — services stamp events with their own clock at the moment the state change happened, which is the instant an auditor cares about, and every caller is in-process (there is no remote submission path), so it is still a server-side timestamp.
func (*Chain) Export ¶
Export streams every row to w, oldest first. JSONL writes one object per line holding the event's canonical fields plus seq, prev_hash and hash (HTML is not escaped, so the line is what an auditor reads). CSV writes CSVHeader then one record per row with details as the canonical JSON object. Uploading the result to an S3 Object Lock bucket is a separate, optional step outside this package.
func (*Chain) ExportAs ¶
ExportAs is Export for an actor: it requires PermAuditRead. Nothing is written to w when the actor is refused, so a handler can still answer 403.
func (*Chain) List ¶
List returns up to limit rows with seq > afterSeq, oldest first. limit is clamped to 1..verifyPage (0 means 100).
func (*Chain) ListAs ¶
ListAs is List for an actor: it requires PermAuditRead (golden rule 6; the denial is audited).
func (*Chain) Record ¶
Record appends e to the chain. It never skips the write because ctx was cancelled (see Sink): the insert runs under context.WithoutCancel with its own bounded timeout. Details values under secret-bearing keys (IsSensitiveKey) are dropped, never stored; every stored string is passed through Redact when one is configured.
type Event ¶
type Event struct {
Time time.Time
Actor string
Action string
ResourceType string
ResourceID string
RequestID string
// Result is one of ResultOK, ResultDenied, ResultError.
Result string
Details map[string]string
}
Event is one audit record. Details must never carry secret values (see the package comment).
type MemorySink ¶
type MemorySink struct {
// contains filtered or unexported fields
}
MemorySink keeps events in memory, in the order they were recorded. It exists for tests and for the Phase 0 skeleton only; Phase 2 replaces it with the hash-chained SQLite log.
func NewMemorySink ¶
func NewMemorySink() *MemorySink
NewMemorySink returns an empty, thread-safe in-memory sink.
func (*MemorySink) Events ¶
func (m *MemorySink) Events() []Event
Events returns a copy of every recorded event, oldest first.
type Report ¶
type Report struct {
// Rows is the number of rows examined.
Rows int `json:"rows"`
// OK is true when every row hashed and linked correctly and no seq was missing.
OK bool `json:"ok"`
// FirstBad is the seq of the first row that failed (or the first missing seq); nil when OK.
FirstBad *int64 `json:"first_bad"`
// Reason describes the first failure in one line; empty when OK.
Reason string `json:"reason,omitempty"`
}
Report is the outcome of a chain verification.
func VerifyExport ¶
VerifyExport re-verifies a JSONL export offline, without the database: every line is parsed strictly (unknown fields refused), its hash recomputed from the canonical encoding of its fields, and its prev_hash and seq checked against the previous line. This is what an auditor runs against a copy of the export they hold themselves. A malformed line is reported as the first bad row (its seq when it can be read, otherwise the seq expected at that position).
type Row ¶
type Row struct {
Seq int64 `json:"seq"`
TS string `json:"ts"`
Actor string `json:"actor"`
Action string `json:"action"`
ResourceType string `json:"resource_type"`
ResourceID string `json:"resource_id"`
RequestID string `json:"request_id"`
Result string `json:"result"`
Details map[string]string `json:"details"`
PrevHash string `json:"prev_hash"`
Hash string `json:"hash"`
}
Row is one stored audit event with its position and hashes.
type Sink ¶
Sink receives audit events. Implementations must be safe for concurrent use.
Record must never drop an event because the request that triggered the action was cancelled: by the time Record is called the state change has already happened, and golden rule 5 requires that it be recorded regardless of whether the client is still connected. ctx therefore carries request-scoped values (trace/request IDs for the logger) but not a licence to skip the write. I/O-backed sinks (the Phase 2 SQLite hash chain) must perform the insert under context.WithoutCancel(ctx), applying their own bounded timeout if one is needed.