The audit log: hash-chained, redaction-safe, and how to verify, export and feed your SIEM
This page is the canonical account of the downpipes audit log, written for an auditor who needs the exact guarantee rather than a slogan, and who wants to read the trail, verify the chain, export it before anything rolls over, and feed it to a SIEM. Every claim here is taken from the recorder, the chain verifier, the retention rollover and the pull credential in the engine code.
The audit log is an append-only record of every privileged action against the engine admin API: who acted, what they acted on (with values redacted), when, from where, and the outcome. It lives in your own Cloudflare account, inside the scheduler Durable Object that holds the chain. The vendor cannot read it and cannot alter it.
Two facts set the reader’s expectations before anything else. The log is tamper-evident, which is detection, not prevention. And it is redaction-safe for your customer data while it deliberately does carry operator identity, including member emails. Both are spelled out in full below, because getting either wrong is the easy mistake.
The precise guarantee
Each entry is hashed together with the entry before it. The chaining hash is SHA-384 computed over the canonical JSON of every field except the hash itself, and that hashed object includes the previous entry’s hash. So each entry commits to its own content and to the prior entry’s hash, which is what makes a later change show up. The genesis entry links to a fixed placeholder, sha384: followed by ninety-six hex zeros (GENESIS_PREV_HASH in audit.ts).
The honest statement of the property is this. A holder of the underlying Durable Object storage could rewrite the whole chain, recomputing every hash from a forged genesis forward, and that fully re-chained log would verify. What the chain guarantees is narrower and real: any edit or deletion of a past entry that is not a full re-chain is detectable by the verify-chain action, and the exported head hash lets an external verifier detect truncation that happened after an export. Detection is what is claimed. Prevention is not, which is why the property is tamper-evident rather than the stronger word.
Tamper-evident means detectable, not impossible
Modification detection is not modification prevention. The chain makes a partial edit, a deletion or a reorder detectable; it does not make the storage physically unwritable. If you need prevention rather than detection, that lives in the storage layer, so pair the audit log with bucket-level immutability at the destination. See immutability and attestation.
Redaction by construction
The recorder cannot be handed a free-form details field. The only way to add an entry is to supply a closed target union plus a small set of safe scalars, so the safe path is the only path the types allow, even for a buggy caller. The closed target union (AuditTarget in audit.ts) can describe a downpipe id or name, a run id, a target binding name on a redirect, a role, a member email, a count, a plan hash, a version string and a free-text reason. It cannot represent a key, a value, a secret, a private fingerprint, a destination credential, an endpoint, a bucket, a region, or the pinned licence or update signer. Those have no field to live in.
The table below lists what an entry can carry, with an example and why each item is safe to record. It is the reviewable surface of an entry, and nothing outside it reaches the trail.
| Field | Example | Why it is safe to record |
|---|---|---|
| Action | restore-apply, role-change, dest-config-set |
A closed enumeration of privileged actions, not a payload. |
| Outcome | success, denied, failed |
A coarse verdict; denied and failed attempts are first-class. |
| Downpipe id or name | dp_ops-db, “Production database” |
Your own non-secret label, like a row id. |
| Run id | run_01J... |
An opaque identifier for a run, carries no content. |
| Redirect binding name | R2_RESTORE_TARGET |
The binding name on a redirect restore, never its credential. |
| Role | Operator, Approver, Owner |
A closed role union, not a permission payload. |
| Member email | ops@acme.example |
Operator identity, the actor key itself (see the scope note below). |
| Counts | 12 capabilities, a pruned-run count |
An integer, never the underlying records or values. |
| Plan hash | sha384:... over names, counts and selectors |
A digest of redaction-safe inputs, not the data. |
| Version string | the engine version after a change | A public build identifier. |
| Reason | “Restoring after a bad migration” | Operator free-text justification, not a secret. |
For the pending dual-control records, the same discipline holds. A queued configuration change carries its opaque id and the mutation kind it queues, never the change body; a queued owner action carries its opaque id and the operation kind it gates, never the parameters. The plain-English diff a reviewer reads lives only on the pending record behind authentication, never in the chain.
The honest scope of redaction
Redaction-safe here means safe for your customer data, not free of personal information. The trail records operator identity on purpose, because attributability is the point of an audit log. Each entry carries the actor’s verified display email and stable subject where there is an attributable human, the actor’s auth method, and the source IP from the CF-Connecting-IP header the engine sees. A restore approval additionally records the approver’s email under the maker-is-not-the-checker rule.
The log carries operator identity
Because the audit log and the SIEM feed carry member emails, and where applicable a source IP and an auth method, the trail is never described as carrying no personal information. It holds none of your customer data and none of your keys; it does hold who did what. Treat the export and the feed as records that contain personal identifiers, and handle them accordingly.
Reading the trail
The console renders the trail newest first, with a chain head line above the table and the full head hash collapsed for copying. Each row names the actor, the action, the redaction-safe target, the time, the source and the outcome. The actor shows the verified email where there is one; where none is recorded it names the method instead, so an engine-observed event reads as the engine, a passkey session as the passkey, an SSO sign-in as OIDC or SAML, a break-glass recovery-code sign-in as the recovery code, the shared-token fallback as the token, and an Access actor with no recorded email as Access. The source column shows the IP for a human action, the word “system” for an engine-observed event, and “not recorded” when an IP was absent.

