Files
pulse/docs/AUDIT_LOGGING.md
T
rcourtman 8a99b16d6c docs: correct customer docs that drifted from the source
Audited the customer-facing docs against the code: about 5,200 concrete
claims (env vars, endpoints, flags, defaults, ports, UI labels), plus every
environment variable the code reads. Each discrepancy was re-verified
against the source before editing, and every edit was reviewed again.

What users would have hit following the old text:
- UI paths and labels that no longer exist (Settings > Relay, Platform
  connections, Access Control, SSO Providers, Create Organization) and
  Patrol mode names (now Ask first, Safe auto-fix, Autopilot).
- Wrong values and behaviour: LOG_MAX_AGE=0 does not disable cleanup,
  Patrol evidence budget is 10 calls and 12 turns, ALLOW_UNPROTECTED_EXPORT
  covers private networks too, PULSE_AUDIT_ASYNC defaults on.
- Agent install: install.sh ignores PULSE_ENABLE_KUBERNETES and
  PULSE_ENABLE_PROXMOX, rejects --disable-auto-update, and the token
  fallback lives in the state directory.
- API: audit endpoints answer 402 on Community, onboarding QR is GET,
  type=container is rejected, several routes need admin plus a scope.
- HTTP_REDIRECT_PORT was not documented anywhere.

Docs are mirrored under frontend-modern/public/docs, so each change is
applied to both copies. The tests that pinned the old wording (including
three that asserted claims the code contradicts) are updated alongside.

Contract-Neutral: Customer docs corrected to match shipped behaviour; no runtime, API or contract change.
2026-10-11 13:59:56 +01:00

15 KiB

Audit Logging

Pulse's audit log records security-relevant events with tamper-evident signatures. Use it for compliance, incident investigation, and tracking who did what.

Requires: Pro, legacy Pro+, or Cloud license with the audit_logging capability to query, export, and verify events via the API. Events are recorded on all plans, but the API endpoints are license-gated.

An active licence alone does not enable the audit UI/API on the public community runtime. If the panel says Pulse Pro runtime required, follow Download Pulse Pro to the private runtime for your deployment. Do not buy a second licence or reset the data directory to clear this gate.

For plan details, see PULSE_PRO.md. For API endpoints, see API Reference.


What Gets Logged

Pulse automatically captures the following events:

Event Type Description Example
login Successful and failed login attempts User admin logged in from 198.51.100.5
logout User logouts User admin logged out
password_change Password modifications Password changed (Docker/systemd)
csrf_failure Blocked cross-site request forgery attempts Invalid CSRF token
lockout_reset Account lockout resets Admin reset lockout for user bob
sso_oidc_login OIDC SSO login attempts (success/failure at each stage) OIDC login success
oidc_token_refresh OIDC token refresh success/failure (global, not tenant-scoped) Token refreshed successfully
sso_oidc_role_assignment Automatic role assignment from OIDC groups Auto-assigned roles: operator, viewer
saml_login SAML SSO login attempts SAML login success via provider-id
saml_role_assignment Automatic role assignment from SAML groups Auto-assigned roles: admin
sso_provider_created SSO provider configuration created Created provider: Authentik
sso_provider_updated SSO provider configuration modified Updated provider: Authentik
sso_provider_deleted SSO provider configuration removed Deleted provider: Authentik
ai_settings_updated AI configuration changes AI settings updated
agent_profile_assigned Agent profile assignments Profile production assigned to agent
agent_profile_unassigned Agent profile removals Profile removed from agent
user_roles_updated RBAC role assignments changed Updated roles for user jane: [operator]
agent_config_fetch Every failed agent configuration fetch. A successful fetch is normally recorded on the agent's first delivery after Pulse starts, when its token or delivered configuration changes, and otherwise once a day; unchanged minute polls are suppressed while the agent is remembered (see below) agent_id=… token_id=… config=sha256:… reason=config_changed

Config-fetch suppression remembers up to 4,096 organisation/agent pairs per Pulse process. New agents can replace entries last recorded at least 24 hours ago; a forgotten agent's next successful fetch is recorded again. If all slots are recent, additional agents' successful fetches are recorded on every poll with reason=capacity until a slot expires. This keeps memory bounded without hiding access events or displacing recent agents' suppression. It does not limit enrolment or configuration delivery, and every failed fetch is still recorded. No existing audit rows are removed by this tracker.

Each event includes:

  • Timestamp (UTC)
  • Event type
  • User who triggered the event
  • Client IP address
  • Request path
  • Success/failure flag
  • Details (human-readable description)
  • Cryptographic signature (tamper detection)

Viewing Audit Events

UI

Settings → Security → Audit Log

The audit log panel shows events in reverse chronological order with filtering by event type, user, date range, and success/failure.

API

Use a token with audit:read, bound to a user permitted to read audit logs, and the licensed audit_logging capability. Prepare the private header file in API authentication; never paste the token or a session cookie into a command. For a one-off read, you can instead open the API path in your signed-in Pulse browser.

Define the pulse_api helper from API authentication in the same Bash session, using your audit:read header file. It is local example code, not an installed Pulse command. The helper requires curl 7.76 or later and makes one request with five-second connection and twenty-second whole request limits. On the Pulse host it uses loopback; for remote access, follow that guide's HTTPS-origin and certificate verification instructions.

Each call prints only the HTTP status and a new private response file's location, including on HTTP failure. Open the file locally, not in a shared terminal or recording: events, summaries and errors can contain usernames, client addresses, paths and other identifying details. Keep whole responses and the header file private; share only a relevant manually redacted excerpt.

Run only the read relevant to your investigation:

# List a small page of recent events
pulse_api GET '/api/audit?limit=50'

# Filter by event type and date range
pulse_api GET '/api/audit?limit=50&event=login&startTime=2026-01-01T00:00:00Z&endTime=2026-01-31T23:59:59Z&success=false'

# Get audit summary
pulse_api GET /api/audit/summary

HTTP 401, 402, 403, other errors and redirects are not an empty audit history. The helper does not follow redirects or retry. A transport failure can leave a partial file, not a complete result; preserve the status and original evidence rather than looping, resetting the store or changing a licence to make it pass.

Query Parameters

Parameter Type Description
limit positive integer List page size (default: 100, capped at 1,000)
offset non-negative integer List page offset (default: 0)
event string Filter by event type (e.g., login, password_change)
user string Filter by username
success boolean Filter by success (true) or failure (false)
startTime RFC3339 Start of date range, with timezone
endTime RFC3339 End of date range, strictly after startTime

These parameters describe the event list, not every audit endpoint. Its response contains an events page and total matching events; fewer rows than total do not establish missing history. Keep the same filters when reading another page. New events can shift offsets; a sequence of pages is not a consistent archival snapshot. Malformed list filters return HTTP 400, not an empty result. URL-encode query values; do not put private usernames or other identifiers into a shared command or report.


Exporting Audit Data

Use the same helper to save an export privately. For an investigation, prefer the relevant event type and known time range over an unfiltered whole-history export; replace the example dates with your incident's UTC bounds:

pulse_api GET '/api/audit/export?event=login&startTime=2026-01-01T00:00:00Z&endTime=2026-01-31T23:59:59Z'

The new owner-only response file is created in the helper's private directory. It does not reuse filters selected in the UI. Export accepts event, user, success, startTime and endTime, but not list pagination: adding limit or offset does not bound an export. With no filters it requests the full available history. List, export and summary reject malformed or empty supplied startTime/endTime values and ranges whose end is not after their start with HTTP 400, before reading events. List and export also reject a supplied success other than exactly true or false, including an empty value. Omit a filter to leave it unset; HTTP 200 alone does not establish complete archival coverage.

Summary accepts event, user, startTime and endTime, not success or list pagination. These unsupported parameters do not narrow a summary; omit them rather than relying on them to bound the read.

Keep the export outside shared repositories and issue attachments. A failed or incomplete download is not a valid archive. Do not widen or repeat a failed export just to fill a report; existing evidence is valid. Dispose of private response files under your normal data-retention policy.


Tamper Detection

When signing is available, Pulse signs audit events at creation time. Events captured without a signer remain unsigned. Use Verify in the signed-in Audit Log panel, or the authenticated read below, to check a stored signature:

pulse_api GET /api/audit/6b3c9c3c-9a2f-4b3c-9a3b-3d0e8c5c5d45/verify

