Ship logs to Amazon S3

Use this guide to archive your Audit Logs to an Amazon S3 bucket as NDJSON objects. Firezone writes objects by assuming an IAM role in your AWS account. No static credentials are stored or exchanged.

Step 1: Start creating the Log Sink in Firezone

In the admin portal, go to SettingsLog Sinks, click Add, and select Amazon S3.

The form displays an External ID, a UUID generated by Firezone and pinned to this sink. The IAM role you create next must require it via an sts:ExternalId condition. The trust policy and snippets shown in the portal are already pinned to it, so complete the AWS setup and save without leaving the page.

Step 2: Create the bucket and IAM role in AWS

The portal provides the exact policies and scripts with your External ID and Firezone's AWS account ID filled in, so prefer copying from there. Choose your tool:

  1. In the S3 console, create a bucket or pick an existing one, and note its region.

  2. In IAM under Roles, choose Create role → Custom trust policy and paste:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": { "AWS": "arn:aws:iam::<firezone-aws-account-id>:root" },
          "Action": "sts:AssumeRole",
          "Condition": {
            "StringEquals": { "sts:ExternalId": "<sink-external-id>" }
          }
        }
      ]
    }
    

    Continue without adding permissions and give the role a name, for example firezone-logs.

  3. On the new role, choose Add permissions → Create inline policy → JSON and paste:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": "s3:PutObject",
          "Resource": "arn:aws:s3:::<bucket>/*"
        }
      ]
    }
    

    If you plan to set a key prefix on the sink, scope the resource to arn:aws:s3:::<bucket>/<key-prefix>/* instead.

  4. Note the role's ARN, shown at the top of the role's page.

Firezone verifies the External ID is actually enforced. Before the first delivery, it attempts to assume the role with a random External ID. If that succeeds, Firezone refuses to deliver and disables the sink with a configuration error. This protects you from a confused-deputy misconfiguration where any Firezone customer could write to your bucket.

Step 3: Configure the sink

Back in the Firezone form, fill in:

FieldDescription
NameA name to identify this log sink.
BucketThe name of the S3 bucket to write log objects to.
RegionThe AWS region the bucket is in, for example us-east-1.
Role ARNThe IAM role in your AWS account that Firezone assumes to write objects, for example arn:aws:iam::123456789012:role/firezone-logs.
Key PrefixOptional. A prefix added to every object key, for example firezone/logs.
External IDRead-only. Generated by Firezone and pinned to this sink.

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. Deliveries begin within about a minute.

Delivery format

Each delivered batch becomes one NDJSON object with one raw event per line and no envelope, written with content type application/x-ndjson to a deterministic key:

<key-prefix>/<stream>/<YYYY>/<MM>/<DD>/<first-seq>-<last-seq>.ndjson

For example: firezone/logs/session/2026/07/14/8123-8390.ndjson. The date comes from the first event in the batch.

One line of change/2026/07/14/4211-4211.ndjson:

{
  "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 …"
  }
}

How Firezone responds to HTTP status codes

ResponseBehavior
2xxThe object is written and delivery advances.
429, 500, 502, 503, 504Retried every minute, including S3 SlowDown. If failures persist for 24 hours, the sink is disabled and account admins are emailed.
413The batch is split in half and retried to isolate an oversized event.
Other 4xx such as 403 AccessDenied, STS errors, or a failed External ID checkTreated as a configuration error: the sink is disabled immediately. A region redirect is reported with the bucket's actual region so you can correct the Region field.

Duplicates and redelivery

Redelivery does not create duplicate objects in your bucket. Object keys are deterministic, so if Firezone delivers the same batch twice, the second delivery writes the same object to the same key with the same contents. Any pipeline that reads objects by key, or that deduplicates lines by log_id, is unaffected.

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 a deleted role, a trust policy missing the sts:ExternalId condition, or a bucket moved to another region, 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 objects in your bucket. Re-creating it starts fresh as a new sink with a new External ID, so update the role's trust policy to match before deliveries can resume. The new sink delivers logs recorded from its creation onward, plus a full backfill if Deliver existing logs is selected, and because backfilled objects are keyed by the same scheme, overlapping ranges overwrite cleanly.


Need help? See all support options.

Found a problem with this page? Open an issue
Last updated: July 20, 2026