audit

package
v0.3.3 Latest Latest
Warning

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

Go to latest
Published: Nov 11, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package audit writes and verifies the hash-chained audit logs (PLAN §5, security.md "Audit log").

There are two kinds of log: the instance log (<data>/audit.db: logins, users, unseal) and one per tenant. Entries that belong to a business change are written to the outbox of the same database in the same transaction (Enqueue) and delivered exactly once by the Deliverer; events without a business change (a failed login, a denied request) are appended directly (Append). Verify walks a chain and finds the first broken entry.

Index

Constants

View Source
const ActionCheckpoint = "audit.checkpoint"

ActionCheckpoint marks a signed checkpoint (PHASE1 step 6a). It is a normal entry of the chain: it signs the entry right before it, and deleting or changing it breaks the chain like any other entry. Rewriting the chain before a checkpoint needs its signing key, which only exists while the server is unsealed.

View Source
const CheckpointEvery = 1000

CheckpointEvery: a checkpoint is made after this many new entries, besides the ones at unseal, before seal and once a day.

View Source
const OutboxRetention = 30 * 24 * time.Hour

OutboxRetention: delivered outbox rows are deleted after this time; they are only needed to redeliver after a crash or restore. Undelivered rows are never deleted.

View Source
const TimeFormat = "2006-01-02T15:04:05.000Z"

TimeFormat is used for every time in the audit log and the outbox: UTC with milliseconds and a fixed width, so text order is time order.

Variables

View Source
var ErrInvalid = errors.New("invalid audit entry")

ErrInvalid is wrapped by every error about an entry that may not be logged.

View Source
var ErrNoSigner = errors.New("checkpoint key unavailable")

ErrNoSigner means no checkpoint key is available – the server is sealed. The Signer wraps it into its own error.

Functions

func Append

func Append(ctx context.Context, db *sql.DB, scope string, e Entry, outboxID string) (seq int64, appended bool, err error)

Append adds e to the log of scope in its own transaction and returns its seq. db must be the log's single writer connection. With a non-empty outboxID an entry that is already in the log is not added again: then Append returns its existing seq and appended=false.

func CheckHead

func CheckHead(ctx context.Context, q auditdb.DBTX, want Head) error

CheckHead compares the log with a head noted earlier (process log, backup manifest): the entry at that seq must still exist and have that hash. This catches what the chain alone can't – a cut-off end, or a chain that was recomputed from that entry on.

func Enqueue

func Enqueue(ctx context.Context, tx *sql.Tx, now time.Time, e Entry) error

Enqueue writes e to the outbox of the producing database inside tx – the same transaction as the business change it describes, so both are stored or neither is. now is the outbox row's creation time.

func Genesis

func Genesis(scope string) string

Genesis is prev_hash of the first entry of the log of scope. Binding the start to the scope means a log can't be passed off as another scope's log.

func KeyFingerprint

func KeyFingerprint(pub ed25519.PublicKey) string

KeyFingerprint is how a checkpoint key is shown and pinned: the first 16 bytes of SHA-256 over the public key, in hex.

Types

type Actor

type Actor struct {
	Type ActorType
	ID   string
	Name string
}

Actor is who acted. Name is what an auditor reads (user name, token name).

type ActorType

type ActorType string

ActorType says who acted.

const (
	ActorUser        ActorType = "user"
	ActorToken       ActorType = "token"
	ActorSystem      ActorType = "system"
	ActorACMEAccount ActorType = "acme_account"
	ActorAgent       ActorType = "agent"
)

Actor types (CHECK constraint in schema/audit/0002).

type Backlog

type Backlog struct {
	Source  string
	Pending int64
	Oldest  time.Time // zero when nothing is pending
}

Backlog is the undelivered part of one outbox.

type Break

type Break struct {
	Seq    int64
	Reason string
}

Break describes the first entry where a chain doesn't hold.

type CheckpointInfo

type CheckpointInfo struct {
	Seq  int64
	Time string
}

CheckpointInfo locates the newest checkpoint.

type CheckpointLog

type CheckpointLog struct {
	DB    *sql.DB // writer of the audit log
	Scope string
}

CheckpointLog is one audit log the Checkpointer signs.

type CheckpointResult

type CheckpointResult struct {
	Made bool
	Head Head   // the checkpoint entry itself
	Key  string // fingerprint of the signing key
}

CheckpointResult describes a new checkpoint.

func Checkpoint

func Checkpoint(ctx context.Context, db *sql.DB, scope string, s Signer, now time.Time) (res CheckpointResult, err error)

Checkpoint appends a signed checkpoint covering the current head of the log. It does nothing (Made=false) for an empty log or when the head is a checkpoint already.

type Checkpointer

type Checkpointer struct {
	Logs   []CheckpointLog
	Signer Signer
	Clock  clock.Clock
	Log    *slog.Logger
	Every  int64 // 0 = CheckpointEvery
}

Checkpointer makes checkpoints for several logs. The server calls MakeAll after unseal, before seal and daily, and MaybeEvery after deliveries.

func (*Checkpointer) MakeAll

func (c *Checkpointer) MakeAll(ctx context.Context, reason string) error

