# Flow Logs

**Minimum versions:** macOS 1.5.19, iOS 1.5.19, Android 1.5.13, Windows GUI 1.5.16, Linux GUI 1.5.16, Windows Headless 1.5.11, Linux Headless 1.5.11, and Gateway 1.6.0.

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](/content/kb/audit-logs/flow#how-flow-uploads-are-authenticated/index.html) 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](https://github.com/firezone/firezone/issues/14416) 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](/content/kb/log-sinks/index.html), 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, `domain` also records which FQDN the destination address was resolved from.
- Each item in **`outers`** is a network path the flow was tunneled over, meaning the source and destination of the encrypted WireGuard packets on the underlying network. Each item contains `src_ip`, `src_port`, `dst_ip`, and `dst_port`. The source fields may be absent or `null` when 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](/content/kb/audit-logs/session/index.html), 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](/content/kb/audit-logs/flow#two-sided-reporting/index.html) 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=true` or 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_logs` target. Include them in the Client's output with `RUST_LOG=info,flow_logs=trace`.

## Example

A closed flow reported by a Client:

```json
{
  "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 Logs→Flow 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.
