Ship logs to Elastic
Use this guide to index your Audit Logs into Elasticsearch. Elastic Cloud, Elastic Cloud Serverless, self-managed clusters, and compatible clusters such as OpenSearch are all supported.
Step 1: Create an Elasticsearch API key
Create a base64-encoded Elasticsearch API key with permission to manage index
templates and to write to the data stream Firezone will use, which
defaults to logs-firezone-default. In Kibana this is under Stack Management → API
keys.
Step 2: Create the Log Sink in Firezone
In the admin portal, go to SettingsLog Sinks, click Add, and select Elastic. Configure the sink:
| Field | Description |
|---|---|
| Name | A name to identify this log sink. |
| Endpoint URL | Your cluster's Elasticsearch endpoint, such as https://my-deployment.es.us-east-1.aws.elastic-cloud.com. |
| API Key | A base64-encoded Elasticsearch API key with permission to manage index templates and write to the data stream. |
| Data stream | The data stream to append events to. Firezone creates it on first delivery. Manage retention with the stream's lifecycle in Kibana. Defaults to logs-firezone-default. |
The endpoint URL must be HTTPS, and hostnames that resolve to private or
reserved IP addresses are rejected. A trailing /_bulk is stripped if you
paste one. The data stream name must be lowercase and may contain digits,
., _, and -.
Then choose which Log streams to deliver, all four of which are enabled by default, and optionally check Deliver existing logs to backfill logs recorded before the sink was created.
Deliver existing logs can only be selected when you create the sink. It can't be turned on later for an existing sink.
Click Create. Before the first delivery, Firezone creates an index
template with priority 500, the data stream, and field mappings that index
Firezone event fields as keyword/flattened with date detection disabled.
You don't need to define any of these yourself.
Delivery format
Firezone POSTs to <Endpoint URL>/_bulk as NDJSON, using the create actions
that data streams require, with the event's log_id as the document _id:
{"create":{"_index":"logs-firezone-default","_id":"c0654d20aa1b4a0000000002"}}
{"@timestamp":"2026-07-14T09:26:31.114Z","message":"firezone change c0654d20aa1b4a0000000002","stream":"change","firezone":{...}}
The full event is under the firezone field of each document:
{
"@timestamp": "2026-07-14T09:26:31.114Z",
"message": "firezone change c0654d20aa1b4a0000000002",
"stream": "change",
"firezone": {
"type": "change",
"log_id": "c0654d20aa1b4a0000000002",
"timestamp": "2026-07-14T09:26:31.114213Z",
"object": "policies",
"operation": "update",
"before": { "id": "0a83588e-…", "description": "Engineering → GitLab" },
"after": {
"id": "0a83588e-…",
"description": "Engineering → GitLab (MFA required)"
},
"subject": {
"actor_id": "e2f6a9c4-…",
"actor_name": "Jamie Admin",
"actor_email": "jamie@company.com",
"actor_type": "account_admin_user",
"auth_provider_id": "9d0c2f4e-…",
"ip": "203.0.113.44",
"ip_region": "US",
"ip_city": "San Francisco",
"ip_lat": 37.7749,
"ip_lon": -122.4194,
"user_agent": "Mozilla/5.0 …"
}
}
}
{
"@timestamp": "2026-07-14T09:26:31.114Z",
"message": "firezone session 53f2c8a91b7e4d20a6c19e04",
"stream": "session",
"firezone": {
"type": "session",
"log_id": "53f2c8a91b7e4d20a6c19e04",
"timestamp": "2026-07-14T09:26:31.114213Z",
"context": "client",
"subject": {
"actor_id": "7a1d9e3c-…",
"actor_name": "Riley Engineer",
"actor_email": "riley@company.com",
"actor_type": "account_user",
"auth_provider_id": "9d0c2f4e-…",
"device_id": "2b4d6f8a-…",
"token_id": "c4e6a8b0-…",
"ip": "203.0.113.44",
"ip_region": "US",
"ip_city": "San Francisco",
"ip_lat": 37.7749,
"ip_lon": -122.4194,
"user_agent": "Firezone/1.4.0 (macOS 15.5; arm64)"
}
}
}
{
"@timestamp": "2026-07-14T09:26:31.114Z",
"message": "firezone api_request a7b3e91c4d208f5a6e1b2c3d",
"stream": "api_request",
"firezone": {
"type": "api_request",
"log_id": "a7b3e91c4d208f5a6e1b2c3d",
"timestamp": "2026-07-14T09:26:31.114213Z",
"actor_id": "b8e0c2a4-…",
"api_token_id": "d0f2a4c6-…",
"method": "GET",
"path": "/policies",
"content_length": 0,
"request_id": "F8nMlbf6MUyJZUUABBzB9-yT",
"user_agent": "terraform-provider-firezone/0.4.1",
"ip": "203.0.113.44",
"ip_region": "US",
"ip_city": "San Francisco",
"ip_lat": 37.7749,
"ip_lon": -122.4194
}
}
Each flow is delivered as two events sharing its log_id. The start event
is suffixed with -s and carries no counters, while the end event is
suffixed with -e and carries the final totals, so the two documents get
distinct _ids:
{
"@timestamp": "2026-07-14T09:26:01.000Z",
"message": "firezone flow f4e8a2c61b09d735e2a48b17-e",
"stream": "flow",
"firezone": {
"type": "flow",
"log_id": "f4e8a2c61b09d735e2a48b17-e",
"timestamp": "2026-07-14T09:26:01.000000Z",
"flow_start": "2026-07-14T09:25:31.000000Z",
"flow_end": "2026-07-14T09:26:01.000000Z",
"last_packet": "2026-07-14T09:26:01.000000Z",
"device_id": "2b4d6f8a-…",
"role": "initiator",
"policy_authorization_id": "6c8e0a2b-…",
"policy_id": "0a83588e-…",
"resource_id": "5c8f6f6e-…",
"resource_name": "GitLab",
"resource_address": "gitlab.company.com",
"actor_id": "7a1d9e3c-…",
"actor_email": "riley@company.com",
"actor_name": "Riley Engineer",
"client_version": "1.4.0",
"device_os_name": "iOS",
"device_os_version": "17.4",
"protocol": "tcp",
"inner_src_ip": "100.64.0.1",
"inner_src_port": 54321,
"inner_dst_ip": "10.0.0.5",
"inner_dst_port": 443,
"outer_src_ip": "203.0.113.10",
"outer_src_port": 51820,
"outer_dst_ip": "198.51.100.5",
"outer_dst_port": 51820,
"domain": "gitlab.company.com",
"rx_packets": 100,
"tx_packets": 80,
"rx_bytes": 102400,
"tx_bytes": 20480
}
}
How Firezone responds to HTTP status codes
The bulk API reports per-document outcomes inside an HTTP 200, so Firezone inspects item-level results as well as the response status:
| Response | Behavior |
|---|---|
200 with no item errors | The batch is accepted and delivery advances. |
200 with item 409s | Accepted. The documents already exist, which is the _id-based idempotency working as intended. |
200 with item 429/503s | The cluster is overloaded, so the whole batch is retried. The _id prevents the retry from creating duplicate documents. |
200 with mapping-conflict item errors | The batch is bisected to isolate the rejected document. Delivery pauses at it, Firezone engineers are notified, and the data stream is rolled over as described below. No entries are skipped. |
200 with other item errors, for example a cluster block | Treated as transient and retried. If failures persist for 24 hours, the sink is disabled and admins are emailed. |
413 | The batch is split in half and retried to isolate an oversized document. |
429, 5xx | Retried every minute. Disabled after 24 hours of persistent failure. |
Other non-2xx, for example 401 | Treated as a configuration error: the sink is disabled immediately. |
Duplicates and self-healing
Redelivery does not normally create duplicate documents. Every document is
created with its log_id as its _id, so if Firezone delivers the same
batch twice, Elasticsearch rejects the second copy of each document as
already existing. The one exception is a retry that lands after the data
stream has rolled over to a new backing index, which can produce a duplicate
document, so deduplicate on firezone.log_id in queries if you need strict
uniqueness.
The sink also heals itself: if a document is rejected due to a mapping conflict, for example a manually edited mapping that no longer accepts Firezone's fields, Firezone automatically rolls the data stream over, at most once per hour, so that corrected mappings apply to a fresh backing index, and delivery of healthy documents continues past the conflict.
Recovering a failed sink
A sink showing Warning is retrying automatically, so unless the failures continue there's nothing you need to do. A sink showing Error has stopped delivering and needs attention: fix the cause , commonly an expired API key or missing privileges, then open the sink, click Edit, and click Save to re-enable it. Delivery picks up right where it left off, from the last acknowledged entry, and Deliver Now triggers a delivery immediately if you don't want to wait for the next cycle.
Deleting the sink deletes its delivery state but not the data stream or its
documents, so re-creating it starts fresh: the new sink delivers logs
recorded from its creation onward, plus a full backfill if Deliver
existing logs is selected. Thanks to _id-based deduplication, a backfill
into the same data stream won't duplicate documents that are still in the
current backing index.
Need help? See all support options.