audit

package
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package audit defines the append-only audit contract. The Postgres store is the system of record; sinks fan events out (OTLP, syslog, SIEM). Audit is free forever — never gate it.

Index

Constants

View Source
const (
	// DenialAction and DenialOutcome are the only rows the coalescer acts on.
	DenialAction  = authz.AuditAction
	DenialOutcome = "denied"
	// CoalesceAction is the summary row's action. It is never authz.denied, so
	// a SIEM rule over denials does not count it twice.
	CoalesceAction = "preflight.denial.coalesce"

	// DefaultCoalesceWindow is how long repeats are counted before the summary.
	DefaultCoalesceWindow = 10 * time.Minute
	// DefaultCoalesceMaxKeys bounds the open windows.
	DefaultCoalesceMaxKeys = 1024
)
View Source
const (

	// PendingMarker is the data key a row carries while any of its fields waits
	// under the pending key. Such a row lives only in the spool: the drain
	// re-seals it before the store sees it.
	PendingMarker = "pending_subject"

	// ErasedValue is what a reader shows for a field whose subject key was
	// destroyed.
	ErasedValue = "[erased]"

	// DeviceActorPrefix begins the actor of a row an enrolled device forwarded
	// (store.FederatedActor) and of this organisation's rows about a device. A
	// subject id or sealed field on a forwarded row was written under the
	// device's own keys, so Unseal leaves its target and data as stored.
	DeviceActorPrefix = "device:"
)

A sealed field is a JSON string in place of the value it hides:

seal2.<handle>.<ciphertext>      under the subject's own key
seal2p.<ciphertext>              under the platform pending key

handle is the random id of the subject's key generation (principal_keys.handle): it names the key, never the person, is not derived from anything about them, and is cleared when the key is destroyed. So the row, and every copy of it (the spool, a sink, an export, a federation push), says whose field it is only through the key table, and after an erasure not even there. ciphertext is unpadded base64url of kek.Seal of the original JSON value; its additional data binds the event id, the action, the field's path and the handle, so a ciphertext moved to another event, field or key fails to open. A pending ciphertext holds the name the value is for inside it, beside the value, for the drain to resolve. The row hash covers the string, so the chain verifies whether or not the key is still there.

View Source
const (
	// PendingActorKey is the data key that holds a human actor waiting under the
	// pending key, and PendingActor the placeholder the actor column carries
	// meanwhile, so the spool never holds the name. PendingTargetKey and
	// PendingTarget are their twins for a person-target action's target column.
	PendingActorKey  = "pending_actor"
	PendingActor     = SubjectActorPrefix + "pending"
	PendingTargetKey = "pending_target"
	PendingTarget    = SubjectActorPrefix + "pending"
)
View Source
const DryRunDatum = "dry_run"

DryRunDatum is the data key the marker rides under.

View Source
const SealPurpose = "audit-seal"

SealPurpose is the principal_keys purpose a person's sealed audit fields sit under (subjectkey.PurposeAuditSeal; cmd/wardynd pins that they are equal).

View Source
const SubjectActorPrefix = "subject:"

SubjectActorPrefix begins the actor of a human row written under SealFull. A principal that starts with it is refused everywhere a person enters, so no one can sign in as, or be recorded under, another person's subject.

Variables

View Source
var ErrKeyErased = errors.New("audit seal: the subject's key is destroyed")

ErrKeyErased is what a SealKeys returns, wrapped, for a key that was destroyed (or whose row is gone): what it sealed is unrecoverable.

View Source
var ErrPendingUnopenable = errors.New("audit seal: a pending value does not open under the pending key")

ErrPendingUnopenable is what Reseal returns, wrapped, for a pending field or actor that is malformed or does not open under the pending key. That never heals by waiting, so the spool drain counts it toward quarantine instead of deferring it.

View Source
var ErrReplayDeferred = errors.New("audit replay deferred: a pending row cannot be re-sealed yet")

ErrReplayDeferred is what the spool drain's recorder returns for a pending row it cannot re-seal yet. The drain keeps the line and retries; it is never counted toward quarantine, because the line is not what is wrong.

View Source
var SealLeafNames = []string{"task", "prompt", "message", "reason", "comment", "note", "email", "display_name", "query", "command"}

SealLeafNames is the closed set of data key names a field may carry to be a candidate for sealing. It does not seal anything by itself.

View Source
var UnsealedActions = []string{"person.erasure", "credential.erase", "principal_key.destroyed"}

