Skip to content

Audit and evidence

A record you can hand over: hash-chained, exportable, and clear about what it proves. Query it, verify it, export it for your SIEM, and score the host's live configuration against ISO 27001, SOC 2 and NIST 800-53 controls.

Build an exportsynthetic record
Format
Decision
Streams
on the host
$ bin/audit-log export --format cef --stream git --day 2026-09-28 --decision deny --out audit-git-2026-09-28.cef

The same over HTTP, operator session onlyGET /api/audit/export?format=cef&stream=git&day=2026-09-28&decision=deny

One record in this format
CEF:0|vendor|product|0.1.0|git:push|push deny|8|rt=2026-09-28T09:14:02+00:00 act=deny duser=dev@example.test dvchost=product cs1Label=workspace cs1=acme/dev01 cs2Label=actorKind cs2=human cs3Label=repo cs3=acme/api cs4Label=reason cs4=validation-missing suid=7

Pick a format, streams, a day and a decision; the command is the real one. LEEF's tab delimiters are shown as <TAB>. The header's vendor and product fields carry the platform's own names on your host, shown here as vendor and product. Nothing is sent anywhere: export writes a file, and you ship it.

What it is, and what it is not

Kodela writes what happens on the host to an append-only, hash-chained audit trail: git, egress, model use, other provider keys, policy changes, detections, workspace lifecycle and identity. Each stream is a file of JSON lines. Credentials are redacted before a line is written. You can query it, verify it, export it in the formats a SIEM reads, and produce an evidence report from the live configuration.

It is tamper-evident, not tamper-proof: an edit breaks the chain and verify names the line, but the host's root user could rewrite a whole file. Nothing leaves the host unless you ship it. The evidence report is evidence for your audit, not a certificate.

Inside the console, each organisation reads its own share of the trail on its Audit tab, a dashboard for owners, admins and auditors, and its AI use on the AI usage tab.

How it works

Eight streams
git, egress, claude (the Claude proxy), keys (the key broker), policy, detect, workspace and identity. Separate files, so a chatty stream cannot bury a quiet one.
The chain
Each record carries the SHA-256 of the previous record with its own contents. Editing or deleting a line breaks every hash after it.
Every record
ts, stream, action, decision (allow, deny, observe, info or error), workspace, user, actor, actor_user_id, actor_kind (human, agent, system or unattributed), repo, reason, and any details.
Authorship blocks
A push.authorship record's block is also signed with HMAC-SHA256 under a key kept in the audit directory, mode 0600, named by id so a rotated key still verifies old records.
Rotation
bin/audit-log rotate drops records older than the retention setting, restarts each chain from the first record kept, and writes a rotated record naming what it dropped. It keeps anything an organisation's longer retention still needs.

Defaults

Settings for audit and evidence
SettingScopeDefaultAllowed
audit_enabledplatformtruetrue, false. Off also stops all identity mail.
audit_retention_daysplatform, organisation (raise only)90 days1 to 3650
bulk_read_thresholdplatform500 files10 to 100000
bulk_read_windowplatform60 seconds5 to 3600
bulk_read_detection in the profileper profileon under strict onlytrue, false
ai_chat_retention_daysplatform, organisation0: no time limit; chats are capped at 200 turns0 to 3650
Export, host-wideoperatoron demandcef, leef, syslog, json, csv
Export from an organisation's Audit tabowner, admin, auditoron demand: exactly the current filterCSV, JSON; formula-defused for spreadsheets; recorded as org-audit-export
Sign-in recordsplatformalways onpassword and shared-credential sign-ins, from orxy V1.7.2.2
Evidence reportoperatoron demandiso27001, soc2, nist80053

Configure it

Retention

on the host
$ bin/policy set --key audit_retention_days --value 180
# one organisation keeps its own records longer
$ bin/policy set --scope tenant --target acme --key audit_retention_days --value 2555
# see what rotation would drop, then apply it
$ bin/audit-log rotate --dry-run
$ bin/audit-log rotate

Nothing runs rotation for you. Add a timer that runs bin/audit-log rotate, or the trail grows until you do. bin/backup includes the audit directory and its signing keys.

SIEM field map