Open the saved response privately; a successful verification result has this shape (HTTP 200 alone is not signature verification):

{
  "available": true,
  "verified": true,
  "message": "Event signature verified"
}

verified: true means the signature matches the verifier's key and supported signature format. It does not prove that the history is complete or that no events were deleted. verified: false means the check failed, not that Pulse has established why it failed. Keep that result as evidence.

Verification failures and safe recovery

Result What it establishes Safe next step
Not checked No verification result has been obtained. Use Verify for the relevant event.
Unsigned No signature was stored. Check whether signing was available when the event was captured. Later enabling signing cannot authenticate it retroactively.
Failed / verified: false The stored event and signature do not verify with the current key and supported format. Preserve the evidence; a different key, missing or damaged signature, unsupported format or modified signed fields can cause this.
Unavailable / available: false The current logger cannot offer verification. Check the audit capability/runtime gate and bounded startup logs for persistent-store initialisation failures.
Error / non-successful HTTP response The verification request did not return a usable result. Keep the HTTP status and redacted error; it is not proof that the event failed verification.

Start with the event's time, the last known working version and whether the data mount, service account, runtime or backup changed. Inspect a bounded startup log excerpt locally; distinguish encryption/signing initialisation errors from an audit database/query error. Do not print the key files or post whole audit exports, service environments or backups. For an empty panel, clear filters and check the selected organisation before assuming events were lost. See Getting Help for safe evidence.

Preserve before changing anything. Keep a consistent private backup of the current data directory, including the audit store and its signing and encryption keys. Use your established snapshot/backup procedure for a running instance; copying a live .db file alone is not a consistent backup. For an offline filesystem backup, stop Pulse first and retain the complete store, including any SQLite sidecar files, with its original ownership and access.

Do not delete or regenerate a key, replace a live signing key with an older one, edit audit rows or re-sign old events to make verification pass. A newly generated signing key does not verify events signed with the previous key. The encrypted signing key also needs its matching .encryption.key; restoring only one member is not a recovery. Swapping keys can break verification of newer events while concealing the original problem.

If a matching historical backup exists, restore and compare it in an isolated test instance using the corresponding Pulse runtime/version, without replacing the live data or contacting monitored systems and notification destinations. Keep the current instance and both backups intact. If the original key is lost or an event was unsigned, do not claim that a restart, new key or successful check on a different event authenticated that history.


Multi-Tenant Audit Isolation

In multi-tenant deployments, most events are scoped to the active organization:

  • Tenant-aware events (logins, role changes, config updates) are stored per-organization.
  • Some auth lifecycle events (e.g., oidc_token_refresh) are global and not tenant-scoped.
  • Switching organizations shows only that organization's tenant-scoped events.
  • The tenant context is determined by X-Pulse-Org-ID header or session cookie.

See Multi-Tenant Organizations for details.


Community vs Pro Behavior

Capability Community Pro / legacy Pro+ / Cloud
Events captured Yes Yes
Persistent storage (SQLite) Yes Yes
Query/filter API License-gated (402) Full access
Signature verification License-gated (402) Available
Export License-gated (402) Available
persistentLogging API flag false true

Pulse attempts SQLite-backed capture on all plans; failure to initialise the store can leave console-only logging. The query, verify and export endpoints also require the audit_logging capability and a compatible runtime. A licence/runtime gate is not proof that events were never captured. For an authorised query, persistentLogging describes the active logger, not a count of historical events or proof that their signatures are valid.


Storage

The default audit store lives in the Pulse data directory:

  • Single-tenant, and the default organisation in multi-tenant installs: {data-dir}/audit/audit.db
  • Non-default organisations (multi-tenant): {data-dir}/orgs/{org-id}/audit/audit.db

The default encrypted signing key is .audit-signing.key inside the audit store directory, alongside audit.db, not at the data-directory root. Its matching .encryption.key is in the data directory (or the organisation's data directory for a non-default organisation). Keep both with the corresponding history. A runtime may supply a different audit directory or managed signing key; use that deployment's actual configuration rather than guessing a path.

Data directory locations:

  • systemd: /etc/pulse/
  • Docker/Kubernetes: /data/
  • Development: tmp/dev-config/