UnsealedActions are actions whose rows are never sealed, so the record of an erasure survives the key it destroys and a person's key is named in the clear. Pinned against sealFields by a test.

Functions

func DelegationFrom added in v0.8.0

func DelegationFrom(ctx context.Context) (types.DelegationVia, bool)

DelegationFrom reports the portal and delegated token ctx's request came through, when it did.

func DryRunFrom added in v0.8.6

func DryRunFrom(ctx context.Context) bool

DryRunFrom reports whether ctx serves a dry run.

func ForgedSubject added in v0.9.0

func ForgedSubject(ev types.AuditEvent) bool

ForgedSubject reports whether ev carries a subject string no Seal wrote: a human actor, the target of a person-target action, or a sponsor field (seal_sponsor.go), that already begins with SubjectActorPrefix. Unseal renders such a string as the person it names, so a caller who typed one (a path segment, a request body) could read an erased person's rows back; Seal refuses the row instead.

func IsPending added in v0.8.6

func IsPending(ev types.AuditEvent) bool

IsPending reports whether ev waits under the pending key.

func LogWriteFailure added in v0.3.1

func LogWriteFailure(ctx context.Context, ev types.AuditEvent, err error)

LogWriteFailure reports a dropped audit write at ERROR level so an operator can alert on it; it must never block the caller.

ev.Data is deliberately NOT logged: it can carry sensitive per-action payloads (grant scopes, jti, approval ids). Callers must not add it here.

func PersonTargetActions added in v0.9.0

func PersonTargetActions() []string

PersonTargetActions is personTargetActions, sorted.

func SealsAction added in v0.8.6

func SealsAction(action string) bool

SealsAction reports whether any field of action is sealed.

func SealsSponsor added in v0.9.0

func SealsSponsor(ev types.AuditEvent) bool

SealsSponsor reports whether ev holds a sponsor field SealFull stores as a subject.

func SealsTarget added in v0.9.0

func SealsTarget(action string) bool

SealsTarget reports whether the target of a row of action is a person stored as a subject under SealFull: a person-target action, and not one of UnsealedActions.

func SponsorFrom added in v0.9.0

func SponsorFrom(ctx context.Context) (string, bool)

SponsorFrom reports the sponsor of the service identity ctx's request was made by, when it was.

func StampDelegation added in v0.8.0

func StampDelegation(ctx context.Context, data json.RawMessage) json.RawMessage

StampDelegation returns data with the portal added under "via" when ctx is a delegated request, and data unchanged otherwise. Data that is not a JSON object is kept whole under "data", so no delegated row can lack the stamp.

func StampDryRun added in v0.8.6

func StampDryRun(ctx context.Context, data json.RawMessage) json.RawMessage

StampDryRun returns data with dry_run set true when ctx serves a dry run, and with any dry_run key removed otherwise, so no writer's data can carry the marker outside preflight. A marked row whose data is not a JSON object keeps it whole under "data", the way StampDelegation does.

func StampRelay added in v0.9.0

func StampRelay(ctx context.Context, data json.RawMessage) json.RawMessage

StampRelay adds the runner transport without changing the event's actor or via.

func StampSponsor added in v0.9.0

func StampSponsor(ctx context.Context, data json.RawMessage) json.RawMessage

StampSponsor returns data with the sponsor added under "sponsor" when ctx is a service request, and data unchanged otherwise. Data that is not a JSON object is kept whole under "data", so no service row can lack the stamp.

func WithDelegation added in v0.8.0

func WithDelegation(ctx context.Context, via types.DelegationVia) context.Context

WithDelegation marks ctx as a request made through via.

func WithDryRun added in v0.8.6

func WithDryRun(ctx context.Context) context.Context

WithDryRun marks ctx as serving a dry run.

func WithRelay added in v0.9.0

func WithRelay(ctx context.Context, runnerID uuid.UUID) context.Context

WithRelay marks a request with its authenticated runner, never a client header.

func WithSponsor added in v0.9.0

func WithSponsor(ctx context.Context, sponsor string) context.Context

WithSponsor marks ctx as a request made by a service identity sponsored by sponsor.

Types

type ActorTranslator added in v0.8.6

type ActorTranslator interface {
	// StoredActor is the stored actor for name; ok is false when there is none
	// (the person has no subject), and the filter then matches name alone.
	StoredActor(ctx context.Context, name string) (actor string, ok bool, err error)
}

ActorTranslator turns the person an audit actor filter names into the actor the store holds for them under SealFull.

type DelegationRecorder added in v0.8.0