Every format carries the same fields. Severity is set from the decision so an analyst can alert on denials: deny 8, error 7, observe 4, allow 2, info 1. CEF drops any custom field that is empty, together with its label, so parsers do not pair the wrong ones.

How record fields map to CEF and LEEF
RecordCEFLEEF 2.0
stream:actionsignature id in the headercat and action
tsrtdevTime
decisionact, and severitysev
userduserusrName
actorsuseractorName
actor_user_idsuidactorUserId
workspacecs1 (label workspace)workspace
actor_kindcs2 (label actorKind)actorKind
repocs3 (label repo)repo
reasoncs4 (label reason)reason

Use CEF for Splunk, Microsoft Sentinel or ArcSight; LEEF for QRadar; JSON for an HTTP event collector or Elastic; syslog for RFC 5424 framing of CEF, facility local0; CSV for a spreadsheet. Records are exported oldest first.

The evidence report

bin/compliance-report runs a check for each technical control against the running host: it reads the firewall, re-walks the audit chain, reads a real password record. It scores 13 technical controls MET, PARTIAL or NOT MET, and lists 6 organisational controls (risk, people, suppliers, physical, continuity, incident response) as ORG: yours, never counted as a pass.

on the host
$ bin/compliance-report                          # text, all three frameworks
$ bin/compliance-report --framework soc2         # text, SOC 2 references only
$ bin/compliance-report --failures-only          # text, only PARTIAL and NOT MET
$ bin/compliance-report --json > evidence.json
$ bin/compliance-report --html evidence.html

The technical controls are identification, authenticator strength, account management, access enforcement, session revocation, logging, audit protection, audit review and export, network boundary, outbound traffic, secrets, encryption in transit and change management. --framework and --failures-only shape the text output; JSON and HTML always carry every control. The command exits 1 when any control is NOT MET, so a script can gate on it.

The organisation's Audit tab

The organisation's Audit tab (owners, admins and auditors) is a dashboard: a timeline by day, refused counts, sections by question, an integrity panel, and an export of the current filter that is itself audited. Up to 90 days, never past your retention; it shows only records provably the organisation's own.

Timeline and counts
It opens on recent activity, grouped by day. Counts of refused and would-be-refused actions filter the view with one click. Every record is written as a sentence.
Sections
People & access, AI activity, Code & provenance, Network, and Workspaces & settings. Approvals decided in the organisation's plans appear here too.
One record
Opening a record shows how it happened: authorship, agent run, approver and model. You can copy a link to it, or show it exactly as stored. No secret value is shown, because records are redacted on write.
Integrity panel
Says whether the audit log's hash chain verifies and how many of the organisation's pushes carry a verified signature, or that the check did not report. It names streams only, never a line.
Export
Exactly the current filter, as CSV or JSON. The file is formula-defused for spreadsheets, and the export is itself recorded in the audit.
Withheld records
Records that merely carry the organisation's name, from workspaces with no record of belonging to it, are withheld; the screen says how many and why.

Sign-in records

Password and shared-credential sign-ins are recorded from orxy V1.7.2.2 on (single sign-on sign-ins are recorded as “signed in with SSO”); earlier sign-ins are not in the trail. Password sign-ins at the portal and the operator console are recorded under People & access, allowed and refused. The password is never recorded, nor a token or session. An address that is not an account gets one host-level record with no address in it, never shown to an organisation, so the audit cannot be used to test which addresses are accounts. A flood becomes at most one record a minute per source, account, outcome and method, carrying the count. Recording never slows or blocks a sign-in: if its queue is full the record is dropped and counted.

The AI usage tab

An AI usage tab for organisation admins and auditors: requests, tokens and cost by member, workspace, surface, model and account. There is no spend cap; cost shows only where the cost meter is on; AI tools that hold their own login inside a workspace are not measured. Each download of its usage report is audited as ai-usage-report. See AI agents.

Tasks

Send a day of denials to your SIEM

  1. Export. bin/audit-log export --format cef --stream egress --stream git --day 2026-09-28 --decision deny --out denials.cef. Result: N records -> denials.cef (cef) on stderr.
  2. Ship the file with your collector: a forwarder watching a directory, or a scheduled copy. Kodela does not push.
  3. Map the fields once, from the table above. Alert on severity 8.

Prove the record to an auditor

  1. Verify the chain. bin/audit-log verify. Result: every chain verifies - no record has been edited or removed and exit code 0.
  2. Keep the output with the export you hand over, or run it in front of the auditor on the host.
  3. If it fails, the output names the stream and line, for example git.jsonl:1042 hash does not match - this line or an earlier one was edited or removed, and the exit code is 2.

Prepare for a SOC 2 evidence request

  1. List what fails. bin/compliance-report --framework soc2 --failures-only. Result: only the PARTIAL and NOT MET rows, each with the evidence the check saw.
  2. Fix the NOT MET controls the evidence names, then run it again.
  3. Write the report. bin/compliance-report --html evidence.html, and attach it with the verify output.

Review an organisation's week without the host

  1. Open the Audit tab as an owner, admin or auditor: OrganisationsacmeAudit Result: a timeline of recent activity, grouped by day.
  2. Press the refused count. Result: only refused and would-be-refused records, each as a sentence.
  3. Check the integrity panel. Result: whether the chain verifies and how many pushes carry a verified signature, or that the check did not report.
  4. Export the view as CSV or JSON. Result: a file with exactly the current filter; the export itself appears in the audit.

Answer the four questions for one day

  1. Ask them. bin/audit-log questions --day 2026-09-28 --user dev@example.test. Result: who cloned what, who pushed where, what was denied, what left the host, and signals raised, each with its records.
  2. Narrow it. bin/audit-log query --workspace acme/dev01 --decision deny --json for the rows behind an answer.

Find organisations looser than the default

  1. Run the posture audit. bin/tenant-audit, or --json. Result: a row per organisation and a verdict, naming any that are handed real credentials in their containers.

Verify it works

on the host
$ bin/audit-log query --day 2026-09-28 --stream policy
$ bin/audit-log verify
every chain verifies - no record has been edited or removed
authorship: 12 signed and verified, 0 unsigned (written before signing, or with no key available)

Good looks like: recent records in every stream you use, verify exiting 0, and the organisation's Audit tab showing that organisation's records on its timeline, with an integrity panel that says the chain verifies. Organisation owners, admins and auditors can read that tab and export its current filter; a plain member is not offered it. The host-wide HTTP export is for operators only.

Troubleshooting

hash does not match - this line or an earlier one was edited or removed

The chain is broken at that line. Treat it as an incident: keep the file, compare it with the latest backup, and find who had write access.

authorship signature does not match - the authorship block was edited

The chain may be intact while the signed block was changed and re-hashed. Only something holding the host key can do that; treat it as a root-level event.

audit-log: cannot read stream(s): …

A query exits 3 when a stream file is unreadable, so an empty answer is never mistaken for none. Check the file's owner and mode.

audit is disabled or the write failed

audit_enabled is off, or the audit directory is not writable. Turn it on; with it off, identity mail stops too.

audit_retention_days may only be raised above the platform's 90, never lowered

An organisation cannot keep less than the platform. Lower the platform value if you want less for everyone.

HTTP export answers 403 {"error": "admin only"}

The host-wide export route needs an operator session. Organisation owners, admins and auditors export their own records from the Audit tab instead.

The Audit tab says records were withheld

They carry the organisation's name but come from workspaces with no record of belonging to it, so the tab does not show them.

What it does not do yet

  • Built-in forwarding to a SIEM. Export writes a file; shipping it is yours.
  • Protection against the host's root user. The chain and the authorship key live on the host. Ship exports off the host if that matters.
  • Scheduled rotation. Nothing runs rotate for you.
  • Mappings for the EU AI Act, GDPR, HIPAA or DORA. The report covers ISO 27001:2022, SOC 2 and NIST 800-53 r5 only.
  • Certification. Kodela holds none. The report is evidence you use in your own audit.
  • The Audit tab's window is 1 to 90 days and never past the audit retention; it is paged and bounded by size and time, and it shows only records provably the organisation's own.
  • Sign-ins before orxy V1.7.2.2 are not in the trail.
  • No spend cap. The AI usage tab measures; cost shows only with the cost meter on, and AI tools that hold their own login inside a workspace are not measured.

Verified against the main branch, 4 Oct 2026.