MakeAll makes a checkpoint in every log that has new entries. A sealed keyring is not an error: there is just no checkpoint.

func (*Checkpointer) MaybeEvery

func (c *Checkpointer) MaybeEvery(ctx context.Context) error

MaybeEvery makes a checkpoint in every log with at least CheckpointEvery entries since its last checkpoint.

type Deliverer

type Deliverer struct {
	Routes []Route
	Clock  clock.Clock
	Log    *slog.Logger

	// Checkpoints, if set, makes a checkpoint after every 1000 entries and
	// once a day (step 6a).
	Checkpoints *Checkpointer
	// contains filtered or unexported fields
}

Deliverer moves outbox rows into the audit logs.

func (*Deliverer) Backlog

func (d *Deliverer) Backlog(ctx context.Context) ([]Backlog, error)

Backlog reports what waits in each route's outbox – the server warns with count and age while deliveries fail (owner 2026-10-16: outbox entries may pile up, they are never lost; direct entries are fail-closed instead).

func (*Deliverer) Cleanup

func (d *Deliverer) Cleanup(ctx context.Context) (int64, error)

Cleanup deletes outbox rows that were delivered more than OutboxRetention ago. Undelivered rows are never deleted, however old they are.

func (*Deliverer) DeliverPending

func (d *Deliverer) DeliverPending(ctx context.Context) (int, error)

DeliverPending delivers every undelivered outbox row of every route and returns how many entries were added. Each row is appended in its own audit transaction and then marked delivered in the producing database. A crash in between is harmless: the row is delivered again and the audit log recognizes its outbox_id. Errors of one route don't stop the others.

func (*Deliverer) LogHeads

func (d *Deliverer) LogHeads(ctx context.Context)

LogHeads writes the head of every audit log to the process log. The process log usually ends up somewhere else (journald, a log collector), so it is an anchor for `myownpki audit verify --expect-head`.

func (*Deliverer) Run

func (d *Deliverer) Run(ctx context.Context, wake <-chan struct{}, pollInterval time.Duration)

Run delivers at start, whenever wake fires (send after committing outbox rows), and every pollInterval; once a day it logs the chain heads and cleans up the outbox. It returns when ctx ends.

type Entry

type Entry struct {
	Time     time.Time
	Actor    Actor
	Action   string // dotted, e.g. "certificate.issue"
	Target   Target
	Result   Result
	SourceIP string         // IP address only (no port, no host name); "" if none
	Details  map[string]any // never secrets – see forbiddenKey
}

Entry is one event before it gets a place in the chain.

type Head struct {
	Seq  int64
	Hash string
}

Head identifies the newest entry of a chain: "seq:hash".

func ParseHead

func ParseHead(s string) (Head, error)

ParseHead reads "seq:hash"; a hash prefix of at least 12 hex digits is enough, so a head can be noted down by hand.

func (Head) String

func (h Head) String() string

type Report

type Report struct {
	Entries int64
	Head    Head   // Seq 0 = empty log
	Break   *Break // nil = intact

	Checkpoints    int64
	LastCheckpoint *CheckpointInfo // nil = none yet
	CheckpointKey  string          // fingerprint of the key all checkpoints use
}

Report is the result of Verify.

func Verify

func Verify(ctx context.Context, q auditdb.DBTX, scope string) (Report, error)

Verify walks the whole log of scope and reports the first broken entry: a gap in seq (deleted entry), a prev_hash that doesn't match the previous entry (deleted, inserted or reordered entries) or a hash that doesn't match the entry's content (modified entry). It only reads.

func VerifyWith

func VerifyWith(ctx context.Context, q auditdb.DBTX, scope string, opts VerifyOptions) (Report, error)

VerifyWith is Verify with options. Checkpoints are checked on the way: each must cover the entry before it, carry a valid Ed25519 signature and use the same (and, if pinned, the expected) key.

type Result

type Result string

Result is the outcome of the action.

const (
	ResultSuccess Result = "success"
	ResultFailure Result = "failure"
	ResultDenied  Result = "denied"
)

Results: denied = refused by policy or permissions, failure = attempted but failed (e.g. wrong password, signing error).

type Route

type Route struct {
	Source string  // outbox_id prefix, e.g. "system" or "pki"; unique per audit log
	From   *sql.DB // writer of the producing database
	To     *sql.DB // writer of the audit log
	Scope  string  // scope of the audit log
}

Route connects the outbox of one producing database with one audit log.

type Signer

type Signer interface {
	SignCheckpoint(msg []byte) (sig []byte, pub ed25519.PublicKey, err error)
}

Signer signs checkpoints; *keys.Keyring implements it with an Ed25519 key derived from the root key.

type Target

type Target struct {
	Type string
	ID   string
}

Target is the object acted on, e.g. {certificate c-0101}.

type VerifyOptions

type VerifyOptions struct {
	// CheckpointKey pins the checkpoint key: a fingerprint (prefix of at
	// least 16 hex digits) noted at init or taken from the process log.
	// Without it, Verify only checks that all checkpoints use one key.
	CheckpointKey string
}

VerifyOptions tighten Verify.

Jump to

Keyboard shortcuts

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