type DelegationRecorder struct{ Inner Recorder }

DelegationRecorder stamps data.via on every event recorded under a delegated context, and data.sponsor under a service identity's, whoever wrote it. cmd/wardynd puts it at the head of the shared chain.

func (DelegationRecorder) Record added in v0.8.0

Record stamps ev when ctx is delegated and hands it on.

type DenialCoalescer added in v0.8.6

type DenialCoalescer struct {
	// Inner receives every event, summaries included. Set before first use.
	Inner Recorder
	// Window and MaxKeys default to DefaultCoalesceWindow and DefaultCoalesceMaxKeys.
	Window  time.Duration
	MaxKeys int
	// contains filtered or unexported fields
}

DenialCoalescer summarises repeated refused dry runs. It is per replica: each keeps its own windows. It acts only on an authz.denied/denied event recorded under a dry-run context (DryRunFrom), never on what the event's data says; every other event, a launch refusal included, passes through untouched.

func (*DenialCoalescer) Flush added in v0.8.6

func (c *DenialCoalescer) Flush(ctx context.Context)

Flush closes every open window and writes its summary through Inner. Run it on graceful shutdown, before the audit sinks and the pool are closed.

func (*DenialCoalescer) Record added in v0.8.6

func (c *DenialCoalescer) Record(ctx context.Context, ev types.AuditEvent) error

Record passes ev on, or counts it when an identical refused dry run is already inside its window.

type DryRunRecorder added in v0.8.6

type DryRunRecorder struct{ Inner Recorder }

DryRunRecorder stamps data.dry_run on every event recorded under a dry-run context, whoever wrote it, and strips the key from every other. cmd/wardynd puts it at the head of the shared chain beside DelegationRecorder.

func (DryRunRecorder) Record added in v0.8.6

func (r DryRunRecorder) Record(ctx context.Context, ev types.AuditEvent) error

Record stamps ev and hands it on.

type Recorder

type Recorder interface {
	Record(ctx context.Context, ev types.AuditEvent) error
}

Recorder persists events to the system of record (append-only).

type SealField added in v0.8.6

type SealField struct {
	Path    string
	Subject Subject
}

SealField is one personal field of one action's data: its JSON path (dots walk into nested objects) and whose it is.

func SealedFields added in v0.8.6

func SealedFields(action string) []SealField

SealedFields lists the fields a row of action seals; nil for an action that seals none.

type SealKeys added in v0.8.6

type SealKeys interface {
	Current(ctx context.Context, owner, purpose string) (handle uuid.UUID, key []byte, err error)
	Key(ctx context.Context, purpose string, handle uuid.UUID) ([]byte, error)
}

SealKeys is the per-subject key service; *subjectkey.Manager (through cmd/wardynd's adapter) is the production one. Current returns the subject's current key and the handle a sealed field names it by, creating the subject's first key when there is none. Key reads the key a handle names and fails ErrKeyErased once it is destroyed, or when no key carries the handle.

type SealMode added in v0.8.6

type SealMode string

SealMode is WARDYN_AUDIT_SEAL.

const (
	// SealOff stores every field in the clear (the default).
	SealOff SealMode = "off"
	// SealFields seals the personal data fields SealedFields lists.
	SealFields SealMode = "fields"
	// SealFull is SealFields and the human actor too: the actor column holds
	// SubjectActorPrefix and the person's subject id.
	SealFull SealMode = "full"
)

func ParseSealMode added in v0.8.6

func ParseSealMode(s string) (SealMode, error)

ParseSealMode reads WARDYN_AUDIT_SEAL; empty is off.

type Sealer added in v0.8.6

type Sealer struct {
	// Keys serves the subjects' keys.
	Keys SealKeys
	// Pending returns the platform pending key (32 bytes), nil when there is
	// none. It seals a field whose subject key could not be had, so the row can
	// wait in the spool instead of being dropped or written in the clear.
	Pending func() []byte
	// Resolve maps a name an event carries (an email form, an entra: principal)
	// to the person's principal, the key every erasure destroys by. Nil keeps
	// the name as it is.
	Resolve func(ctx context.Context, name string) (string, error)
	// GoneSince reports whether subject's seal key was destroyed after since. A
	// pending field that waited past its subject's erasure is then erased on
	// re-seal instead of sealed under a fresh key. Nil means never.
	GoneSince func(ctx context.Context, subject string, since time.Time) (bool, error)
	// Subjects renders and translates the subject actors of rows written under
	// SealFull. Nil reads every actor as it is stored.
	Subjects SubjectDirectory
	// SealActor stores a human actor as its subject (WARDYN_AUDIT_SEAL=full).
	// Reads open subject actors whatever its value, so rows written while it was
	// on stay readable after it is turned off.
	SealActor bool
}

Sealer seals the personal fields of audit rows under their subjects' keys.

func (*Sealer) ActorSubject added in v0.8.6

func (s *Sealer) ActorSubject(ctx context.Context, principal string) (string, error)

ActorSubject is the actor column a human row written outside the recorder chain stores under SealFull (the retention drop's event and anchor): the person's subject, with their audit-seal key created when there is none, as Seal stores it. A person with no identity row keeps their name.

func (*Sealer) Reseal added in v0.8.6

func (s *Sealer) Reseal(ctx context.Context, ev types.AuditEvent) (types.AuditEvent, error)

Reseal is the spool drain's step for a row that waited under the pending key: each pending field is opened and sealed under its subject's own key, the marker is dropped, and the row can be stored. A field whose subject was erased while it waited becomes ErasedValue rather than being sealed under a new key. Any failure is returned and the caller keeps the spool line; a field that carries the pending prefix but does not parse is ErrPendingUnopenable, so the drain quarantines the line instead of storing the junk.

func (*Sealer) Seal added in v0.8.6

Seal returns ev with the fields sealFields lists for its action sealed. A field whose subject key could not be had is sealed under the pending key and the row carries PendingMarker: pending is then true, and the row must go to the spool, never to the store. No path writes a personal field in the clear; when even the pending key is missing, Seal fails.

func (*Sealer) StoredActor added in v0.8.6

func (s *Sealer) StoredActor(ctx context.Context, name string) (string, bool, error)

StoredActor implements ActorTranslator.

func (*Sealer) Unseal added in v0.8.6

func (s *Sealer) Unseal(ctx context.Context, evs []types.AuditEvent) ([]types.AuditEvent, error)

Unseal returns evs with every sealed field opened. A field whose key was destroyed reads ErasedValue. A key that cannot be had for another reason (the key store is down) fails the whole read, so a reader never mistakes ciphertext for the record. A string that does not open under its own additional data is left as it is: it is not a seal Wardyn wrote for this row. A device row (DeviceActorPrefix) keeps its target and data as stored: a subject id or sealed field on it was written under the device's own keys.

type Sink

type Sink interface {
	Name() string // "otlp" | "syslog" | ...
	Emit(ctx context.Context, ev types.AuditEvent) error
}

Sink streams recorded events to an external destination. Sinks must not block recording; failures are logged and retried, never silently dropped.

type Subject added in v0.8.6

type Subject int

Subject says whose a sealed field is, and so whose key seals it.

const (
	// SubjectActor is the event's actor, when a person: the one who typed it.
	SubjectActor Subject = iota + 1
	// SubjectTarget is the event's target column: the person the row is about.
	SubjectTarget
)

type SubjectDirectory added in v0.8.6

type SubjectDirectory interface {
	// AuditSubjectFor is principal's subject id; ok is false when the person has
	// no identity row, and their actor then stays as it is.
	AuditSubjectFor(ctx context.Context, principal string) (id string, ok bool, err error)
	// AuditPrincipalOf is the principal an id names; ok is false for an id no
	// identity row carries.
	AuditPrincipalOf(ctx context.Context, id string) (principal string, ok bool, err error)
	// AuditKeyDestroyedAt is when the latest generation of principal's seal key
	// was destroyed; ok is false while none has been.
	AuditKeyDestroyedAt(ctx context.Context, principal string) (at time.Time, ok bool, err error)
}

SubjectDirectory maps a person to the subject id a SealFull row carries in its actor, and back. The id is the person's oldest principal_identities row.

type Unsealer added in v0.8.6

type Unsealer interface {
	Unseal(ctx context.Context, evs []types.AuditEvent) ([]types.AuditEvent, error)
}

Unsealer opens the sealed fields of rows being read.

Directories

Path Synopsis
Acknowledged audit delivery (#1513): the opt-in mode where a leader-only loop reads the already-masked, hash-chained rows from audit_events and delivers them to a webhook collector from a durable checkpoint, advancing only on the collector's acceptance.
Acknowledged audit delivery (#1513): the opt-in mode where a leader-only loop reads the already-masked, hash-chained rows from audit_events and delivers them to a webhook collector from a durable checkpoint, advancing only on the collector's acceptance.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL