Matrix: M12M13
Open in panel — /app/audit
Reporting

Audit Event Log

PostMTA maintains an immutable, append-only audit log for every security-relevant action that occurs across the platform. The log captures 25+ event types spanning send operations, authentication events, configuration changes, and administrative actions. All entries include operator identity, client IP address, and timestamp with nanosecond precision.

Matrix refs: M12 (Audit Trail)   M13 (Immutable Log)

Append-Only Guarantee (PMH-SEC-012)

The audit log is designed to satisfy the immutable log requirement defined in PMH-SEC-012. Entries are written to a write-ahead log (WAL) segment that is replicated across three availability zones before acknowledgement. Once written, entries can never be modified or deleted — not even by a platform administrator with full database privileges.

Immutability is enforced at the storage layer:

PMH-SEC-012 requirement: Audit log entries MUST be retained for a minimum of 2 years. Entries older than 2 years may be migrated to cold storage (S3 Glacier) but remain queryable via the export API within 24 hours of a retrieval request.

Event Types

The following table lists all currently instrumented event types. Each entry shows the trigger condition and the metadata fields included in the metadata JSON column.

+-------------------+----------------------------------------+------------------------------------+
| Event Type        | Trigger                                | Metadata Fields                    |
+-------------------+----------------------------------------+------------------------------------+
| send              | Message submitted to transport          | msg_id, subuser_id, from, to,     |
|                   |                                        | ip_pool_id, template_id            |
| domain_verify     | DNS verification check completed       | domain, verification_method,       |
|                   |                                        | record_type, record_value          |
| api_key_create    | New API key generated                  | key_id, key_name, scopes[],        |
|                   |                                        | created_by, expires_at             |
| api_key_rotate    | API key secret rotated                 | key_id, rotated_by                 |
| api_key_revoke    | API key revoked                        | key_id, revoked_by                 |
| webhook_create    | Webhook endpoint registered            | webhook_id, url, events[],         |
|                   |                                        | created_by                         |
| webhook_delete    | Webhook endpoint removed               | webhook_id, deleted_by             |
| shaping_apply     | Shaping policy applied to subuser     | subuser_id, policy, applied_by     |
| shaping_override  | Manual epoch override issued           | subuser_id, from_epoch, to_epoch,  |
|                   |                                        | reason, applied_by                 |
| fbl_complaint     | ARF complaint processed                | recipient, source_isp,             |
|                   |                                        | feedback_type, complaint_id        |
| fbl_fraud         | ARF fraud/phishing report received     | recipient, source_isp, complaint_id |
| suppress_add      | Address added to suppression list       | recipient, subuser_id, reason,      |
|                   |                                        | ttl_days, added_by                 |
| suppress_remove   | Address removed from suppression list  | recipient, removed_by               |
| subuser_create    | Subuser account created                | subuser_id, plan, created_by       |
| subuser_suspend   | Subuser account suspended              | subuser_id, reason, suspended_by   |
| subuser_delete    | Subuser account deleted                | subuser_id, deleted_by             |
| billing_alert     | Credit threshold crossed               | subuser_id, credits_remaining,     |
|                   |                                        | threshold_type                     |
| mfa_enabled       | MFA enabled on account                 | user_id, mfa_method (totp/webauthn)|
| mfa_disabled      | MFA disabled on account                 | user_id, disabled_by               |
| export_request    | Data export initiated                  | export_id, export_type,            |
|                   |                                        | requested_by, record_count         |
| login_success     | Successful authentication              | user_id, ip_address, user_agent,   |
|                   |                                        | auth_method                        |
| login_failure     | Failed authentication attempt          | attempted_user, ip_address,        |
|                   |                                        | reason, user_agent                 |
| permission_denied | Action blocked by RBAC                | user_id, action, resource,         |
|                   |                                        | required_scope                     |
+-------------------+----------------------------------------+------------------------------------+

Schema

The audit_events table schema:

ColumnTypeDescription
event_idUUIDUnique event identifier (ksuid-based)
event_typevarchar(64)Event type name (see table above)
occurred_attimestamptzWall-clock time of the event (UTC, nanosecond precision)
actor_idUUIDUser ID of the principal that triggered the event. NULL for system-generated events.
actor_typeenumOne of: user, subuser, system, api_key, webhook
ip_addressinetClient IP address (IPv4 or IPv6). NULL for internal system events.
user_agentvarchar(512)Client User-Agent string for API and web events
resource_typevarchar(64)Type of resource affected (message, subuser, api_key, domain, etc.)
resource_idvarchar(128)ID of the affected resource
metadatajsonbEvent-specific key-value pairs (schema varies by event_type)
cmac_hashtextCMAC-SHA256 of the log batch segment (for tamper evidence)

Actor Resolution via SQL

The actor_id in audit events is a raw UUID. To resolve it to a human-readable identity for investigation, join against the users and subusers tables:

SELECT
  a.event_id,
  a.event_type,
  a.occurred_at,
  a.metadata,
  u.user_id,
  u.email     AS actor_email,
  u.full_name AS actor_name,
  COALESCE(s.subuser_id, a.metadata->>'subuser_id') AS affected_subuser
FROM audit_events a
LEFT JOIN users u ON a.actor_id = u.user_id
LEFT JOIN subusers s
  ON s.subuser_id = a.metadata->>'subuser_id'
WHERE a.event_type IN ('send', 'shaping_override', 'suppress_add',
                       'api_key_create', 'mfa_enabled')
  AND a.occurred_at >= NOW() - INTERVAL '30 days'
ORDER BY a.occurred_at DESC
LIMIT 100;

IP Logging

Client IP addresses are captured for all events initiated by a human actor or an API key. The following rules apply:

IPv6: IPv6 addresses are stored in the inet column as-is. Queries using inet_not_in() or inet_contains() operate correctly on both IPv4 and IPv6 values.

Retention Policy

Storage TierDurationQueryable via APICost
Hot (PostgreSQL / read replica)0 – 90 daysYes, < 5 s responseStandard
Warm (S3 Standard)90 days – 2 yearsYes, via export API, up to 24 h latencyReduced
Cold (S3 Glacier)2 years – 7 yearsYes, via export API, up to 12 h retrievalMinimal
Archived> 7 yearsManual request onlyCompliance tier

Compliance-tier archives are accessible only via a written request approved by the Data Protection Officer (DPO) and require a 30-day notice period. Archive retrieval is logged in the audit log itself (event type archive_retrieval).

Admin Export API

Account administrators can export audit events via the REST API. Exports are returned as gzip-compressed NDJSON and are available for download for 24 hours after generation. The export API supports filtering by event type, date range, subuser, and actor.

POST /v1/audit/export
Content-Type: application/json
Authorization: Bearer pmta_live_...

{
  "start_time": "2026-06-01T00:00:00Z",
  "end_time":   "2026-07-18T23:59:59Z",
  "event_types": [
    "send", "shaping_override", "api_key_create",
    "fbl_complaint", "mfa_enabled", "permission_denied"
  ],
  "subuser_id": "sub_usr_01HX...",
  "format": "ndjson",
  "compression": "gzip"
}

-- Response:
{
  "export_id":  "aud_exp_01HX9P7N3...",
  "status":     "processing",
  "record_count": null,
  "download_url": null,
  "expires_at":   "2026-07-19T12:00:00Z"
}

-- Poll for completion:
GET /v1/audit/export/aud_exp_01HX9P7N3...
Authorization: Bearer pmta_live_...

{
  "export_id":  "aud_exp_01HX9P7N3...",
  "status":     "ready",
  "record_count": 48291,
  "download_url": "https://exports.postmta.com/aud_exp_01HX...gz",
  "expires_at":   "2026-07-19T12:00:00Z",
  "checksum_sha256": "a3f5c8..."
}
Export permissions: The audit:export scope is required. This scope is granted only to roles with admin or security_admin. Export requests are themselves logged in the audit log (event type export_request).

Searching the Audit Log

Filter by Event Type

GET /v1/audit/events?event_type=shaping_override&start=2026-07-01T00:00:00Z
Authorization: Bearer pmta_live_...

Filter by Resource

GET /v1/audit/events?resource_type=subuser&resource_id=sub_usr_01HX...
Authorization: Bearer pmta_live_...

Filter by Actor

GET /v1/audit/events?actor_id=usr_01HX9P7N3...&limit=50
Authorization: Bearer pmta_live_...

Technical Immutability Controls

Beyond the storage-layer controls described above, PostMTA enforces immutability through several additional mechanisms:

Partitioning Strategy

The audit_events table is partitioned by month using PostgreSQL declarative range partitioning. Old partitions are detached and archived to S3 without affecting query performance on recent data. Queries spanning multiple months are automatically routed to the relevant partitions.

-- Example: list active partitions
SELECT
  relname                  AS partition_name,
  pg_size_pretty(pg_relation_size(relid)) AS size
FROM pg_stat_user_tables
WHERE relname LIKE 'audit_events_%'
ORDER BY relname;

Cross-Account Audit Access

For enterprise accounts with multiple subaccounts (multi-tenant setups), the audit log supports cross-account queries for users with the security_admin role. Cross-account queries use the all_subaccounts=true filter:

GET /v1/audit/events?all_subaccounts=true&event_type=permission_denied&start=2026-07-01T00:00:00Z
Authorization: Bearer pmta_live_...

{
  "events": [...],
  "meta": {
    "total_matched": 142,
    "subaccounts_queried": ["sub_usr_01HX...", "sub_usr_02HY..."],
    "query_time_ms": 387
  }
}
Cross-account audit access: Only security_admin role holders may query across subaccounts. Standard admin and operator roles are limited to their own subaccount's events.

Security Alert Events

The following event types should trigger immediate security review when observed in the audit log:

Audit Dashboard

The Reporting → Audit Log panel in the PostMTA dashboard provides a searchable, filterable UI for the last 90 days of audit events. Operators can: