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.
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-IDheader 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/
Related Documentation
- Plans and Entitlements — Audit logging availability by plan
- RBAC — Role-based access control (role changes are audit logged)
- OIDC / SSO — SSO login events are audit logged
- Security Policy — Core security model
- Multi-Tenant Organizations — Per-tenant audit isolation
- API Reference — Audit log API endpoints