Flow Logs
Flow Logs record the network flows between Clients and Resources. Each flow captures the actor and Policy it was authorized under, when it started and ended, the network path it took, and the volume of data transferred in each direction. This makes Flow Logs the primary stream for incident response and forensic analysis, where the goal is to reconstruct network activity precisely.
Both sides of the tunnel report flows: the initiator is always the Client
that opened the connection, while the responder is the device that answered
for the Resource, either a Gateway or another Client for device pool
Resources. Each device batch-uploads its flow data to the control plane
through a dedicated ingest endpoint, but the authorization attribution,
meaning the actor, Policy, and Resource, comes from the control plane rather
than the device, so a device can't misattribute its own traffic.
How flow uploads are authenticated
explains how this works.
Flow Logs are enabled by default for all Policies except those targeting the Internet Resource. You must enable Flow Logs explicitly for those Policies. You can enable or disable Flow Logs on a per-Policy basis.
DNS queries are not tracked by Flow Logs. If DNS query logging would be useful for your organization, leave a comment on the tracking issue describing your use case.
Log IDs
Flow Log IDs start with f, and because the bits after the stream character
are random, they carry no ordering meaning. Order entries by flow_start
instead.
Flow lifecycle
A flow produces one entry that evolves over its lifetime: when the flow
opens, the entry records the inner network tuple and attribution, but
outers and the traffic counters are null. When it closes, the same entry
is updated in place with the complete outers array, flow_end,
last_packet, and the final packet and byte counts.
When delivered through a Log Sink, each flow is rendered as
a start event and an end event sharing the flow's 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. A flow that closes before
its open state was ever delivered still yields both events.
Two-sided reporting
Each flow is reported twice on purpose: the initiator and the responder track it independently, report it with the same inner tuple, and label directions from the initiator's perspective, so the two records describe the same traffic and can be joined on their inner tuple and time window.
Both records also name both devices. Their role and counters differ, and
their outers arrays may differ because NAT or a Relay can make the outer
path appear different from each endpoint.
This redundancy is the integrity mechanism for flow statistics. While the
ingest token signs a flow's attribution, the packet and byte counters come
from the reporting device itself, so trust in them rests on the two-sided
cross-check rather than on any single report. If the initiator and responder
report meaningfully different statistics for the same inner tuple within the
same flow_start to flow_end window, treat that as a suspicious event worth
a deeper look: one side may be tampering with its reports, or something
between the two devices is interfering with traffic.
Fields
Authorization
| Field | Description |
|---|---|
log_id | Unique ID of the entry. |
role | Which of the two devices reported this entry: initiator or responder. |
initiator_actor_id, initiator_actor_email, initiator_actor_name | The actor the flow is attributed to. |
policy_id | The Policy that authorized the flow. |
policy_authorization_id | The specific authorization grant for this flow. |
resource_id, resource_name, resource_address | The Resource the flow was authorized for. |
initiator_auth_provider_id | The authentication provider used to sign in. |
authorized_at, authorization_expires_at | When the authorization was granted and when it expires. |
The actor is always the initiating Client's actor. A Gateway has no actor of its own, and for device pool Resources the answering Client has an owner that is not recorded here.
Devices
| Field | Description |
|---|---|
initiator_device_id | The Client that opened the connection. |
responder_device_id | The device that answered: a Gateway, or another Client for device pool Resources. |
initiator_client_version | Version of the Firezone Client that opened the connection. |
initiator_device_os_name, initiator_device_os_version | Operating system of that Client. |
initiator_device_serial, initiator_device_uuid | Hardware identifiers of that Client, where available. |
Both devices are named on both entries, so a single entry tells you the two
ends of a flow without joining anything else. role tells you which of the
two wrote the entry.
Every field starting with initiator_ describes the initiating Client, even
on the responder's entry. The responder's own version and hardware details
are not recorded.
Network
| Field | Description |
|---|---|
protocol | tcp or udp. |
domain | The resolved domain, for DNS Resources. |
inner_src_ip, inner_src_port, inner_dst_ip, inner_dst_port | The tunneled connection's source and destination. |
outers | The WireGuard network paths, in the order they were observed. |
The inner 4-tuple and the outers array describe the same flow at two
different layers:
- The inner tuple describes the connection inside the tunnel, the
traffic as the application sees it: the source is the initiator's Firezone
tunnel address, and the destination is the address and port the Resource
was reached at. For DNS Resources,
domainalso records which FQDN the destination address was resolved from. - Each item in
outersis a network path the flow was tunneled over, meaning the source and destination of the encrypted WireGuard packets on the underlying network. Each item containssrc_ip,src_port,dst_ip, anddst_port. The source fields may be absent ornullwhen the source is unavailable, but they are always populated or omitted together. The addresses are typically each side's public, NAT-translated address or a Relay's address when a direct connection couldn't be established.
The outers array identifies the network paths that carried the traffic,
while the inner tuple identifies the endpoints that communicated over them.
Its items are ordered by when each path was observed, so a flow that changes
paths can contain more than one item. When present, the array contains at
least one path. outers is null while a flow is open and contains the
complete path history when the flow closes.
Both sides orient the inner tuple and each outer path from the initiator's
perspective, with the initiator side as the source. The inner tuples match,
but the outers arrays can disagree when NAT or a Relay sits between the
peers because each side observes the path from a different point.
Timing and counters
| Field | Description |
|---|---|
flow_start, flow_end | When the flow started and ended. |
last_packet | When the last packet was seen. |
tx_packets, tx_bytes | Traffic sent from the initiator to the responder. |
rx_packets, rx_bytes | Traffic sent from the responder back to the initiator. |
Like the network tuples, the counters are labeled from the initiator's
perspective on both entries. tx always means initiator to responder, no
matter which side reported it.
Flow Logs carry no geolocation data because, unlike the other streams, there is no subject IP to geolocate: the network tuples identify the endpoints directly. If you need location information for a device, correlate with Session Logs, which record a session every time a Client or Gateway connects to the control plane, including the connection's source IP and its resolved location.
How flow uploads are authenticated
When a Client is authorized to access a Resource, the control plane mints a per-authorization ingest token and hands one to each side of the tunnel. The token is stamped at authorization time with the complete attribution snapshot: both devices in the flow, which of the two this token reports as, the policy authorization, Policy, Resource, actor, authentication provider, and the authorization's validity window.
The token is the sole authenticator for flow uploads. When the portal ingests a batch, every attribution field is copied from the verified token's claims, and the device itself supplies only the network tuples, timing, and counters. This is what prevents spoofing: a device can only report flows for authorizations it actually holds, so it cannot attribute traffic to another device, another actor, or a Resource it was never authorized to access. The strongest claim a malicious device can make is a false statistic about its own authorized traffic, and that is exactly what two-sided reporting exists to catch.
Reporting and spooling
While connected to Firezone, devices spool flow records to disk locally and upload them in batches, and on disconnect or shutdown they perform a best-effort flush of whatever is pending. Flows that couldn't be uploaded, for example from a laptop that went offline, remain spooled and are uploaded on the next connection.
A spooled flow stays uploadable until its ingest token expires, 30 days past its authorization's expiry, after which it is cleaned up on the device automatically to prevent unbounded log growth.
Flow logs are spooled at the following locations:
| Platform | Spool location |
|---|---|
| Gateway | /var/lib/firezone/flow_logs |
| Linux Client, GUI and headless | /var/lib/dev.firezone.client/flow_logs |
| Windows Client, GUI and headless | C:\ProgramData\dev.firezone.client\flow_logs |
| macOS, iOS, and Android Clients | Managed inside the app's sandbox container. |
While the Gateway or Client is stopped, it's safe to delete the contents of the spool directory to reclaim space, though any flows not yet uploaded are lost and won't appear in your Flow Logs.
Emitting flow logs to STDOUT
Independent of the hosted feature, Gateways and headless Clients can print flow records directly to STDOUT. This is useful for feeding flows into your own log pipeline, and it's the only supported way to capture flows for the Internet Resource:
- Gateways: set
FIREZONE_FLOW_LOGS=trueor pass--flow-logs. The Gateway then tracks flows even when flow logs aren't enabled for your Policies, and emits each record to its log output. Flows tracked only because of this flag stay local, since uploading to Firezone is always controlled by the portal. - Headless Clients: flow records are emitted as structured log events
under the
flow_logstarget. Include them in the Client's output withRUST_LOG=info,flow_logs=trace.
Example
A closed flow reported by a Client:
{
"log_id": "f4e8a2c61b09d735e2a48b17",
"role": "initiator",
"initiator_actor_id": "7a1d9e3c-5b2f-4c8d-9e0f-1a2b3c4d5e6f",
"initiator_actor_name": "Riley Engineer",
"initiator_actor_email": "riley@company.com",
"policy_id": "0a83588e-0d92-4a4e-a856-4b8f2f4a2f8e",
"policy_authorization_id": "6c8e0a2b-4d6f-4a8c-9e0b-2d4f6a8c0e2d",
"resource_id": "5c8f6f6e-7c1a-4d8b-9c9e-1c2c3d4e5f60",
"resource_name": "GitLab",
"resource_address": "gitlab.company.com",
"initiator_auth_provider_id": "9d0c2f4e-8b6a-4c2d-8e1f-3a5b7c9d0e2f",
"authorized_at": "2026-07-14T09:20:00.000000Z",
"authorization_expires_at": "2026-07-14T19:20:00.000000Z",
"initiator_device_id": "2b4d6f8a-0c1e-4a3b-8d5f-7e9a1c3b5d7f",
"responder_device_id": "8f1c3e5a-6b2d-4e7c-9a0f-3c5e7a9b1d3f",
"initiator_client_version": "1.4.0",
"initiator_device_os_name": "iOS",
"initiator_device_os_version": "17.4",
"protocol": "tcp",
"domain": "gitlab.company.com",
"inner_src_ip": "100.64.0.1",
"inner_src_port": 54321,
"inner_dst_ip": "10.0.0.5",
"inner_dst_port": 443,
"outers": [
{
"src_ip": "203.0.113.10",
"src_port": 51820,
"dst_ip": "198.51.100.5",
"dst_port": 51820
}
],
"flow_start": "2026-07-14T09:25:31.000000Z",
"flow_end": "2026-07-14T09:26:01.000000Z",
"last_packet": "2026-07-14T09:26:01.000000Z",
"rx_packets": 100,
"tx_packets": 80,
"rx_bytes": 102400,
"tx_bytes": 20480
}
Viewing and retention
Flow Logs live under LogsFlow Logs in the admin
portal. You can also query them via the REST API's /logs endpoint with
type=flow. Entries are kept for 90 days.
Need help? See all support options.