Vault

Audit Logging

Append-only audit trail with extensible hook system.

The audit system spans two packages: audit provides the core append-only audit log with persistence, while audit_hook provides an extensible event recording system for broadcasting audit events to external systems. Together they give you a complete audit trail for every Vault operation.

Architecture

audit.Logger
  ├── audit.Store          (append-only persistence)
  ├── audit_hook.Extension (optional event broadcaster)
  └── scope.FromContext    (extracts appID, tenantID, userID, IP)

audit_hook.Extension
  ├── audit_hook.Recorder  (external system adapter)
  └── action filter        (optional allowlist)

Installation

import (
    "github.com/xraph/vault/audit"
    audithook "github.com/xraph/vault/audit_hook"
)

audit.Entry

Each audit log entry captures the who, what, when, and outcome of an operation.

type Entry struct {
    ID        id.ID          `json:"id"`
    Action    string         `json:"action"`
    Resource  string         `json:"resource"`
    Key       string         `json:"key"`
    AppID     string         `json:"app_id"`
    TenantID  string         `json:"tenant_id,omitempty"`
    UserID    string         `json:"user_id,omitempty"`
    IP        string         `json:"ip,omitempty"`
    Outcome   string         `json:"outcome"`
    Metadata  map[string]any `json:"metadata,omitempty"`
    CreatedAt time.Time      `json:"created_at"`
}
FieldTypeDescription
IDid.IDUnique audit entry identifier (TypeID)
ActionstringThe action performed (see standard actions below)
ResourcestringThe resource type ("secret", "flag", "config", "override")
KeystringThe key of the resource being acted upon
AppIDstringApplication scope identifier
TenantIDstringTenant identifier (from context)
UserIDstringUser identifier (from context)
IPstringClient IP address (from context)
Outcomestring"success" or "failure"
Metadatamap[string]anyAdditional context (e.g., error messages on failure)
CreatedAttime.TimeUTC timestamp of the event

ListOpts

type ListOpts struct {
    Limit  int
    Offset int
}

audit.Store

The store provides append-only persistence for audit entries.

type Store interface {
    RecordAudit(ctx context.Context, e *Entry) error
    ListAudit(ctx context.Context, appID string, opts ListOpts) ([]*Entry, error)
    ListAuditByKey(ctx context.Context, key, appID string, opts ListOpts) ([]*Entry, error)
}
MethodDescription
RecordAuditPersists a single audit log entry
ListAuditReturns audit entries for an app
ListAuditByKeyReturns audit entries filtered by a specific key within an app

audit.Logger

The Logger records audit entries and optionally broadcasts them via an audit hook extension. It extracts scope information (appID, tenantID, userID, IP) from the context using the scope package.

Creating a Logger

logger := audit.NewLogger(auditStore,
    audit.WithHook(auditHookExtension),
    audit.WithLogger(slogLogger),
)

LoggerOption

OptionSignatureDescription
WithHookWithHook(h *audithook.Extension) LoggerOptionAttaches an audit hook extension for event broadcasting
WithLoggerWithLogger(sl *slog.Logger) LoggerOptionSets the slog logger for internal error reporting

LogAccess

Records a successful access event. Extracts scope from context and records both to the store and (if configured) to the hook.

func (l *Logger) LogAccess(ctx context.Context, key, action, resource string)
auditLogger.LogAccess(ctx, "db-password", audithook.ActionSecretAccessed, audithook.ResourceSecret)

The logger automatically:

  1. Extracts appID, tenantID, userID, and IP from the context via scope.FromContext.
  2. Creates an Entry with outcome "success".
  3. Persists it to the store.
  4. If a hook is attached, broadcasts an AuditEvent with severity "info" and outcome "success".

LogFailure

Records a failed access event. Same flow as LogAccess but with outcome "failure" and the error message captured in metadata.

func (l *Logger) LogFailure(ctx context.Context, key, action, resource string, err error)
auditLogger.LogFailure(ctx, "db-password", audithook.ActionSecretAccessed, audithook.ResourceSecret, err)

The error message is stored in Metadata["error"]. The hook event is broadcast with severity "warning" and outcome "failure".

audit_hook package

The audit_hook package provides the event types, constants, and the Extension that forwards audit events to external recording systems.

Standard actions

13 standard action constants cover all Vault operations:

ConstantValueCategory
ActionSecretAccessed"secret.accessed"Secret
ActionSecretSet"secret.set"Secret
ActionSecretDeleted"secret.deleted"Secret
ActionSecretRotated"secret.rotated"Secret
ActionFlagEvaluated"flag.evaluated"Flag
ActionFlagCreated"flag.created"Flag
ActionFlagUpdated"flag.updated"Flag
ActionFlagDeleted"flag.deleted"Flag
ActionFlagToggled"flag.toggled"Flag
ActionConfigSet"config.set"Config
ActionConfigDeleted"config.deleted"Config
ActionOverrideSet"override.set"Override
ActionOverrideDeleted"override.deleted"Override

Use audithook.AllActions() to get all action strings as a slice.

Categories

ConstantValue
CategorySecret"vault.secret"
CategoryFlag"vault.flag"
CategoryConfig"vault.config"
CategoryOverride"vault.override"

Resources

