Skip to content
GantryCD

Storage

GantryCD keeps three things in S3-compatible storage, configured independently so you can split them across buckets, accounts, or even providers:

ConcernHoldsPrefix
StateOpenTofu state (the Terraform S3 backend)STATE_*
LogsRun logs and plan analysisLOGS_*
ArtifactsPlan files passed from plan to applyARTIFACTS_*

Each concern takes the same shape of variables. The minimum for AWS S3 is a bucket, a region, and a role ARN:

STATE_S3_BUCKET=my-tfstate
STATE_S3_REGION=eu-west-1
STATE_S3_ROLE_ARN=arn:aws:iam::123456789012:role/gantrycd-state

(Repeat for LOGS_* and ARTIFACTS_*.)

Per-run credentials (the AWS path)

This is the security property worth understanding. GantryCD does not hand a runner the bucket keys. For each run it calls sts:AssumeRole on the concern’s ROLE_ARN with an inline session policy scoped to that run’s prefix. The runner receives temporary credentials that can touch only its own run’s state/logs/artifacts, and they expire with the run.

Provision one IAM role per concern (or a shared one) with:

  • a trust policy allowing the backend’s principal to assume it;
  • a permissions policy granting S3 access to the bucket — the minted credentials are the intersection of this and the per-run session policy;
  • MaxSessionDuration ≥ RUN_TTL + grace (mind the role-chaining cap noted in Configuration).

The assuming principal can be a static IAM user, an EKS IRSA role, or an instance profile — AssumeRole permits role chaining either way.

The backend itself also touches the state bucket directly (not via a per-run role): it archives and cleans up a stack’s state when the stack is deleted, and performs the State administration operations — force-unlock, overwrite, and restore-a-prior-version — for managed-backend stacks. Force-unlock and overwrite reuse the state concern’s existing access (s3:GetObject/s3:PutObject/s3:DeleteObject/s3:ListBucket), so they need no extra configuration. Version restore needs three more actions on the state bucket, granted to the state principal (not a per-run role): s3:GetBucketVersioning (to detect whether history is retained), s3:ListBucketVersions (to list a state object’s versions), and s3:GetObjectVersion (to read the version being restored). It also requires bucket versioning to be enabled on the state bucket (an infrastructure setting, e.g. an aws_s3_bucket_versioning resource). Without versioning, the Restore tab shows an “enable versioning” prompt; without the IAM actions the version list fails. All three operations are unavailable for unmanaged (bring-your-own) backends, whose state the backend can’t see.

The backend also touches the artifacts bucket directly, for local plans: a HeadObject to verify the uploaded source tarball at trigger time, and DeleteObject when the cleanup job reaps it. These use the artifacts concern’s own credentials, so make sure that principal (not just the assumed per-run role) carries s3:GetObject-level metadata access and s3:DeleteObject on the bucket. The CLI’s upload uses the same per-object STS minting as runners — s3:PutObject scoped to exactly the reserved key, valid for LOCAL_DEPLOY_UPLOAD_TTL (default 15m) — and reaches the bucket through ARTIFACTS_S3_PUBLIC_ENDPOINT when set, like off-network runners. LOCAL_DEPLOY_MAX_SOURCE_BYTES (default 512 MiB) caps the upload.

Non-AWS S3 (MinIO, RustFS)

Providers without an STS endpoint can’t mint per-run credentials. Opt into static keys explicitly, per concern:

STATE_S3_ENDPOINT=http://minio.internal:9000
STATE_S3_USE_PATH_STYLE=true
STATE_S3_SKIP_STS=true                 # hands the static keys straight to runners
STATE_S3_AWS_ACCESS_KEY_ID=…
STATE_S3_AWS_SECRET_ACCESS_KEY=…
STATE_S3_TFBACKEND_SKIP_AWS_VALIDATION=true   # STATE only — skips Terraform's AWS validation

SKIP_STS disables the per-run storage boundary: every runner receives the same static credential and can use all of its permissions across any organization, stack, or prefix it covers. Plans get the same storage access as applies, and the credential remains valid until it is rotated. Use this only within one trusted environment, such as a single-tenant installation, development setup, or bucket dedicated to one trust domain—not for multi-tenant or managed hosting. The backend logs a warning at startup for every affected storage concern.

Nothing is inferred from the mere presence of an endpoint; each non-AWS behavior is its own explicit flag.

Off-network runners

A GitHub Actions runner runs on GitHub’s infrastructure, not inside your network. If the credentials it receives carry a cluster-internal endpoint (http://minio.internal:9000), it can’t reach storage and every run fails at the first S3 call.

Set a publicly reachable address per concern:

LOGS_S3_PUBLIC_ENDPOINT=https://s3.example.com

The backend keeps dialing the internal ENDPOINT for its own reads; only the credentials handed to runners carry the public one. When unset it defaults to ENDPOINT, so single-network deployments need nothing extra.