---
title: "Audit log"
---

# Audit log

The tamper-evident audit log of Neural Inverse Govern, with signed exports, offline verification, hot, cold and archive retention tiers, and client events from the Neural Inverse apps.

Govern writes every change and every enforced decision to one append-only audit log. Changes are written in the same database transaction as the change itself; if the audit entry cannot be written, the change does not happen.

## What makes it tamper-evident

- **Hash chains.** Each organisation has its own chain of entries. Every entry carries an HMAC-SHA256 hash over its content and the previous entry's hash, keyed with the deployment's `NI_AUTH_AUDIT_HMAC_KEY`. Removing, reordering or changing an entry breaks the chain.
- **Lanes.** For throughput, an organisation's chain runs in several lanes (8 by default); each lane is a chain of its own and entries within a lane are totally ordered. An organisation that needs one order for every entry can set one lane with `PUT /api/v1/audit-logs/chain-lanes` (Owner and Admin, signed-in users only). The change applies at the next epoch seal and never reorders existing entries.
- **Signed epochs and checkpoints.** Every minute, the heads of the chains that moved are sealed into an epoch: a Merkle root, linked to the previous epoch and signed with the audit signing key (Ed25519). Every five minutes a signed Merkle checkpoint is made over the new entries. Someone holding the HMAC key therefore cannot rewrite one organisation's chain without breaking the signed record.
- **Immutable rows.** A database trigger refuses any update or delete of audit rows, with one exception: the verified move of old entries to the cold tier (below).
- **Region.** Every entry records the region (cell) that wrote it inside its hash.

The public keys that verify signed checkpoints, exports and cold segments are published at `/.well-known/keystone-audit-keys.json`. The signing keys are sealed under `KMS_MASTER_KEY`, so keep that key stable.

## In the console

The **Audit** service has three pages: the log itself (Audit log on a self-hosted installation, Audit trail on the hosted service) with a detail view of each record, **Integrity** for chain verification, and **SIEM export**. Audit records from other services are linked from it, among them **Agent decisions** and the approvals **Decision history**. Reading the log needs `audit:read`.

### Filters

`GET /api/v1/audit-logs` and `GET /api/v1/organizations/{id}/audit-logs` take:

| Parameter | Meaning |
|---|---|
| `action` | One action, for example `agentpolicy.rule.created` |
| `action_prefix` | Actions starting with it, for example `models.`: 1 to 64 characters of `[a-z0-9._-]` ending with `.`; not together with `action` |
| `resource_type` | The kind of object changed |
| `outcome` | `success` or `failure` |
| `project_id` | A project of your organisation |
| `from`, `to` | RFC 3339 times |
| `limit`, `offset` | Paging |

Every non-GET API request is recorded with its action, outcome, request id, source IP, user agent, route, response status and duration. Request bodies, credentials and tokens are never captured.

## Reading and verifying

| Route | Permission | What |
|---|---|---|
| `GET /api/v1/audit-logs` | `audit:read` | List and filter the organisation's entries (hot tier) |
| `GET /api/v1/audit-logs/verify` | `audit:read` | Verify the chain over a range; longer ranges run as verification jobs. Each run is itself audited. |
| `GET /api/v1/audit-logs/query` | `audit:read` | Query both tiers (below) |
| `POST /api/v1/audit-logs/export` | `audit:create` | Start an export |
| `GET /api/v1/audit-logs/exports/{exportID}/download` | `audit:read` | Download a finished export |

### Query

`GET /api/v1/audit-logs/query?from=&to=&query=` runs a pipeline over one organisation's entries in both the hot and cold tiers:

```
fields @timestamp, action, outcome
| filter action like /^models\./ and outcome = "failure"
| stats count(*) as failures by action
| sort failures desc
| limit 20
```

Commands are `fields`, `filter`, `stats ... by`, `sort` and `limit`. A query is refused whole, never answered partially, when it would read more than 2,000,000 entries or 1 GiB of cold objects, or span more than 366 days.

### Signed exports

An export is a queued NDJSON or CSV file of a time window, with a manifest signed with the audit signing key (Ed25519) that can be checked offline. Exports read both tiers, so moving entries to the cold tier never removes them from an organisation's export of its own records.

### Offline verification

`keystone-audit-verify` verifies a chain without network access or a connection to Govern, for air-gapped installations and external auditors.

```
keystone-audit-verify verify \
  --chain audit_chain.json \
  --tree-heads tree_heads.json \
  --public-key keystone.pub \
  [--hmac-key KEY] \
  [--output report.json]
```

```
keystone-audit-verify cold \
  --dir ./audit-bucket \
  --keys keystone-audit-keys.json \
  [--hmac-key KEY] [--org ORG_ID | --platform-entries] [--hot chain.json] \
  [--output report.json]
```

`verify` checks an exported chain; with the HMAC key it also re-derives every entry hash. `cold` checks cold-tier segments: the manifest signatures, every chain link, every object's hash and the link from each segment to the next, and with `--hot` the boundary between the cold tier and the live database. An organisation can download a bundle of its own segments with `GET /api/v1/audit-logs/cold-segments/{seq}/bundle` and prove its rows complete and in the chain without anyone else's data and without the HMAC key.