ConstantValue
ResourceSecret"secret"
ResourceFlag"flag"
ResourceConfig"config"
ResourceOverride"override"

Severity

ConstantValueUsed for
SeverityInfo"info"Successful operations
SeverityWarning"warning"Failed operations
SeverityCritical"critical"Critical security events

Outcome

ConstantValue
OutcomeSuccess"success"
OutcomeFailure"failure"

AuditEvent

The event struct emitted by the extension to the recorder:

type AuditEvent struct {
    Action     string         `json:"action"`
    Resource   string         `json:"resource"`
    Category   string         `json:"category"`
    ResourceID string         `json:"resource_id"`
    Key        string         `json:"key"`
    Metadata   map[string]any `json:"metadata,omitempty"`
    Outcome    string         `json:"outcome"`
    Severity   string         `json:"severity"`
    Reason     string         `json:"reason,omitempty"`
}
FieldTypeDescription
ActionstringOne of the 13 standard action constants
ResourcestringResource type string
CategorystringAudit category string
ResourceIDstringThe ID of the specific resource instance
KeystringThe key of the resource
Metadatamap[string]anyAdditional key-value context
Outcomestring"success" or "failure"
Severitystring"info", "warning", or "critical"
ReasonstringError message on failure

Recorder interface

Implement Recorder to forward events to your external audit system (e.g., a SIEM, logging service, or message queue).

type Recorder interface {
    Record(ctx context.Context, event *AuditEvent) error
}

RecorderFunc adapter

A convenience adapter for using plain functions as recorders:

type RecorderFunc func(ctx context.Context, event *AuditEvent) error
recorder := audithook.RecorderFunc(func(ctx context.Context, event *audithook.AuditEvent) error {
    log.Printf("[AUDIT] %s %s key=%s outcome=%s",
        event.Action, event.Resource, event.Key, event.Outcome)
    return nil
})

Extension

The Extension forwards audit events to a Recorder, with optional action filtering.

ext := audithook.New(recorder,
    audithook.WithActions(
        audithook.ActionSecretAccessed,
        audithook.ActionSecretSet,
        audithook.ActionSecretDeleted,
    ),
    audithook.WithLogger(logger),
)

Extension options

OptionSignatureDescription
WithActionsWithActions(actions ...string) OptionLimits recording to only the specified actions. If not set, all actions are recorded.
WithLoggerWithLogger(l *slog.Logger) OptionSets the logger for internal error reporting

Extension.Record

The Record method is called by the audit.Logger. It checks the action filter, builds an AuditEvent, and forwards it to the recorder.

func (e *Extension) Record(
    ctx context.Context,
    action, severity, outcome, resource, resourceID, category, key string,
    err error,
    kvPairs ...any,
)

The kvPairs parameter accepts alternating string key / any value pairs that are added to the event metadata.

Scope integration

The audit logger uses the scope package to extract request context:

import "github.com/xraph/vault/scope"

// Set up scope in middleware or request handler.
ctx = scope.WithScope(ctx, "myapp", "tenant-abc", "user-123", "192.168.1.1")

The logger then automatically extracts these values:

appID, tenantID, userID, ip := scope.FromContext(ctx)

Full example

package main

import (
    "context"
    "fmt"
    "log/slog"

    "github.com/xraph/vault/audit"
    audithook "github.com/xraph/vault/audit_hook"
    "github.com/xraph/vault/scope"
)

func main() {
    ctx := context.Background()

    // Set request scope.
    ctx = scope.WithScope(ctx, "myapp", "tenant-abc", "user-42", "10.0.0.1")

    // Create a recorder (sends to stdout for this example).
    recorder := audithook.RecorderFunc(func(ctx context.Context, event *audithook.AuditEvent) error {
        fmt.Printf("[%s] %s %s key=%s outcome=%s severity=%s\n",
            event.Category, event.Action, event.Resource,
            event.Key, event.Outcome, event.Severity)
        return nil
    })

    // Create the extension with action filter.
    ext := audithook.New(recorder,
        audithook.WithActions(
            audithook.ActionSecretAccessed,
            audithook.ActionSecretSet,
            audithook.ActionSecretDeleted,
            audithook.ActionSecretRotated,
        ),
        audithook.WithLogger(slog.Default()),
    )

    // Create the audit logger.
    logger := audit.NewLogger(auditStore,
        audit.WithHook(ext),
        audit.WithLogger(slog.Default()),
    )

    // Log a successful access.
    logger.LogAccess(ctx, "db-password", audithook.ActionSecretAccessed, audithook.ResourceSecret)
    // Output: [vault.secret] secret.accessed secret key=db-password outcome=success severity=info

    // Log a failure.
    logger.LogFailure(ctx, "db-password", audithook.ActionSecretAccessed, audithook.ResourceSecret,
        fmt.Errorf("decryption failed"))
    // Output: [vault.secret] secret.accessed secret key=db-password outcome=failure severity=warning

    // Query audit history.
    entries, _ := auditStore.ListAuditByKey(ctx, "db-password", "myapp", audit.ListOpts{Limit: 50})
    for _, e := range entries {
        fmt.Printf("  %s %s outcome=%s user=%s at=%s\n",
            e.Action, e.Key, e.Outcome, e.UserID, e.CreatedAt)
    }
}

On this page