Forwarding the audit log to your SIEM (push)
The engine also lets a SIEM collector pull the hash-chained audit trail on a schedule (wiring the audit feed into your SIEM). Push is the other direction. The engine dials out to your SIEM’s own intake, on the scheduler’s own tick. The audit trail reaches your SIEM, and nothing on your side polls for it.
One fact matters before any of the mechanics below. Push sidesteps the Cloudflare Access problem the pull feed can run into. The engine makes the outbound call, so there is no inbound request to turn away. An Access application in front of your console hostname never sees the delivery.
Your console hostname may sit behind Access. Push gets the same audit trail out, and it touches no Access policy.
Why push, alongside pull
SIEMs do not agree on how they want to receive events. downpipes supports both mainstream methods rather than picking one.
Some collectors poll a REST endpoint on a schedule, which is what the pull feed serves. Microsoft Sentinel, Exabeam, Cribl and Sumo Logic all take that cleanly. Others expect the opposite: an HTTP endpoint of their own that a source posts into. Splunk and the CrowdStrike Falcon Next-Gen SIEM lean on this, through Splunk’s HTTP Event Collector (HEC). Datadog accepts this shape only. Its hosted pull sources cannot carry the custom header the pull feed needs behind an Access-fronted hostname.
Push shapes eight body formats, mapped in the table below and the note after it, so most HTTP-intake SIEMs take it. A few, such as Cribl and Sumo Logic, do either method well. Place those on whichever method fits your topology. Push exists so the choice is yours per SIEM rather than a limitation of the platform. Running both at once is fine: a collector can poll for one system while a push destination feeds a second.
Push dials out, so a Cloudflare Access perimeter never sees it
The pull feed rides your console’s public hostname, because /support/* is proxied through the console to the engine. A Cloudflare Access application in front of that hostname changes what a collector meets. The edge answers the poll with a redirect to your team’s login page, before the engine ever sees the bearer credential. The fix is a service token, or a narrow path exemption for the collector. If Cloudflare Access fronts the hostname gives the full account.
Push has no equivalent problem. The reason is structural rather than a workaround. The engine is the client on a push delivery. It opens the connection to your SIEM, and your SIEM never opens a connection to the engine.
Cloudflare Access protects a hostname by gating inbound requests to it. A push delivery is not an inbound request to your console hostname, so an Access policy has nothing on that hostname to gate. You add no service token and you carve out no Bypass. You do not need the split-topology arrangement the pull page describes as its last resort. What you take on instead is a stored secret and an outbound destination you control, held to the security posture below.
Prefer push for an Access-fronted deployment for one reason: a perimeter that gates only inbound traffic cannot block it.
Which format for which SIEM
A push destination is four choices. The sink says where the batch goes. The format says what shape the body takes. The auth is whatever that sink needs. The fourth is whether the destination is enabled.
The engine shapes every batch to your format and delivers it over your sink. Eight body formats across three sinks reach the mainstream SIEMs. The table maps each SIEM to the format and sink to start with. The sinks and their caveats follow it.
| SIEM | Format | Sink | Notes |
|---|---|---|---|
| Splunk | splunk-hec | http | HTTP Event Collector, Authorization: Splunk <token> |
| CrowdStrike Falcon Next-Gen SIEM | splunk-hec | http | Its HEC-compatible connector, Authorization: Bearer <token> |
| Cribl | splunk-hec | http | Cribl’s HEC-style source; it also polls the pull feed if you prefer |
| Sumo Logic | splunk-hec | http | HEC-compatible HTTP source; pull-capable too |
| Panther | splunk-hec or json-array | http | HTTP log source; use json-array if it wants a bare array. Needs a schema/log-type step in Panther before fields normalise |
| Rapid7 InsightIDR | splunk-hec | http | HEC-style HTTP intake |
| Datadog | datadog | http | DD-API-KEY: <key>; push only, its hosted pull cannot carry the feed’s custom header |
| Elastic | datadog or json-array | http | The Datadog body flattens cleanly; json-array suits an array-splitting input |
| Google Security Operations | ndjson | http | One audit event per line. Needs a schema/log-type step in Google Security Operations before fields normalise |
| Stellar Cyber | ndjson or json-array | http | Generic HTTP intake that splits on newlines or an array |
| Any generic HTTPS collector | ndjson | http | The default format; splits N events out of one body |
| Graylog | gelf | http | Graylog’s GELF HTTP input |
| QRadar | cef or leef | syslog-tls | Needs a one-time Universal CEF (or LEEF) log source added before its DSM parses the stream; a real but small step, not zero-touch. Only over real syslog, not an HTTPS POST (see the sinks below) |
| LogRhythm | cef | syslog-tls | No generic CEF source ships in LogRhythm: you build a bespoke Log Source Type plus an MPE (Message Processing Engine) rule yourself, over syslog TLS. Meaningfully more work than QRadar’s one-time log source add, closer to writing a parser than clicking a wizard |
| FortiSIEM | cef | syslog-tls | Reported, community sources rather than FortiSIEM’s own primary docs, to parse CEF automatically over syslog TLS; treat as plausible, not vendor-confirmed |
| Securonix | cef | syslog-tls | Needs a schema/log-type step in Securonix before the incoming stream’s fields normalise, not automatic on arrival |
| Wazuh | ndjson | s3 | The Wazuh tile fixes both, so the engine drops NDJSON batches into a bucket Wazuh already reads |
| Microsoft Sentinel | pull | pull feed | Push cannot reach it; deploy the Codeless connector, no CEF needed (below) |
| Exabeam | pull | pull feed | Its REST API collector polls the feed |
| Cortex (Palo Alto) | pull | pull feed | Polls the feed with a custom header |
| Logpoint | pull | pull feed | Polls the feed |
Nothing about a specific vendor is hard-coded beyond these envelope shapes. Any collector that accepts one of these formats over the matching sink is a candidate target, not only the named products. The last four rows are the SIEMs that take the feed by pull rather than push. The pull page owns their mechanics, and Microsoft Sentinel has its own note below.
The eighth format is raw-json, the Downpipes wrapper. It is one JSON object carrying the whole batch and its metadata, rather than a per-event shape. No named SIEM needs it, which is why the table above leaves it out. Choose it for a collector of your own that wants the batch verbatim. Payload shapes on the wire below gives the exact bytes each format sends.
The three sinks
A sink is where a shaped batch is delivered. The format decides the body; the sink decides the transport and the auth.
| Sink | What the engine does | Auth |
|---|---|---|
http (default) | POSTs the shaped batch to your HTTPS endpoint with one named auth header | A sealed header secret, or a URL-path token (below) |
s3 | Writes each drain batch as one object, shaped by your chosen format, into an S3-compatible bucket your SIEM already reads, keyed by a prefix, the batch’s sequence range and the format’s own extension | A sealed S3 access key over a signed request (SigV4), no header |
syslog-tls | Opens a TLS connection to your SIEM’s syslog listener and writes each event as an RFC 5424 record whose message is the CEF or LEEF line | Network-level, so no stored secret |
The URL-token option
The Carry the auth token in the URL option lives on the http sink. A few HTTP intakes read the secret from the URL path rather than from a header. Devo is the common one. Turn the option on. The engine then adds the sealed secret to your endpoint as the last path segment and sends no auth header.
The token stays sealed at rest. It never appears in a log line, on the delivery trail, or in the redacted view. The trail records the host only.
The s3 sink is the universal fallback and the native path for Wazuh. Drop batches into a bucket and any SIEM that reads an object store can take the feed. It uses the same signed-request S3 client as a backup destination. The syslog-tls sink exists for one specific reason, which the next warning gives.
Format and sink are not free of each other. cef and leef require the syslog-tls sink, and syslog-tls carries nothing else. Eight formats and three sinks make twenty-four combinations. Fourteen are deliverable: the six JSON-shaped formats over http or s3, plus cef or leef over syslog-tls.
The console does not offer the other ten. The engine refuses them with a 400 that names the format, so an API client or a replayed approval cannot reach them either (pushSinkFormatError, engine/src/sched/scheduler-do-limits.ts). The warning below gives the reason. No SIEM auto-parses a CEF or LEEF line delivered over HTTPS or dropped into a bucket. An RFC 5424 record carries a CEF or LEEF line and nothing else.
Setting up the S3 sink
The s3 sink drops objects into a bucket your SIEM already reads, so its fields describe that bucket and a credential to write to it. The bucket must already exist; the engine writes into it and never creates it.
The S3 endpoint URL is the S3-compatible endpoint the bucket lives on. Use https://s3.<region>.amazonaws.com for Amazon S3, or https://<account>.r2.cloudflarestorage.com for Cloudflare R2. The engine screens it like the HTTP endpoint, so it refuses a private, loopback or cloud-metadata address. From engine 0.3.6, when the push sends, the engine also refuses a host name that resolves to such an address. The Bucket is that bucket’s name.
The Region is the bucket’s real region, for example ap-southeast-2. Use auto for R2. Amazon S3 rejects auto and answers a bucket in another region with an HTTP 301, so set the real region there.
The Key prefix is an optional folder path inside the bucket, to keep the audit objects together. Leaving it blank does not write at the bucket root. The engine falls back to a downpipes-audit prefix, so the objects land under downpipes-audit/ unless you set your own.
The Access Key ID and secret access key are an S3-API credential for that bucket. It is the same kind of credential a backup destination uses. Mint it wherever the bucket lives, not in downpipes.
For a Cloudflare R2 bucket, create an R2 API token in the Cloudflare dashboard under R2, API, Manage API Tokens. It gives you the S3-compatible access key id and secret. For Amazon S3, create an IAM user or role access key. For another S3-compatible store, use that provider’s access-key screen.
The object is shaped by the format you chose, not by the sink. An ndjson destination drops one raw event per line. A json-array destination drops a bare array. A splunk-hec destination drops HEC envelopes. Each writes a single object per batch.
The key carries an extension describing how to read those bytes: .json for the three whole-document formats, .ndjson for the three newline-joined ones. The PUT signs no content type, so the key is all a bucket consumer sees before it opens the object (objectExtensionForFormat, engine/src/cron/siem-push-shape.ts). The extension describes the wire shape rather than naming the format, so raw-json, json-array and datadog objects all end .json.
The engine only ever writes to this sink. It PUTs one object per batch, keyed by your prefix and the batch’s sequence range. It never reads, lists or deletes (putSiemBatchToS3, engine/src/cron/siem-push-pass.ts).
So the credential needs exactly one permission: s3:PutObject on that bucket, scoped to your prefix if you want it tight. It does not need read, list or delete. Giving it more than PutObject grants access this sink never uses.
The push sink has no separate write-probe, unlike a backup destination. A key that cannot PUT surfaces as a delivery failure on the trail rather than a refusal at save time. Confirm the key can write to the bucket before you rely on the feed.
CEF and LEEF need real syslog, and syslog arriving is not the same as being parsed
A plain HTTPS POST of a CEF or LEEF line is never read by the built-in log-source parsers (the DSMs) of QRadar or LogRhythm at all. Those parsers only look at real syslog transport, an RFC 5424 record over TCP with TLS on port 6514, which is why cef and leef are confined to the syslog-tls sink: the console does not offer the other pairings and the engine refuses them. Arriving over syslog TLS is necessary on both, but it is not by itself sufficient: QRadar still needs the one-time Universal CEF or LEEF log source added before its DSM parses the stream, and LogRhythm has no generic CEF source at all, so you build a bespoke Log Source Type and an MPE rule yourself, as the table above says. Transport is not parsing; budget for the extra step regardless of which sink carries the bytes.
Two parts of that path can only be confirmed from your own deployment. First, Cloudflare does not document whether a Worker can open an outbound socket to port 6514. It documents port 25 and its own ranges as blocked, and stays silent on 514 and 6514. Confirm the connection lands from your engine before you rely on it.
Second, whether a given SIEM parses the exact CEF or LEEF line the engine emits depends on how you configure that SIEM’s log source. Only a live send to your own instance confirms it.
FortiSIEM sits apart from the rest of this table. Its syslog-tls auto-parse rests on community reports rather than FortiSIEM’s own primary documentation. Treat FortiSIEM as plausible, not confirmed. Securonix’s syslog-tls path is different, a documented schema step rather than a guess, as the table above says. Neither has an HTTPS alternative here, because CEF over http is a pairing the engine refuses.
Setting up the syslog sink
The syslog-tls sink delivers to a syslog collector over TCP with TLS. It is the sink that carries the cef and leef formats, because nothing parses a CEF or LEEF line sent as a plain HTTP POST. The warning above explains why. It has two fields.
This sink carries cef or leef and nothing else, and a syslog destination saved with any other format is refused rather than converted. A syslog record’s message is one CEF or LEEF line, so it has no HEC, NDJSON or GELF shape to carry. Quietly substituting CEF would send your SIEM plausible bytes under the wrong parser, with nothing on the record to say so. Pick cef or leef above, or choose a different delivery.
The Syslog host is the hostname or address of your collector, a bare host with no scheme and no path, for example siem.example.com. It is required. The engine refuses a private, loopback or link-local address at save with a 400: it rejects an address such as 10.0.4.20, and an internal-only name such as siem.internal.example, because it runs at the Cloudflare edge and cannot reach a host on your private network. The same screen covers cloud-metadata addresses, and the sender checks the host again at send time. From engine 0.3.6 the sender also resolves the host name before it connects, and refuses a name that resolves to an internal address (syslog-internal-sink-resolved).
Being reachable is not the same as being trusted. The connection is implicit TLS. The engine validates your listener’s certificate against the public trust store like any other. There is no way to hand it a private CA bundle.
So a collector holding a certificate from your internal CA, or a self-signed one, fails the handshake. Nothing is written, and the trail records syslog-tls-untrusted. Issue that listener a certificate from a public CA for a name you control, or use the http or s3 sink instead.
The Port is a whole number from 1 to 65535. Leave it blank and it defaults to 6514, the RFC 5425 port for syslog over TLS.
Microsoft Sentinel takes the feed by pull, not push
Microsoft Sentinel is the one large SIEM this push cannot reach. Its log ingestion authenticates with OAuth2 and an HMAC-signed request. There is no static HTTP intake to point a push destination at, and it needs no CEF.
Sentinel consumes the same audit feed by pull instead, through a Codeless connector you deploy into your own Azure workspace. The deployable ARM package and its deploy guide ship in the engine repo under integrations/microsoft-sentinel/. The connector polls /support/audit-feed with a bearer token and the feed’s own nextAfterSeq cursor, exactly as the pull page describes. For the pull mechanics the connector rides, read wiring the audit feed into your SIEM.
Payload shapes on the wire
The underlying events are identical to the ones the pull feed serves, whichever format you choose. They carry the same redaction-safe, identity-bearing fields: seq, ts, actorEmail, actorSubject, actorMethod, sourceIp, action, outcome, target, prevHash and hash. Only the envelope around them changes. The default for a new destination is ndjson.
ndjson writes one raw audit event per line. A generic HTTP intake splits the body into N events, with no array or wrapper to unpick. This is the default.
{"seq":4097,"ts":"2026-07-05T02:14:08Z","action":"restore-approve","outcome":"success","actorEmail":"ops@acme.example","...":"..."}
{"seq":4098,"ts":"2026-07-05T02:14:09Z","action":"downpipe.update","outcome":"success","...":"..."}
json-array sends the same events as one bare array. It suits an intake that splits an array rather than newlines, such as Elastic or Panther.
[ { "seq": 4097, "action": "restore-approve", "outcome": "success", "...": "..." },
{ "seq": 4098, "action": "downpipe.update", "...": "..." } ]
splunk-hec sends one HEC event object per audit event, whitespace-separated in the one request body, because HEC accepts concatenated JSON objects. time is epoch seconds, and the audit event nests under event.
{"time":1718763248,"source":"downpipes","sourcetype":"downpipe:audit","event":{"seq":4097,"action":"restore-approve","...":"..."}}
{"time":1718763249,"source":"downpipes","sourcetype":"downpipe:audit","event":{"seq":4098,"...":"..."}}
datadog sends a JSON array with ddsource and service set, a status field, and the audit event flattened alongside a message string. Batches sit under Datadog’s 5 MB and 1000-item request limits.
Left unset, Datadog defaults an event to info. That would flatten a denied or failed action to the same severity as a success. The shaper always sets the status instead: a denied or failed outcome maps to error or warn, and a success maps to info. A failed or denied action reads at the right severity in Datadog, with nothing for you to configure.
[ { "ddsource": "downpipes", "service": "downpipe-engine", "status": "info", "message": "restore-approve success",
"seq": 4097, "action": "restore-approve", "outcome": "success", "...": "..." } ]
raw-json posts the pull feed’s own wrapper body. A collector that already parses the pull response can point at either mechanism with the same parser. It is selectable but is not the default, because no named SIEM needs the wrapper.
{ "kind": "downpipe-audit-feed", "v": 1, "afterSeq": 4096, "nextAfterSeq": 4220,
"headSeq": 4220, "headHash": "sha384:...", "count": 124, "events": [ { "seq": 4097, "...": "..." } ] }
cef, leef and gelf are the SIEM-specific shapes. cef emits an ArcSight CEF line per event, delivered over syslog-tls for QRadar and LogRhythm auto-parse. leef emits IBM QRadar’s LEEF 2.0 line. gelf emits a Graylog GELF object per line, for the GELF HTTP input.
A CEF line looks like this. The audit action is both the event class and the act key, and the timestamp is epoch milliseconds in rt:
CEF:0|Maelstrom AI|Downpipes|0.3.5|downpipe.update|Downpipe updated|1|rt=1783131453482 suser=ops@acme.example src=203.0.113.42 act=downpipe.update outcome=success cs3Label=targetKind cs3=downpipe cs4Label=targetId cs4=dp_8f2a1
The engine escapes or substitutes every field value it places into a CEF, LEEF or GELF line. A crafted value cannot break the format’s grammar or forge a second event, and neither can an actor email or a target name holding a delimiter or a newline.
GELF is JSON-encoded, so it is safe by construction. CEF escapes the reserved characters. LEEF has no escape mechanism, so the engine replaces any character that would carry its delimiter with an underscore.
Every shaped batch is capped under the tightest target’s per-request limit (Datadog’s 1000 events or 5 MB, whichever comes first), so a large backlog sends across several ticks rather than as one oversized request.
Setting up the push destination
One requirement decides whether a destination can deliver at all, on all three sinks. Know it before you paste anything into the form. The host you name must present a TLS certificate that chains to a publicly trusted root and matches the hostname you entered.
Verification happens before the request is sent. So a self-signed certificate, an expired one, a chain missing its intermediate, or a certificate issued for another name each refuse the connection while your auth secret is still unused. The engine offers no way to skip verification, and it will not. An audit trail delivered over a connection nobody authenticated is not evidence of anything.
This matters because the failure lands at the same moment in setup that a wrong token does. It is not an auth fault. The credential was never offered to the endpoint, so re-minting the token, re-pasting the secret, or changing the header name will not move it. The fix is on the endpoint’s side, or on a different endpoint.
Where the certificate fault is named
On the http sink the delivery trail records the reason network-tls, which the console expands into the certificate sentence rather than showing you the slug, and GET /admin/push carries the same code on the attempt, so a support pack shows it too. The delivery runtime answers a certificate it refuses with a synthesised 525 or 526 status rather than throwing, and the engine classifies both as network-tls, not as your SIEM erroring.
The syslog-tls sink opens its socket against the same public trust store and names its own handshake failure syslog-tls-untrusted. The s3 sink reaches your bucket’s endpoint over HTTPS, so its endpoint carries the same requirement.
An Owner configures the destination
Where you choose the sink and the format depends on the tile you start from. A named vendor’s tile fixes both to the pair that vendor expects and shows no picker for either. There the choice is already made, and you supply the sink’s details only.
The Custom endpoint tile leaves both to you. Choose the sink: an HTTPS endpoint, an S3 bucket, or a syslog host. Choose the format, which defaults to
ndjson. Then supply the sink’s details.For the HTTPS sink, that means the endpoint URL and the auth header your SIEM expects, or the URL-token option, with the secret pasted once. The auth header name defaults to
Authorizationwhen you leave it blank. Otherwise it takes any valid HTTP header name of up to 100 characters, for exampleDD-API-KEY. The engine refuses a hop-by-hop header, and the URL-token option sends no header at all. For the S3 sink, it means the bucket, region and endpoint plus the access key, sealed the same way.A Splunk HEC destination needs the literal
Splunk <token>string: the word “Splunk”, one space, then the token. HEC treats that prefix as part of the authentication scheme rather than decoration, so the token alone will not authenticate. From Splunk’s own tile on the Integrations screen, the console refuses a value without the prefix as you leave the box. It does not accept it and leave you with a 401 that reads exactly like a revoked token.That check belongs to the vendor rather than to the wire format. CrowdStrike Falcon Next-Gen SIEM takes the same
splunk-hecshape over the same HTTPS sink and issues a plain bearer token, so its own tile asks for the token alone.This step is owner-only and requires a fresh step-up re-authentication. Once a second owner exists on the account, it is dual-control gated the same way setting a backup destination is, because the engine reconstructs the secret on every delivery rather than verifying it once. A non-owner sees the control disabled with the reason, never hidden.
A stored secret is sealed at rest the way a destination credential is, and it is never read back. Changing it later means re-entering it, not editing it.
Send a test event
The Test send button pushes one synthetic, audit-shaped event through the same egress-secure path the real drain uses. It reports the sink’s outcome: a rejection from your SIEM, a timeout, a refused connection, or a reason from the egress guard. A test send never advances the delivery cursor, so it cannot be mistaken for real drain progress.
A pass here proves the wiring, and that is all it proves. It says the endpoint was reachable, the certificate was trusted, and the credential was accepted. On the HTTP sink it does not prove the event was indexed: a Splunk HEC refusal can arrive inside a
200. At-least-once delivery sets out why.Confirm the synthetic event in your SIEM before you treat the destination as working. Its own
detailfield labels it as a synthetic test event, so it is easy to find and easy to tell apart from a real engine action.Enable it
With the wiring proven, enable the destination. From the next scheduler tick onward the engine reads events above the cursor. It shapes them to your format and delivers the batch over your sink.
Watch the delivery trail and the cursor lag
The panel shows recent attempts and how far the cursor sits behind the current head. Each attempt carries an outcome and a coarse reason, never a response body. A growing lag alongside failing attempts means your sink is unreachable or rejecting the batch. The trail tells you which.
Replacing the destination follows the same owner path. When the replace takes effect, the engine stops using the old settings and the old secret immediately. Clearing it takes the owner gate and step-up, but no second-owner approval, because closing an outbound path is the safe direction.
Change control is the separate axis, and clearing is still change-controlled. With Require Change Number on, the clear asks for a change reference and the engine refuses it without one.
Security posture
The push destination is an egress of identity-bearing audit data, a stored secret, and a sink you control. It carries three protections, and each matches one the engine applies elsewhere.
The auth secret is sealed at rest and never returned. What the engine hands back, in the console or from GET /admin/push, carries the sink, the format and the endpoint. For the S3 sink that endpoint is the bucket, region, endpoint and prefix. It also carries the header name, whether the destination is enabled, who set it and when, and the delivery trail.
It never carries the header secret, the S3 access key, a URL-path token, or a hash of any of them. An S3 access key is sealed under its own distinct wrapping, separate from the header secret, so one sink’s key can never be resolved through another’s path. Rotation is re-enter rather than edit, exactly as a pull credential’s rotation is revoke and re-mint.
Opening or replacing the destination is owner-only, step-up-authenticated, and dual-control gated once a second owner exists. That is the same authority path a backup destination follows, described in full at four-eyes change control. Clearing the destination is owner-only and step-up-authenticated too, but carries no second-owner approval, because closing the path is the safe direction.
All three directions are change-controlled. Under Require Change Number, the set, the replace and the clear each take a change reference. The clear enforces it at its own engine route rather than through the owner-action gate, because it is not a dual-control operation.
Closing an egress at speed is when you are least likely to hold a CAB number. Use Emergency Change mode with a justification rather than delaying the close. See change control.
The outbound call inherits every egress guard the alert webhook has. An HTTP endpoint must be https; a private, loopback, link-local, or cloud-metadata address is refused by default, at both save time and send time; a redirect is not followed; and the request carries a timeout. The response body is read for one format only, Splunk HEC, because HEC states its acceptance in the body rather than in the status; that read is bounded at 16 KiB taken off the socket, so an endpoint cannot make the engine buffer an unbounded reply, and only a closed set of classifications is kept, never the endpoint’s own text. Every other format’s response is discarded unread.
The S3 endpoint is screened by the same rules before any request, and the syslog sink opens an outbound-only TLS socket to the host you name (whether a Worker can reach port 6514 is one of the two points you confirm yourself, as the warning above says). The send uses the engine’s egress-screened fetch and signed-request clients, so no outbound path bypasses these guards. The screening itself, including the one deliberate override for a private sink and the one gap it does not close, is described in full at securing notification webhooks; the push destination is held to the same standard.
A stolen ambient session cannot redirect the audit egress
Every route that sets, replaces, or clears the push destination requires a fresh step-up re-authentication, the same requirement every destination-mutating route carries. A session cookie alone is not enough to point your audit trail at a new endpoint.
At-least-once delivery, not exactly-once
The drain is at-least-once. On each scheduler tick, if the destination is enabled, the engine reads events with a sequence above the last delivered one. It shapes them and delivers the batch once.
A success advances the cursor to the last delivered sequence and records a success on the trail. Success means a 2xx for the HTTP sink, or for Splunk HEC a 2xx whose body declares code 0. For the S3 sink it means a completed PUT, and for the syslog sink a batch written and flushed.
On anything else the engine leaves the cursor where it was. It records the failure and a coarse reason, then retries on a later tick with backoff, so a persistently unreachable sink is not hammered every tick.
One gap remains. The engine can succeed and then fail before it persists the cursor, and the same batch then sends again on the next tick. Delivery is not exactly-once.
Every audit event carries a stable seq and a hash, and either is a safe dedup key on your SIEM side. Index on seq or on hash. A re-sent event then lands as a duplicate you can drop rather than a second incident.
On the HTTP sink, a 2xx is acceptance, not proof of indexing
The distinction matters most on Splunk HEC. HEC answers 200 with a JSON body that carries its own status code, and several refusals arrive that way rather than as an HTTP error. On Splunk Cloud, {"text":"Incorrect index","code":7} arrives under HTTP 200.
The engine reads that body. A 2xx whose HEC code is anything other than 0 is a refusal. The engine records the batch as a failure with the refusal named, leaves the cursor where it was, and re-exports and re-sends the same events on the next tick. A persistent HEC 400 gets the same at-least-once treatment, so a refused batch is retried rather than skipped.
Two limits remain.
A body the engine could not read never invents a failure. If the answer is empty, larger than the 16 KiB read bound, or not the envelope HEC documents, the batch keeps the verdict its HTTP status gave it and the trail carries the unreadable answer as a caveat beside it. A healthy sink is never retry-stormed on the strength of a reply nobody could parse, and an absent or non-numeric code is treated as unreadable rather than quietly read as 0.
Acceptance is still not indexing. Code 0 means HEC parsed and queued the batch; events behind it can still be dropped further in by an index-time nullQueue transform, a blocked indexing queue, or a full disk, and none of those are visible in the response. Confirming that needs Splunk’s indexer acknowledgement, which the engine does not implement, so the cursor does advance on a code 0 that Splunk later drops downstream.
Set the index on the HEC token, not in downpipes. The engine sends time, source, sourcetype and event, and does not send an index key, so the index is whichever one the token is configured to write to. There is no index field on the destination and nothing in the console can correct a mismatch. Where HEC itself refuses the index, it answers code 7 and the trail shows hec-declined-index rather than a success, so the misconfiguration is visible. Where the index exists and accepts the write but your own routing discards it afterwards, that is the downstream case above and the trail still shows a success.
Confirm the events arrived, once, in Splunk. A green delivery trail says the request was accepted. Search your index for sourcetype="downpipe:audit" after you enable the destination and confirm the count moves, and alert on the absence of events rather than relying on the trail alone. That is the same discipline the pull feed’s headHash gives you, applied to the push direction.
The other sinks do not have this gap. The S3 PUT and the syslog write are confirmed by the transport itself, with nothing to carry a refusal past a successful status.
The drain runs on the scheduler’s own tick cadence, minutes rather than per event. It is built for an audit trail, not a real-time stream, so treat a few minutes of lag as normal rather than a fault.
When a delivery fails, and what the reason means
The delivery trail records a coarse reason per failure, never the error text, so nothing about your endpoint or its certificate is retained. The console expands that reason into what to check. These are the ones worth knowing before you set a destination up.
The left column is the code the trail stores and the API returns; the console shows the sentence beside it rather than the slug.
| Reason on the trail | What it means, and where to look |
|---|---|
network-tls (certificate not trusted) | The TLS handshake failed, so nothing was sent and the credential was never offered. The certificate your endpoint serves is self-signed, expired, missing an intermediate, or issued for a different hostname. Check it on the exact host and port you entered, not on 443 |
network-dns (hostname did not resolve) | The endpoint’s host does not exist. Check the spelling, and check the ingest hostname exists on your plan at all |
network-reset (connection refused or dropped) | Nothing accepted the connection on that port from your Cloudflare account |
timeout (timed out) | The endpoint did not answer inside the send timeout |
http-auth (credential rejected) | A 401 or 403. The token or key is wrong, expired, or lacks permission. This is not a network fault |
http-rate-limited (throttled) | A 429. The credential is fine and the batch is retried on a later tick |
http-bad-request (request shape refused) | A 400. The format you selected may not be the one this endpoint accepts |
Splunk HEC adds its own reasons, because its refusals arrive in the body. The first six are refusals HEC declared, and each holds the cursor so the batch is re-sent. The last three say only that its answer could not be read, and those ride beside the verdict the HTTP status already gave.
| Reason on the trail | What it means, and where to look |
|---|---|
hec-declined-index | HEC refused the index. The token’s default index is missing, or the token may not write to it. Fix it on the token, not here |
hec-declined-token | HEC refused the credential or the way it was presented. The scheme is a literal, case-sensitive Splunk , so Bearer or a bare token is refused even when the token itself is valid |
hec-declined-format | HEC would not take the body as data. This one is ours to fix, not yours; raise it with support |
hec-declined-channel | HEC refused the request channel or the acknowledgement configuration on that token |
hec-declined-busy | The indexer queue is full. This is real backpressure and the retry is the right answer; no action is needed unless it persists |
hec-declined-other | HEC declared a non-zero code this engine does not recognise, for example a code a later Splunk release adds. The batch is still held and re-sent |
hec-body-absent | The reply carried no body, so HEC’s own code could not be read |
hec-body-oversized | The reply was past the 16 KiB read bound, so it was never parsed |
hec-body-unparseable | The reply was not the envelope HEC documents, or its code was not a number |
A vendor's ingest port can serve a different certificate than its web port
A stack that looks healthy in a browser is not evidence that its ingest port will be accepted, because the two can serve different certificates. Splunk Cloud is one such case. The customer-facing HEC endpoint is a dedicated ingest hostname, http-inputs-<stack>.splunkcloud.com, and that is the host to configure where your plan publishes it. Where it does not resolve, HEC is reached on the stack’s own host at port 8088, and on a Splunk Cloud trial stack that port can serve splunkd’s stock self-signed certificate (CN=SplunkServerDefaultCert, issued by CN=SplunkCommonCA) while 443 on the same host serves a valid CA-issued chain. Certificate verification refuses the self-signed port before any HTTP is exchanged, so no batch reaches it.
Read that as the shape of the problem rather than a description of every stack: check what your own ingest host and port present, on that host and that port. If it is splunkd’s default certificate, the remedy is on the Splunk side, by installing a certificate for the ingest listener that chains to a public CA. Splunk documents this, and Splunk Cloud customers can raise it with Splunk support. The engine offers no way to skip verification.
Where the runtime does not disclose why a connection failed, the trail says the connection failed and does not guess. For an HTTPS endpoint the certificate chain and the hostname are the two things to check first.
What the push carries, and what it does not
The events pushed out are the same redaction-safe records the pull feed serves. The push carries operator and admin identity on purpose: member emails, source IPs, roles, and approver emails, because attributing who did what is the point of an audit trail. It carries no customer backup data, no keys, and no secret values.
| Field | Example | What it is |
|---|---|---|
actorEmail | ops@acme.example | The verified display email of the human who acted |
actorSubject | https://acme.cloudflareaccess.com|01J9Z7M3QF8K2WX4P6R0V5T1AB | The stable opaque principal the engine authorises on |
sourceIp | 203.0.113.7 | The source address the engine saw |
The feed carries operator identity, on both paths
Whether you consume the audit trail by pull or by push, it carries member emails, source IPs, roles, and approver emails, so it is not free of personal data or identifying information. Apply your SIEM’s data-handling rules to it, the same as you would to any access log.
Where this fits
This page covers the push mechanics: the formats, the three sinks, the security posture, and the at-least-once semantics. For the pull counterpart, cursor by sequence, and the continuity verification the same events support, read wiring the audit feed into your SIEM. For the owner-minted credential the pull path uses instead of a stored secret, read owner-minted pull credentials. The wire-level reference for the pull endpoints is support diagnostics and audit-feed pull endpoints; the four push admin routes are catalogued alongside every other /admin route in the admin API endpoint catalogue.
For the egress screening the outbound call inherits in full, read securing notification webhooks. For Microsoft Sentinel, deploy the Codeless connector shipped under integrations/microsoft-sentinel/ in the engine repo and consume the feed by pull. For the audit chain itself and its on-screen export, read the audit log.
Last updated .