Skip to main content
Self-hosted Opal stores async data exports (large CSV/ZIP downloads) in an S3 or S3-compatible bucket. Cloud customers get this bucket automatically. If you self-host, you provision the bucket and provide credentials. The default path below is AWS S3. Opal also supports any S3-compatible object store — see Alternative: Use Google Cloud Storage (GCS) at the bottom for the GCS variant. This setup is optional. If you skip these steps, exports still work, but they’re tied to the browser session — they cancel if you navigate away or close the tab.

1. Create the bucket

Create a private bucket in the same region as your cluster, with:
  • All public access blocked
  • Server-side encryption (AES256 or KMS)
  • A bucket policy that denies any request where aws:SecureTransport=false
Use a dedicated bucket per Opal deployment (e.g. <your-org>-opal-exports). Don’t share it with other applications or other environments.
Do not add an S3 lifecycle expiration rule. Opal runs its own file cleanup job, and should be the only thing deleting objects in this bucket. A lifecycle policy might cause previously-saved exports to be orphaned.
Versioning and CORS are not required. Downloads are served through the Opal backend, not directly from the browser.

2. Create an IAM user and access key

Opal authenticates to the bucket with a static access key pair. This is a two-step process: create a dedicated IAM user with a scoped policy, then issue an access key for that user.

2a. Create the IAM user

Create a dedicated IAM user (e.g. opal-exports-service) and attach a policy with only these permissions:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::my-org-opal-exports/*"
    },
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::my-org-opal-exports"
    }
  ]
}

2b. Issue an access key

Generate one access key pair for the user (aws iam create-access-key --user-name opal-exports-service, or via the IAM console). Store both the access key ID and secret access key in your secret manager — you’ll paste them in step 3. If you provision the user via infrastructure-as-code, create the access key separately so the secret stays out of state. The secret access key is shown only once at creation time.
IAM Roles for Service Accounts (IRSA) and Workload Identity are not supported today. Static access keys are the only authentication method.

3. Update Opal Configuration

Pick the section that matches your install method.

KOTS

Open the admin console and find the Async Exports Storage section:
  1. Click Enable Async Exports.
  2. Storage type: leave as AWS S3 (default).
  3. Bucket name: my-org-opal-exports
  4. Region: e.g. us-east-2
  5. Access key ID and secret access key from step 2.
Save and deploy. KOTS encrypts the secret at rest.

Helm

Add the following to your helm values:
exportStorage:
  bucketName: my-org-opal-exports
  region: us-east-2
  accessKey: <access key id>
  secretKey: <secret access key>
Then run a helm upgrade to apply the configuration.

4. Verify

Run a query in Opal Query and trigger an export. You should get a download link and be able to download the exported data. Async exports are currently only supported via Opal Query.

Alternative: Use Google Cloud Storage (GCS)

If you run Opal on GCP and would rather not provision AWS infrastructure, GCS works via its interoperability mode — an S3-compatible XML API. The setup mirrors the AWS flow above with three differences:
  • You create a GCS bucket instead of an S3 bucket.
  • You generate an HMAC key for a service account instead of an IAM user access key.
  • You point Opal at GCS’ S3-compatible endpoint (https://storage.googleapis.com) instead of an AWS region.

1. Create the GCS bucket

In your GCP project, create a bucket with:
  • Location type: Region (close to your cluster)
  • Access control: Uniform (recommended)
  • Public access prevention: Enforced
  • Encryption: Google-managed (default)
Versioning is not required. GCS encrypts at rest and rejects plain HTTP by default — no extra policy needed.
Do not add a Lifecycle rule that deletes objects. Opal runs its own cleanup job and should be the only thing deleting objects in this bucket.

2. Create a service account and HMAC key

  1. IAM & AdminService accounts → create a dedicated service account (e.g. opal-exports-service). Skip the “grant access to project” step — we’ll scope to the bucket instead.
  2. Cloud Storage → your bucket → PermissionsGrant access → assign the service account the Storage Object Admin role on this bucket.
  3. Cloud StorageSettingsInteroperabilityCreate access key for service account → select your service account.
  4. Save the Access key (starts with GOOG1...) and Secret. The secret is shown only once.

3. Update Opal Configuration

KOTS:
  1. Click Enable Async Exports.
  2. Storage type: select S3-compatible (e.g. GCS).
  3. Bucket name: your GCS bucket name.
  4. Endpoint URL: https://storage.googleapis.com
  5. Access key ID and Secret access key: from step 2.
Save and deploy. Helm: Use endpoint instead of region:
exportStorage:
  bucketName: my-org-opal-exports
  endpoint: https://storage.googleapis.com
  accessKey: <GOOG1... HMAC access key>
  secretKey: <HMAC secret>
Then run a helm upgrade to apply the configuration.

4. Verify

Same as the AWS flow: run a query in Opal Query, trigger an export, and confirm a .zip appears in your GCS bucket under exports/<org-id>/<job-id>.zip.
Last modified on June 1, 2026