Audit trail retention policy

Written policy, 2026-10-06. The archive retention period is to be agreed with the customer; until it is, every audit row stays online. Enforced truncation is not in this release.

Reference · Updated 2026-10-19 · Describes release 0.1.164

Written policy, 2026-10-06. The archive retention period is to be agreed with the customer; until it is, every audit row stays online. Enforced truncation is not in this release.

The record archive (DESIGN.md §13.8) does not change this policy: it archives operational records past their window and carries copies of the sealed audit rows that name them, but no audit row is ever removed or moved. Archiving the audit chain itself is phase 2.

Scope

Every row of the auditevent table, in its hash chain:

What each action records is listed in the in-product chapter People, access and audit (/portal/docs) and, for staff, Platform configuration (/ops/docs).

What the platform guarantees today

What it does not guarantee. The chain head is stored in the same database as the rows. Someone with direct write access to the database who rewrites a row and recomputes every later hash and the head produces a chain that verifies. The defence is an anchor kept outside the platform: the archive manifest below. The database is also not write-once storage — the write-once copy is the customer's archive.

Retention

Copy Period Status
Online, in the platform database Indefinite: the product never deletes an audit row In force
Archive, in customer storage To be agreed with the customer — period: ____, agreed on ____, by ____ Pending

Until an archive period is agreed, nothing is removed and the monthly archive below is the recommended practice. Agreeing a period changes only this document.

Shortening the online trail — archive, then truncate behind a sealed checkpoint — is planned, not built. It needs a checkpoint row that seals the head before truncation, a database role allowed to delete plus a trigger change, and a verify that starts from the checkpoint. Until it ships, online storage grows with the trail; the sizing below quantifies it.

Archive procedure

Monthly, per chain, run by the customer. A workspace chain needs a read-scope token of an account with report.view in that workspace. The platform chain needs an admin-scope token of a platform superadmin.

  1. Take the last archived seq and hash from the previous manifest entry (0 and no hash for the first archive).

  2. Export from there with the cursor, 50,000 rows per file, until X-Truncated is false:

    after=$LAST_ARCHIVED_SEQ
    while :; do
      curl -s -D headers.txt -H "Authorization: Bearer $AIOPS_TOKEN" \
        "https://aiops.example.com/api/aiops/$TENANT_ID/audit/export?fmt=json&after_seq=$after&limit=50000" \
        -o "audit-$TENANT_ID-after-$after.ndjson"
      after=$(grep -i '^x-next-after-seq:' headers.txt | tr -d '\r' | cut -d' ' -f2)
      grep -qi '^x-truncated: true' headers.txt || break
    done
    

    For the platform chain, call https://aiops.example.com/api/aiops/admin/audit/export?fmt=json&after_seq=$after&limit=50000 (no tenant id). Rows written in the last minute are not sealed yet and are left for next month's export.

  3. Verify every file offline:

    python3 scripts/verify-audit-export.py audit-$TENANT_ID-after-$LAST_ARCHIVED_SEQ.ndjson
    

    Each file must print OK and no gap: line. A cursor export with no filter is the chain itself, so the script fails it on any seq gap, and any file whose row count differs from its trailer's; a gap: line means the file was exported with a filter or a period and is not an archive file. The first file's links back to: line must show the previous manifest's last seq and hash; each later file must link back to the anchor: of the file before it.

  4. Store the files in write-once storage for the agreed period — for example an S3 bucket with Object Lock in compliance mode, or an immutable index in the customer's SIEM.

  5. Append a manifest entry and store the manifest the same way: chain id; file names with the sha256 of each file; first and last seq; the last row_hash; the X-Chain-Last-Seq and X-Chain-Head-Hash returned with the last file; export time; who ran it. The manifest is the external anchor.

Boundary guarantee. The first row after an archive (seq = last archived + 1) carries prev_hash equal to the last archived row_hash. That holds between consecutive archive files, and between the archive and the online trail — today because nothing is deleted, later because the truncation checkpoint will seal exactly that boundary.

Checking the anchor. At any time, export the archived boundary row again (after_seq=<manifest last seq − 1>&limit=1) and compare its row_hash with the manifest. A difference means the online trail was rewritten after it was archived; the archive is the record.

Sizing

Measured on the demo deployment on 2026-10-06 with read-only queries (53 days of history). The demo runs automated fault-injection labs alongside its workspaces, so these rates are an upper-end example, not a forecast for a production workspace.

Chain Rows Rate, last 30 days NDJSON bytes per row (newest 10,000) NDJSON per year at that rate
Busiest workspace 223,532 470 a day (bursts averaged 4,239 a day over the 53 days) 470 ≈ 81 MB
Second workspace 16,739 (20 days) 558 a day (899 a day over the last 7) 527 ≈ 107 MB
Third workspace 12,023 301 a day 565 ≈ 62 MB
Platform chain 9,339 170 a day 360 ≈ 22 MB
Whole deployment 292,700 ≈ 2,400 a day ≈ 470 ≈ 0.4 GB

Dry run, 2026-10-06. The newest 10,000 rows of the three busiest demo workspaces and the whole platform chain (9,339 rows), in the export's row format, verified with scripts/verify-audit-export.py: all OK, each ending on its chain head. It ran inside the demo cluster against the database, in memory, because the cursor export was not yet deployed there; repeat it through the export API on the first monthly archive.

After a database restore

A restore (point-in-time or snapshot) rolls the trail back to the restore point: rows written after it are gone, and what remains is an earlier, consistent chain that still verifies. After any restore:

  1. Run a full replay of every chain: the platform console's Verify chains with Full replay, or GET /api/aiops/admin/audit/verify?full=true.
  2. Compare each chain's head with the latest manifest. A head below the manifest's last seq means archived rows are no longer online — expected after restoring to an earlier point; the archive is the record for that span, and the next export continues from the restored head.

Automated database backups are kept for 7 days; see availability.md in this folder.

Not in this release

References

Next steps