Storage
GantryCD keeps three things in S3-compatible storage, configured independently so you can split them across buckets, accounts, or even providers:
| Concern | Holds | Prefix |
|---|---|---|
| State | OpenTofu state (the Terraform S3 backend) | STATE_* |
| Logs | Run logs and plan analysis | LOGS_* |
| Artifacts | Plan files passed from plan to apply | ARTIFACTS_* |
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.