Middleware & Auth

Audit Logging

New to acton-service?

Start with the homepage to understand what acton-service is, then explore Core Concepts for foundational explanations. See the Glossary for technical term definitions.


Tamper-evident audit logging with BLAKE3 hash chaining, automatic auth event capture, and SIEM export via Syslog RFC 5424 and OpenTelemetry Logs.

Overview

acton-service provides immutable audit trails for security compliance. Every audit event is sealed into a BLAKE3 hash chain, guaranteeing tamper detection. Events are processed sequentially by an internal actor, persisted to your configured database backend with append-only enforcement, and exported to SIEM systems via Syslog or OTLP.

Actor-Based Processing

Audit events are processed by an AuditAgent (acton-reactive actor) that owns the hash chain state and guarantees correct ordering. Events are sent via fire-and-forget message passing, so audit logging never blocks request handling. See Reactive Architecture for details.

How It Works

HTTP Request --> AuditMiddleware --> ActorHandle::send() --> AuditAgent --+--> DB (append-only)
Auth Events --> AuditLogger.log() -------------------------+             +--> Syslog RFC 5424
                                                                         +--> OTLP Logs (optional)

Feature Interactions

Features EnabledBehavior
audit aloneIn-memory hash chain + syslog export
audit + database/turso/surrealdbPersistent append-only storage
audit + observabilityOTLP log export via tracing
audit + token auth (PASETO/JWT)Automatic auth event emission

Installation

Enable the audit feature:

[dependencies]

With a database backend for persistent storage:

[dependencies]

Configuration

Add an [audit] section to your config.toml:

[audit]
enabled = true
audit_all_requests = false        # Audit every HTTP request
audit_auth_events = true          # Auto-audit auth events (login, logout, etc.)
otlp_logs_enabled = false         # Export via OTLP (requires observability feature)
audited_routes = ["/api/v1/admin/*"]   # Glob patterns for per-route auditing
excluded_routes = ["/health", "/ready", "/metrics"]

[audit.syslog]
transport = "udp"                 # "udp", "tcp", or "none"
address = "127.0.0.1:514"
facility = 13                     # 13 = audit (RFC 5424)
# app_name = "my-service"         # Defaults to service.name

Environment Variable Override

ACTON_AUDIT_ENABLED=true
ACTON_AUDIT_AUDIT_ALL_REQUESTS=true
ACTON_AUDIT_SYSLOG_TRANSPORT=tcp
ACTON_AUDIT_SYSLOG_ADDRESS=syslog.example.com:514

Configuration Options

  • enabled: Enable or disable audit logging globally (default: true)
  • audit_all_requests: Log every HTTP request as an audit event (default: false)
  • audit_auth_events: Automatically emit events for auth actions (default: true)
  • otlp_logs_enabled: Export audit events via OpenTelemetry Logs (default: false, requires observability feature)
  • audited_routes: Glob patterns for routes to audit (e.g., "/api/v1/admin/*")
  • excluded_routes: Routes to never audit, even when audit_all_requests is true (default: ["/health", "/ready"])

Basic Usage

The audit logger is available via AppState when the audit feature is enabled:

use acton_service::prelude::*;

async fn delete_user(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Result<Json<()>> {
    // Perform the deletion
    // ...

    // Log a custom audit event
    if let Some(logger) = state.audit_logger() {
        logger.log_custom(
            "user.delete",
            AuditSeverity::Warning,
            Some(serde_json::json!({ "user_id": id })),
        ).await;
    }

    Ok(Json(()))
}

Auth Events (Automatic)

When audit_auth_events is enabled (default), the PASETO and JWT middleware automatically emit audit events:

Event KindWhen EmittedSeverity
AuthLoginSuccessToken validated successfullyInformational
AuthLoginFailedToken missing, invalid, or expiredWarning
AuthTokenRevokedRevoked token presentedWarning

No additional code is required. These events include the client IP, user agent, and authenticated subject.

Per-Route Auditing

Mark specific routes for auditing with custom event names:

use acton_service::prelude::*;

let routes = VersionedApiBuilder::new()
    .with_base_path("/api")
    .add_version(ApiVersion::V1, |router| {
        router
            // These routes are audited with custom event names
            .route("/admin/users/:id", delete(delete_user)
                .layer(Extension(AuditRoute::new("user.delete"))))
            .route("/admin/settings", put(update_settings)
                .layer(Extension(AuditRoute::new("settings.update"))))
            // This route is NOT audited (unless audit_all_requests is true)
            .route("/users", get(list_users))
    })
    .build_routes();

Routes annotated with AuditRoute are always audited, regardless of the audited_routes config patterns.

Route Pattern Matching

The audited_routes config supports simple glob patterns:

[audit]
audited_routes = [
    "/api/v1/admin/*",        # Any single segment under /admin/
    "/api/v1/admin/**",       # Any path starting with /admin/
    "/api/v1/users/*/delete", # DELETE-like paths with any user ID
]

Hash Chain Integrity

Every audit event is sealed into a BLAKE3 hash chain. Each event's hash covers:

  • Sequence number (monotonically increasing)
  • Previous event's hash (chain linkage)
  • Event ID, timestamp, kind, severity
  • Service name, HTTP method, path, status code
  • Authenticated subject (if present)

Verifying the Chain

use acton_service::audit::{verify_chain, AuditEvent};

// Fetch events from storage
let events: Vec<AuditEvent> = storage.query_range(from, to, 1000).await?;

// Verify the hash chain is intact
match verify_chain(&events) {
    Ok(()) => println!("Chain integrity verified"),
    Err(e) => eprintln!("Tamper detected: {}", e),
}

The chain detects:

  • Modified events: Hash won't match recalculated value
  • Deleted events: Sequence gaps or broken chain links
  • Reordered events: Previous hash won't match prior event
  • Inserted events: Chain linkage will be broken

Database Storage

When a database feature is enabled alongside audit, events are persisted with append-only enforcement:

PostgreSQL

[dependencies]
acton-service = { version = "0.11.0", features = ["audit", "database"] }

Uses CREATE RULE to prevent updates and deletes on the audit_events table.

Turso (libsql)

[dependencies]
acton-service = { version = "0.11.0", features = ["audit", "turso"] }

Uses CREATE TRIGGER ... RAISE(ABORT) to enforce immutability.

SurrealDB

[dependencies]
acton-service = { version = "0.11.0", features = ["audit", "surrealdb"] }

Uses PERMISSIONS FOR update, delete NONE on the audit table.

Append-Only Enforcement

The storage implementations create database rules/triggers that prevent any modification or deletion of audit records. This is enforced at the database level, not just in application code. A database administrator with direct schema access could still remove these protections, so protect your database credentials accordingly.

Syslog Export (RFC 5424)

Audit events are automatically formatted as RFC 5424 syslog messages and sent via UDP or TCP:

<109>1 2026-01-15T10:30:00.000Z my-service acton-audit - - [audit@0 event_id="..." kind="AuthLoginSuccess" severity="Informational" sequence="42" hash="abc123..."] User login successful

Syslog Configuration

[audit.syslog]
transport = "udp"              # "udp", "tcp", or "none" to disable
address = "127.0.0.1:514"     # Syslog server address
facility = 13                  # RFC 5424 facility (13 = audit)
app_name = "my-service"       # Override app name (defaults to service.name)

Integration with SIEM Systems

Configure your SIEM to receive RFC 5424 messages:

  • Splunk: Configure a UDP/TCP input on port 514, parse structured data from the [audit@0] SD-ELEMENT
  • Elastic/ELK: Use Filebeat's syslog input or Logstash's syslog filter
  • Datadog: Configure a syslog source via the Datadog Agent
  • Graylog: Add a Syslog UDP/TCP input

OTLP Export

When the observability feature is also enabled, audit events are exported as structured OpenTelemetry log records:

[dependencies]
acton-service = { version = "0.11.0", features = ["audit", "observability"] }
[audit]
otlp_logs_enabled = true

[otlp]
endpoint = "http://otel-collector:4317"

Events are emitted via tracing::info! with structured fields:

audit.event_id = "550e8400-..."
audit.kind = "AuthLoginSuccess"
audit.severity = "Informational"
audit.sequence = 42
audit.hash = "abc123..."
audit.service = "my-service"

These fields are automatically picked up by any OpenTelemetry-compatible collector.

Custom Audit Events

Emit custom events from anywhere in your application:

use acton_service::prelude::*;

async fn process_payment(
    State(state): State<AppState>,
    Json(payment): Json<PaymentRequest>,
) -> Result<Json<PaymentResponse>> {
    // Process payment...
    let result = charge_card(&payment).await?;

    // Log audit event with metadata
    if let Some(logger) = state.audit_logger() {
        logger.log_custom(
            "payment.processed",
            AuditSeverity::Informational,
            Some(serde_json::json!({
                "amount": payment.amount,
                "currency": payment.currency,
                "payment_id": result.id,
            })),
        ).await;
    }

    Ok(Json(result))
}

Event Kinds

Built-in event kinds for common operations:

KindDescription
AuthLoginSuccessSuccessful authentication
AuthLoginFailedFailed authentication attempt
AuthLogoutUser logout
AuthTokenRefreshToken refresh
AuthTokenRevokedRevoked token used
AuthPasswordChangedPassword change
AuthApiKeyCreatedAPI key created
AuthApiKeyRevokedAPI key revoked
AuthOAuthCallbackOAuth callback processed
AuthPermissionDeniedAuthorization denied
HttpRequestHTTP request (from middleware)
HttpRequestDeniedDenied HTTP request
Custom(String)Custom application event

Severity Levels

Severity levels map to RFC 5424 syslog severity values:

SeveritySyslog ValueUsage
Emergency0System is unusable
Alert1Immediate action required
Critical2Critical conditions
Error3Error conditions
Warning4Warning conditions
Notice5Normal but significant
Informational6Informational messages
Debug7Debug-level messages

Best Practices

DO:

  • ✅ Enable audit_auth_events in production (it's on by default)
  • ✅ Use per-route AuditRoute annotations for sensitive operations
  • ✅ Configure syslog export to a SIEM for real-time monitoring
  • ✅ Periodically verify hash chain integrity
  • ✅ Use a persistent database backend for compliance requirements
  • ✅ Exclude health check routes from auditing

DON'T:

  • ❌ Enable audit_all_requests without considering volume (can be noisy)
  • ❌ Store audit events only in-memory for production (no persistence)
  • ❌ Disable database-level append-only protections
  • ❌ Log sensitive data (passwords, tokens) in audit event metadata

Production Deployment

[audit]
enabled = true
audit_all_requests = false
audit_auth_events = true
otlp_logs_enabled = true
audited_routes = ["/api/v1/admin/*", "/api/v1/billing/*"]
excluded_routes = ["/health", "/ready", "/metrics"]

[audit.syslog]
transport = "tcp"                 # TCP for reliable delivery
address = "syslog.internal:514"
facility = 13

Kubernetes Integration

env:
  - name: ACTON_AUDIT_ENABLED
    value: "true"
  - name: ACTON_AUDIT_SYSLOG_TRANSPORT
    value: "tcp"
  - name: ACTON_AUDIT_SYSLOG_ADDRESS
    value: "syslog-service.logging.svc.cluster.local:514"

Compliance Considerations

The audit system supports common compliance frameworks:

  • SOC 2: Immutable audit trails with hash chain verification
  • PCI DSS: Logging of authentication events and access to sensitive data
  • HIPAA: Audit controls for access to protected health information
  • GDPR: Record of processing activities with tamper detection

Ensure your syslog/SIEM retention policies match your compliance requirements.

Previous
Resilience Patterns