メインコンテンツまでスキップ
このページはまだお使いの言語に翻訳されていないか、翻訳が技術レビュー待ちです。以下には英語の原文を表示しています。 英語のページを開く

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.