Lewati ke konten utama
Halaman ini belum tersedia dalam bahasa Anda, atau terjemahannya sedang menunggu tinjauan teknis. Teks asli dalam bahasa Inggris ditampilkan di bawah. Buka halaman berbahasa Inggris

Access Ledger

This page answers the question a security or compliance reviewer asks: "Can you prove who looked at our data?" It describes the audit surfaces the Control Zero backend actually writes, where they live, how long they are kept, and how a customer gets them out.

Read this first: there is no single "disclosure boundary" table​

There is no single disclosure-boundary table where every channel that can release subject-bearing data writes an event before release.

What does exist is a set of five separate sinks, each of which covers a different part of "who did what, and who looked":

SinkTableCovers
Decision / egress eventsaudit_logsPolicy decisions on tool calls, gateway requests and browser egress attempts, as reported by connected clients (enrolled machines, API-keyed SDKs, the gateway, the browser extension). A local-only SDK with no API key or enrollment logs locally and does not write here.
Control-plane admin actionsadmin_audit_logConfiguration changes, auth events, SCIM, billing, retention changes, exports.
Args unmask trailaudit_unmask_logEvery time an admin decrypts redacted audit args.
Captured-I/O reveal trailaudit_view_logEvery time a user decrypts a captured input/output payload.
Captured-I/O purge trailaudit_purge_logCrypto-shred purges of captured I/O. The scheduled retention worker writes its record after shredding, on a best-effort basis, so a failed write can leave a scheduled shred unrecorded.

The unmask and view trails implement a record-before-release property: the paired audit row is written and confirmed before the plaintext bytes leave the server, and if the write fails the plaintext is withheld with a 500. Manual purges use the same pattern before the shred: if the record write fails, nothing is shredded. The scheduled retention shred records its row afterwards, on a best-effort basis. That is the closest the product has to a disclosure boundary, but it is spread across three tables rather than one, and there is no single table that enumerates every disclosure channel.

1. Decision events — audit_logs​

audit_logs is the main ledger. It lives in a dedicated Postgres audit database, separate from the transactional database, and that database is the audit read source. On the self-hosted bundle both databases run on the same Postgres server by default.

What is recorded​

Each row is one decision — allow, deny, warn, or not_evaluated — or one credential_leak_detected event. The current ingest seams all write the same table (the legacy POST /v1/sdk/logs route still writes to it):

  • Enrolled CLI SDKs: POST /api/audit (machine auth).
  • Hosted SDKs / the gateway LLM proxy: POST /v1/sdk/audit (API key).
  • Browser extension: POST /api/v1/browser/audit.
  • Legacy hosted SDK seam: POST /v1/sdk/logs (API key), which mirrors /api/audit.

The SDKs' tool integrations send each call's tool arguments with its audit event by default. What the platform keeps of that content is governed by two opt-in mechanisms, described in "Arguments and captured I/O" below.

What each event contains​

The core fields are:

FieldMeaning
idRow UUID. On the CLI-SDK and API-key seams (/api/audit, /v1/sdk/audit) it is client-supplied and is the dedup key, so retried batches are idempotent. Browser-extension rows get a server-generated id, so a retried browser batch is not deduplicated.
org_id, project_idTenancy. Org is always overwritten from the authenticated credential, never trusted from the wire; project is validated to belong to that org.
agent_id / user_email / hostname / machine_idWho ran it and on which machine.
tool_name, method_name, extracted_methodWhat was invoked (tool, method, and the hook-extractor's resolved method).
policy_decisionallow / deny / warn / not_evaluated.
policy_id, policy_source, reason_codeWhich policy/rule produced the decision, its provenance (hosted/local/cache-fallback/tamper-quarantine), and the machine-readable reason.
enforcement_statusWhether enforcement was actually healthy (enforcing, degraded_below_floor, fail_closed, unreported, …). Empty means "unknown", never "healthy".
dlp_status, dlp_reason, dlp_detailsWhether the DLP scan ran and what it found — kept separate so "did not run" is never confused with "ran clean".
status, error_type, error_messageOutcome beyond the policy verb (success/error/timeout/blocked).
latency_ms, input_tokens, output_tokens, total_tokens, estimated_cost_usd, model_id, providerLLM telemetry for proxy rows.
client_ip, client_name, client_version, controlzero_sdk_version, source, surface, trace_idProvenance: IP, host CLI, SDK package version, coarse surface class, W3C trace id.
args_*, input_/output_ciphertext and the io_* columnsOpt-in content (see below).
credential_* columnsCredential-leak events: pattern id, severity, a one-way keyed per-org fingerprint of the value (16 hex characters) used to group repeat leaks, and a masked context window. The leaked value itself is never stored.
tagsStructured extras (enforcement outcome, browser-only metadata).

Arguments and captured I/O (opt-in)​

  • Arguments are governed by the per-org audit_redaction_mode: redact (default, values dropped), redact_with_admin_unmask (values sealed with a per-org DEK, admin-unmaskable), or store_full (plaintext JSON, explicit opt-in). The ingest handler resolves the mode once per batch and defaults to redact on any storage error — "drop values, never leak them".
  • Captured input/output is off by default and cascaded through org tier, org default, project override, and per-call SDK hint. Before it is sealed, it is run through two redaction passes — one for secret-shaped tokens, one for structured PII — then sealed with a per-record DEK wrapped under the org KEK.
BETA (manual install; not in the Chrome Web Store)

The browser extension goes further: detected values are never stored. Only a keyed per-device fingerprint of a match is persisted; a non-fingerprinted value is dropped and marked redacted.

2. Control-plane admin actions — admin_audit_log​

admin_audit_log records who changed the governance configuration and other security-relevant control-plane actions. Fields: admin_email, action, target_type, target_id, old_values, new_values (JSONB), ip_address, user_agent, created_at, trace_id, and actor provenance: actor_type (user / api_key / system), actor_id, key_id (the API key that acted, when one did) and source (dashboard / api / cli / mcp / migration / system).

Action families:

  • Authentication: auth.login.success, auth.login.failure, auth.login.lockout, auth.password.changed, …
  • SSO: sso_config.created/updated/deleted, sso.login.success/denied, sso.enforce.*, sso.break_glass.*.
  • SCIM provisioning: scim.user.provisioned/deprovisioned/role_changed, scim.auth.failure.
  • Billing: billing.subscription.upgraded/changed/downgraded.
  • Audit retention: audit_retention_changed.
  • Report exports: report_export.queued/cancelled/downloaded.
  • Account self-deletion: account.self.deleted.

Secrets are never written: SSO secret fields record only *_set presence booleans.

3. Who looked at the data — the three "self-incriminating" trails​

These are the tables that directly answer "who accessed what customer data". audit_unmask_log and audit_view_log rows are written before any decrypted bytes are returned; a manual purge's audit_purge_log row is written before the crypto-shred runs, while the scheduled retention shred records its row afterwards, on a best-effort basis.

TableWritten byFields recorded
audit_unmask_logPOST /api/orgs/{orgID}/audit/{auditRowID}/unmaskorg_id, actor_user_id, target_audit_row_id, mfa_assertion_id, reason, unmasked_at.
audit_view_logPOST /api/orgs/{orgID}/audit/{auditRowID}/view-io and POST …/io-capture/revealorg_id, actor_user_id, target_audit_row_id, viewed_field (input/output), reason, viewed_at.
audit_purge_logPOST …/io-capture/purgeorg_id, actor_user_id, purge_kind, reason, rows_shredded, purged_at.

Access floors for the two reveal paths are enforced in the handlers: authenticated org member/admin, a non-empty reason, and an explicit acknowledged attestation; native-auth callers additionally require a recent step-up. If the paired row write fails, the request 500s and no plaintext leaves the function.

4. Where it is stored​

  • audit_logs: a dedicated Postgres audit database, separate from the transactional database (both run on the same Postgres server in the default self-hosted bundle).
  • admin_audit_log, audit_unmask_log, audit_view_log, audit_purge_log: the transactional Postgres database.
  • audit_logs in Postgres is append-only at the database level. A trigger rejects every UPDATE except the captured-I/O crypto-shred, which may only clear the payload key and set the purge flags. It also rejects every DELETE unless the transaction explicitly opts in. The backend opts in only for org-deletion erasure (GDPR Art. 17 / PIPA Art. 21) and for moving rows between monthly partitions. The opt-in is a session setting, so it stops ad-hoc deletes, not a database operator who sets it deliberately.

5. How long it is kept​

Two independent windows:

  1. Per-org retention setting: organizations.audit_retention_days, default 30. The org owner sets it between 1 and 2555 days (7 years) via POST /api/orgs/{orgID}/settings/audit-retention, and every change is recorded in admin_audit_log. On the Postgres audit store that production runs, no worker currently deletes audit_logs rows past this window: the per-org retention worker runs only against the retired legacy audit store. Decision rows are removed only by org-deletion erasure (or cold-tier export, when armed); if that erasure fails, the organization deletion still completes and the failure is logged for manual follow-up.
  2. Cold tier — older month partitions of audit_logs can be moved to object storage as Parquet+zstd, with a verified dual-cloud export before any drop. This is dormant by default (AUDIT_COLD_TIER_ENABLED=true required); it is not active unless explicitly armed.

6. Who can read what​

  • Decision ledger (audit_logs): authenticated org members, scoped to their org and optionally one project, via GET /api/audit-log, /api/audit-log/{id}, /api/audit-log/stats, /api/audit-log/tools. The list carries metadata only — never ciphertext.
  • Args unmask: org admin/owner route, plus the reason + attestation floor.
  • Captured-I/O reveal: authenticated org member, plus reason + attestation.
  • Captured-I/O purge: org owner.
  • admin_audit_log: platform admins via the admin console. This is Control Zero's own admin surface, not a customer-facing endpoint.

7. How a customer obtains the ledger​

  1. Dashboard audit log — the list/detail pages with filters for decision, tool, machine, surface class, enforcement status, and DLP outcome.
  2. Report export — POST /api/orgs/{orgID}/report-exports creates an async job; completed parts are gzip-compressed CSV (events-part-NNNNN.csv.gz) downloadable with a SHA-256 ETag and an expiry (AvailableUntil). Queue, cancel, and download are recorded in admin_audit_log on a best-effort basis: a download is streamed before its audit record is written, and a failed audit write does not block it, so a download can occur without a ledger row.
  3. SIEM forwarding: a syslog forwarder, configured deployment-wide by whoever operates the backend (SIEM_SYSLOG_ADDR), pushes decisions stored through the CLI-SDK and API-key ingest paths to a collector. Browser-extension rows are not forwarded. It is fire-and-forget: it never blocks ingest, and when its queue is full rows are dropped (and counted in a metric).
  4. Cold reads: when the cold tier is armed, the operator can read cold Parquet objects back with an operator-side tool. No dashboard page or API endpoint serves cold data.

8. Partial implementation — named gaps​

  • There is no single disclosure-boundary table and no unified customer-facing endpoint that lists the audit_unmask_log / audit_view_log / audit_purge_log history. The only customer-visible surface of those trails today is io_last_viewed_by / io_last_viewed_at / io_last_viewed_reason on a decision row that has captured I/O; the full history tables are server-side.
  • The audit cold tier is dormant by default and is gated behind explicit configuration.
  • The audit-list read default range is the last 24 hours; older data requires an explicit start_time/end_time or the retention window.