Audit Log
The audit log records sensitive operations as JSON Lines, one event per line, append-only. Configure the path via audit.file in horizon.yaml (see Setup → audit).
Event schema
Source: apps/bff/src/audit/logger.ts.
interface AuditEvent {
ts: string; // ISO-8601 timestamp
actor: string | null; // username; null for system events
action: string; // e.g. 'auth.login', 'rule.addOrUpdate'
verb?: string; // RBAC verb checked, if any
target?: string; // resource id / name
outcome: string; // 'success' | 'failure' | 'break-glass' | HTTP status | OAP status
details?: Record<string, unknown>; // free-form context
fromIp?: string; // requester IP
sessionId?: string;
}
One event per line, \n-terminated. Use jq -c to filter:
tail -f horizon-audit.jsonl | jq -c 'select(.action | startswith("auth."))'
Recorded actions
The recorded set evolves with the codebase. As of the current build:
| Action | Outcome values | Notes |
|---|---|---|
auth.login |
success, failure |
Standard login. details.backend is local or ldap. On success details.roles carries the resolved role list. |
auth.login.break-glass |
break-glass |
Emergency admin login. Logged in addition to a WARN application log line. |
auth.logout |
success |
Explicit logout (cookie cleared). Sessions that simply expire are not logged. |
rule.addOrUpdate |
success, HTTP status on failure |
DSL Management create / update. target is the rule name; details carries the diff summary. |
rule.delete |
success, HTTP status on failure |
DSL Management delete. |
alarm-rule.addOrUpdate |
success, HTTP status on failure |
Alarm Rule editor write. |
setup.write |
success, HTTP status on failure |
Per-user setup state write. |
overview-template.write |
success |
Overview template admin edits. |
layer-template.write |
success |
Layer template admin edits. |
outcome is the literal string for normal flows (success, failure, break-glass) and a stringified HTTP / OAP status code when the underlying call failed. This makes audit-time error correlation straightforward: an entry with outcome: "503" tells you OAP returned a server error.
Example entries
Successful local login
{
"ts": "2026-05-18T09:14:02.118Z",
"actor": "alice",
"action": "auth.login",
"outcome": "success",
"fromIp": "10.0.5.12",
"sessionId": "k7r...",
"details": { "backend": "local", "roles": ["operator"] }
}
Failed LDAP login
{
"ts": "2026-05-18T09:14:08.221Z",
"actor": "alice",
"action": "auth.login",
"outcome": "failure",
"fromIp": "10.0.5.12",
"details": { "backend": "ldap" }
}
(No sessionId — no session was created.)
Break-glass login
{
"ts": "2026-05-18T14:29:33.456Z",
"actor": "emergency-admin",
"action": "auth.login.break-glass",
"outcome": "break-glass",
"fromIp": "192.0.2.10",
"sessionId": "z3a...",
"details": { "backend": "ldap" }
}
Rule write
{
"ts": "2026-05-18T15:02:11.004Z",
"actor": "alice",
"action": "rule.addOrUpdate",
"verb": "rule:write",
"target": "service_resp_time_rule",
"outcome": "success",
"fromIp": "10.0.5.12",
"sessionId": "k7r..."
}
File format
- JSON Lines. One JSON object per line,
\n-terminated. - Append-only. The BFF opens the file in append mode and never truncates / rotates.
- No rotation built in. Pair with
logrotate,vector,fluent-bit, or a sidecar shipper.
Storage placement
- Durable storage required. Break-glass logins, rule edits, and setup changes should outlive the container.
- Filesystem perms matter for forensic integrity — typically
0640(BFF user write, ops group read), not world-readable. Adjust to your operations posture. - Encrypted at rest if your compliance posture requires it. Use disk-level encryption (LUKS, EBS encryption) — the BFF itself does not encrypt.
What is NOT in the audit log
- Read operations. Dashboard fetches, alarm queries, MQE reads are not logged (volume would be unworkable, and reads have no side effects). Configure
debugLog.enabledfor wire-level read logging. - Typed passwords. Never logged. Failed logins show the actor but not the attempted password.
- OAP response bodies. Audit entries reference what was attempted, not the underlying response payload. For payload-level visibility, use
debugLog(./horizon-wire.jsonl).
In-memory “seen cache”
In addition to the on-disk audit log, the BFF maintains an in-memory UserSeenCache of successful logins:
- Records: username, source (
local/ldap/break-glass), roles, last-seen timestamp, last IP. - Reset on BFF restart.
- Exposed via
GET /api/admin/usersand visible on the Users admin page.
This is a UX convenience — it lets the Users page show “who has logged in to this BFF instance recently” without parsing the audit log. For historical / cluster-wide analysis, parse the JSONL file directly.
Wire-up to log pipelines
JSON Lines is ingestable by almost everything. Common pipelines:
| Pipeline | Configuration |
|---|---|
| Vector | [sources.horizon] type = "file", format = "json" |
| Fluent Bit | INPUT tail, Parser json |
| Promtail / Loki | pipeline_stages: - json |
| Elastic Filebeat | filebeat.inputs: - type: filestream, parsers: - ndjson |
| Splunk Universal Forwarder | INDEXED_EXTRACTIONS=JSON |
Index on actor, action, outcome for the common queries (“all admin actions by user X in the last 24h”).