The filters are the server-side query the engine applies, so a search reaches the whole retained log rather than the page on screen. You can constrain by actor, by a single action, by outcome, by a downpipe id, and by a date range. A filtered view is reflected back into the URL, so it is bookmarkable and survives a refresh. The filters do not carry into an export: the console’s export always requests the whole retained log. The downpipe constraint is the one filter with no box in the bar: you reach it by following a downpipe’s own audit link, and it then shows as a removable chip.

The actor filter matches the actor’s verified email exactly and case-insensitively, and it matches nothing else. It does not match the sign-in method, so typing a method name like passkey or token finds nothing, and an entry that carries no recorded email never matches an actor filter at all. That empty-email case covers every engine-observed event and every action taken on the shared-token break-glass, so to find those, filter by action or by outcome rather than by actor.
The date range is compared against each entry’s UTC timestamp, and its two bounds are not symmetrical. The From date is inclusive and covers everything from the start of that UTC day onward. The To date is exclusive and covers only entries before the start of that UTC day, so the To day itself is not included. To take in a particular end day, set To to the day after it. Because the comparison is in UTC while the picker uses your local calendar date, an event near midnight can land on an adjacent UTC day, so allow for that when you choose the bounds.
The actions you can filter by
The Action filter offers the recorded actions grouped into twelve families, so the dropdown is a set of labelled groups rather than one flat list. The families and a sense of what each covers are below, in the order the dropdown lists them.
| Family | What it covers |
|---|---|
| Restores | Requesting, approving, rejecting and applying a restore |
| Downpipes and runs | Creating and deleting a downpipe, repairing its roster, attaching or detaching its sources, triggering a run, and a retention prune’s request, approve, reject and apply lifecycle |
| Roles and access | Role, group-mapping and custom-role changes, an access-policy change intent, and a security-check override being set or withdrawn |
| Identity and sign-in | Identity-provider connection changes, an IdP sign-in, an authentication failure, recovery-code generation and use, revoking a passkey, revoking every sign-in factor for one member, ending a session, bootstrap consumption and break-glass token retirement |
| Approvals and change control | The four-eyes propose, approve, reject and supersede lifecycle, the approval-policy toggle, a recorded change number, and an owner-action’s propose, approve, execute and reject lifecycle |
| Keys | The key ceremony, key installation, break-glass rotation, adding or removing the operational key, a posture acceptance, and emailing a custody share |
| Attested verification | Starting, proving, verifying a run against and aborting an attended-verification session |
| Engine and destinations | The discovery token and accounts, the archive destination, canary configuration, licence activation, expiry-item cleanup, and the engine-observed secret and version changes |
| Engine updates | Promoting, applying, rolling back and refusing an engine update |
| Test faults (harness estates only) | A deliberately injected test fault being armed, cleared before it fired, or fired |
| Control-plane recovery | Exporting, reading an empty response from, reconciling and resuming from the control plane |
| Support and egress | Minting or revoking a support credential, and the SIEM and OTLP push destination and delivery-failure events |
The Test faults family is listed on every estate, and on your own it will select nothing. Those three actions are only ever written on a harness estate running deliberately injected faults, and they exist so that a reviewer can ask whether a fault was armed when a particular verdict was banked. Seeing the group in your dropdown means the console lists the whole vocabulary, not that faults have been injected into your engine.
The filter offers every action the engine can write, so no recorded action is unreachable from the dropdown. The completeness is enforced at build time against the action union, and the filter runs server-side, so a selection reaches the whole retained log. An action a console build does not recognise, from a newer engine, still lists and reads by its raw identifier rather than being dropped.
Verifying the chain
The verify-chain action recomputes the chain in ascending sequence order and reports either intact through the head entry, or the first sequence number at which it breaks. It detects three break classes, and a break is a calm result rather than an error.
| Break class | What it means | How it is detected |
|---|---|---|
| Stored hash does not recompute | An entry was edited in place | The recomputed SHA-384 over the entry’s own fields does not equal its stored hash. |
| Previous-hash does not match | An entry was inserted, removed, reordered, or its predecessor edited | An entry’s prevHash does not equal the prior entry’s stored hash. |
| Non-contiguous sequence | An entry was deleted, even if the surrounding hashes were re-linked | A sequence number is not exactly its predecessor’s plus one. |
A break is a result at HTTP 200, not an error
When verify finds a break, the route still returns HTTP 200 and the console renders it calmly as “break detected at entry N”. This is the detection the design exists to provide, so it is reported, not thrown. The console only treats verification as pending when the route is genuinely absent (an HTTP 404 or 501 on an engine build without the audit store), and treats a real server or network failure as “chain integrity is unknown”, never as a quiet pass.
The contiguity check is what makes the deletion claim true within the retained set. A re-linked chain with a hole still has a sequence gap, and the gap is reported at the entry after the hole. After a documented rollover the verifier is told not to expect a genesis (see below), but every other check still applies to every retained entry.
The fixed-cap rollover
The chain does not retain everything forever. It is capped at ten thousand entries (AUDIT_CAP is 10000), so the audit store cannot grow Durable Object storage without limit. At the cap the engine rolls over the oldest entries first. This is a documented retention rollover, not a silent drop, and it preserves the properties that matter:
- The sequence numbers stay monotonic, because a rolled-over sequence is never reused.
- The retained chain still verifies from its first retained entry, because every retained hash and link is unchanged.
- The engine records the earliest retained sequence and a cumulative count of how many entries have rolled over, so a reviewer sees the chain legitimately begins above sequence one.
- The verifier is told not to expect a genesis once a rollover is on record, so a pruned genesis reads as a rollover rather than a spurious break.
The operator path to keep the full history is to export before the rollover. Status flags the log as near its cap at ninety per cent of the limit (AUDIT_NEAR_CAP_FRACTION is 0.9), and the console raises a calm export prompt only when the engine reports that flag.
Export is how you keep history beyond the cap
The cap bounds only what the live store holds. Because the rollover is counted and surfaced rather than hidden, no entry silently loses its evidentiary value, but the bytes of a rolled-over entry are gone from the live store. An export taken before rollover, carrying its own head hash, is the durable copy of the older history.
Exporting before rollover
The export is the whole retained log, not a single page and not the slice you are looking at. The engine’s export route does accept the same narrowing parameters the filter bar uses, but the console’s export button does not send them: it asks for the whole log every time, with the chain head attached so an external verifier can confirm completeness and detect truncation after the export. If an auditor asked you for one actor’s activity, export the whole log and narrow it afterwards, and do not present a filtered screen as the evidence you exported. JSON is the default and CSV is offered for a spreadsheet or a SIEM import. The CSV flattens each entry’s target to a single redaction-safe target column, a short human description built only from the closed union’s safe fields, never a raw value.
Open the audit log
Open the audit log in the console under Access and security, then the Audit log tab. The filter bar is the server-side query the engine applies (actor, action, outcome, downpipe and a date range), so what you read on screen reaches the whole retained log rather than the first page. It does not narrow what you are about to download.
Export as JSON or CSV
Use Export (JSON) for the self-describing document, or Export (CSV) for a flattened import. Both downloads carry the chain head, and the console confirms the head hash was included. JSON keeps each entry’s structured target; CSV renders the target into one column.
Pin the head hash for an external check
The exported document carries the head sequence and the head hash. Keep that head hash offline. Later, an external verifier can recompute the chain from the export and confirm it ends at the same head, which is how truncation after the export becomes detectable.
Standing up a SIEM pull
The on-screen export is one path. The other is an out-of-band pull, distinct from the export, that a SIEM collector polls on a schedule. An Owner mints a scoped audit-feed credential, and the collector reads the hash-chained events from a route outside the admin API, with the chain head attached for continuity checking. This is the feed described in full on the SIEM audit feed; the steps here are the audit-log view of it.
Mint an audit-feed credential as Owner
In the console support surface, an Owner mints an ingest credential for the audit-feed scope. The mint asks you to re-authenticate with your passkey at the moment you do it, because the route sits in the engine’s step-up set, so a session cookie on its own does not reach it. Have your authenticator to hand before you start. The break-glass token and a caller arriving through Cloudflare Access are exempt, each carrying its own possession check already; the SIEM audit feed covers that boundary in full.
The secret is shown once at grant and only its SHA-384 is stored; verification is constant-time and expiry is enforced server-side. One credential is active per scope, so re-granting replaces the previous one, and revocation takes effect immediately. The default lifetime is ninety days with a three hundred and sixty-five day cap. The grant itself is recorded in the audit log as a
support-credential-grant, carrying the scope and the public client id, never the secret.Configure your collector to poll the feed
Point your SIEM HTTP-pull connector at the audit-feed route on your engine custom domain, presenting the credential as a single bearer of the form
clientId.secret. The collector polls with anafterSeqcursor and a bounded limit; the engine returns the ascending events with sequence greater than the cursor, plus the head sequence and head hash so the collector can checkpoint by sequence and confirm continuity by previous-hash and hash.Verify continuity on the collector side
Each poll carries the chain head, so your collector can confirm the events it has received link contiguously and end at the head the engine reports. Recent successful pulls are recorded on the grant (the most recent fifty; older entries roll over), so you can see in the console when the credential was used. The feed returns only the redaction-safe events; it reaches no admin route, no restore, no configuration write and no customer data.
What the engine observes for you
The audit log records actions driven against the admin API. Some posture changes happen out of band: applying a secret, redeploying the engine, or editing Cloudflare Access policy. The authoritative record for those lives in Cloudflare, in the deploy logs and in the Wrangler logs. The engine cannot witness those directly, but it can detect two kinds of result and append an engine-observed event for each, which partly closes the boundary.
On each status read the engine diffs the live status against a stored snapshot. It appends an engine-observed event when a tracked presence flag flips from off to on (the signer, the break-glass recipient or the destination becoming configured), or when the engine version changes between two observations. A flag flipping from on to off is recorded the same way with a failed outcome, because a configured signer or destination going absent is never healthy. That version-change event fires on either of two triggers: the software version itself changing, or the underlying Cloudflare deploy id changing while the software version stays the same, which catches a same-version redeploy that could otherwise silently drop a live source binding. These events carry the engine as the actor method, no attributable human, and a redaction-safe target naming only the field, the new software version, or the new deploy id. The very first observation only sets the baseline and records nothing, so onboarding does not emit a burst of events for wiring you are in the middle of.
Deeper detail: the hash, the head pointer and the credential
Exactly what is hashed. The hashed object is the entry’s sequence, timestamp, actor email, actor method, source IP, action, outcome, target and the previous hash, in canonical sorted-key order; the stable actor subject is folded in only when present, and so is the IdP advisory signal (acr, amr and auth_time) on a sign-in that carried one. Canonical JSON sorts keys and forbids non-integer numbers, so the digest is reproducible across the append and the verify, and the hash is sha384: followed by the hex digest (auditHash in audit.ts). The actor subject is optional for backward compatibility, so an entry recorded before subject-keying hashes byte-for-byte as before and the chain still verifies across that upgrade.
Why the append stays cheap, and why integrity is unchanged. The engine persists a single head pointer (the head sequence, the head hash and the retained count) and chains the next entry off the head hash, rather than listing the whole chain on every write. The new entry’s previous-hash is exactly the prior head’s hash and the sequence is strictly monotonic from the pointer, so the verifier’s link check and contiguity check are identical to a full re-derivation; only how the head is located changed.
The rollover record, precisely. When the retained count exceeds ten thousand, the engine prunes the oldest entries through a bounded list, then records the new earliest retained sequence and the cumulative rolled-over count under a single storage key. In steady state the cap is crossed by one entry per append, so the prune touches one entry. The verify route reads that record to decide whether to expect a genesis and to report the rolled-over count to the console.
The pull credential, precisely. The audit-feed credential is a client id plus a secret; the client id is a non-secret lookup label and the secret carries one hundred and ninety-two bits of entropy. Only the secret’s SHA-384 persists, the comparison is constant-time, and a failed presentation is a plain HTTP 401 with no detail. The same mechanism issues a separate, shorter-lived diagnostics credential for the vendor support bundle, which is a different scope from the audit feed.
Where this fits
This page owns the audit-log story for the site. For the credential lifecycle and the collector setup in full, see the SIEM audit feed. For storage-layer immutability that pairs prevention with this detection, and for what an attestation does and does not prove, see immutability and attestation. For the honest phrasings the whole corpus is held to, including why this is tamper-evident and why it is never described as free of personal data, see precise claims and honesty.
Last updated .