## Retention tiers

| Tier | Where | Protection |
|---|---|---|
| Hot | PostgreSQL | Immutability trigger |
| Cold | An S3 or S3-compatible bucket, as signed Parquet segments | Server-side encryption with your KMS key; the signed manifest detects any change |
| Archive | The same bucket | Object Lock in compliance mode, until the newest entry of the object plus the archive retention |

Without a bucket, the whole audit log stays in PostgreSQL. With tiering configured:

- Entries older than the hot window move to the cold tier. On a self-hosted installation the hot window is `NI_AUTH_AUDIT_HOT_WINDOW_DAYS` (default 90) and the archive retention is `NI_AUTH_AUDIT_ARCHIVE_YEARS` (default 0, at most 10).
- **Fail toward keeping.** A row leaves PostgreSQL only after the object holding it was written, read back and matched, and only while its signed proof is in the bucket. A range whose chain does not verify is never archived; tiering stops with every entry still hot.
- **Retention only grows.** A longer archive retention extends the Object Lock of existing objects; a shorter one changes nothing.
- **Never deleted.** Govern has no delete operation on the audit bucket, and its IAM policy needs no `s3:DeleteObject`.
- **Legal holds** keep an organisation's entries hot.
- The bucket must have versioning, Object Lock and server-side KMS encryption. Cross-region replication is required unless you set `NI_AUTH_AUDIT_TIERING_REQUIRE_REPLICATION=false`. Requests are signed with Signature Version 4 and no cloud SDK, so an on-premises S3-compatible store with Object Lock (for example MinIO) works air-gapped.

| Variable | Default | |
|---|---|---|
| `NI_AUTH_AUDIT_TIERING_BUCKET` | | Turns tiering on |
| `NI_AUTH_AUDIT_TIERING_KMS_KEY_ID` | | Required: the server-side encryption key |
| `NI_AUTH_AUDIT_TIERING_REGION` | `AWS_REGION` | Bucket region |
| `NI_AUTH_AUDIT_TIERING_ENDPOINT` | AWS S3 in the region | S3-compatible endpoint |
| `NI_AUTH_AUDIT_TIERING_PATH_STYLE` | `false` | Path-style addressing |
| `NI_AUTH_AUDIT_TIERING_REQUIRE_REPLICATION` | `true` | Refuse to run without cross-region replication |
| `NI_AUTH_AUDIT_HOT_WINDOW_DAYS` | `90` | Hot window |
| `NI_AUTH_AUDIT_ARCHIVE_YEARS` | `0` | Archive retention |

A server whose log already has entries in the cold tier refuses to start without the bucket, because exports and queries would leave those entries out.

## Client events from the Neural Inverse apps

The Editor, the Agents app and the CLI record what a signed-in person does in the same chained log, through `POST /api/v1/audit/client-events`:

```json
{"events": [{
  "client_event_id": "0b0e8a2c-5f0a-4f0e-9c52-6a3b9f1e7d10",
  "type": "editor.file.opened",
  "occurred_at": "2026-10-16T11:59:12.431Z",
  "outcome": "success",
  "resource_type": "file",
  "resource_id": "src/main.go"
}]}
```

- **Only a person, only through a first-party app.** The caller must be a signed-in person whose access token was issued to a registered first-party OAuth client. API keys and other clients get 403.
- **Identity from the token.** Organisation and user come from the access token. A body that names another organisation or user is refused whole with `403 not_own_identity`.
- **Strict schema.** Unknown fields are rejected, so a client cannot set server-owned fields such as hashes or times. The timestamp must fall within bounds of the server's clock.
- **DLP.** Events are scrubbed with the organisation's DLP patterns; if the patterns cannot be loaded, nothing is recorded (`503 dlp_unavailable`).
- **Idempotent.** Each event is recorded once per `client_event_id`, and the answer acknowledges each event as accepted, duplicate or rejected.
- **Limits.** Per-person and per-organisation minute limits and daily quotas, set with the `NI_AUTH_CLIENT_EVENTS_*` variables, answer 429 with `Retry-After`.

## Not available yet

These audit features are present in the source but recorded as stubs or not mounted, so do not rely on them: the audit retention policy route (`/api/v1/audit-logs/retention`), the Merkle checkpoint routes under `/api/v1/audit-logs/merkle`, SIEM forwarding destinations configured from the audit page, and the separate enterprise audit API. Retention is set by the tiering configuration above, and signed checkpoints are made by the server's own job.

## Related records

- The agent policy decision log and the AI tool hook decisions are kept as their own records, with targets stored only as hashes. See [Agent policy](https://neuralinverse.com/docs/govern/agent-policy/).
- Audit and usage records pulled from third-party AI vendors are kept as vendor events with their own read and export API. See [Third-party AI coding tools](https://neuralinverse.com/docs/govern/third-party-tools/) and [Evidence](https://neuralinverse.com/docs/evidence/).
