- Just 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Deploy Terraform state backend / deploy (push) Successful in 21s
|
||
| .forgejo/workflows | ||
| cloudformation | ||
| .gitignore | ||
| justfile | ||
| lefthook.yml | ||
| LICENSE | ||
| README.md | ||
| renovate.json | ||
Terraform state bucket
This repository contains a CloudFormation template that provisions the AWS resources needed to use an S3 bucket as a Terraform remote state backend, shared by multiple projects.
- tries to solve the problem of also managing the S3 backend bucket via IaC and keeping it in a place that I can easily access and manage (and won't lose/forget easily)
- leverages Forgejo's OIDC identity provider to authenticate with AWS so no long-lived access keys are needed
Resources
The template creates:
- a KMS key used to encrypt the state files
- an S3 bucket used to store the state files
- an IAM managed policy granting the minimal permissions needed to use the backend from Terraform (attach it to the user/role that runs Terraform)
- Forgejo registered as an OIDC identity provider, plus an IAM role that Forgejo Actions assumes via OIDC — so the deploy pipeline uses short-lived credentials and there are no long-lived access keys to rotate
- optionally, an IAM role with
AdministratorAccessthat other Forgejo repositories can assume via the same OIDC provider (disabled by default — see Optional: OIDC admin role for other repositories)
Bootstrap
The deploy workflow bootstraps itself. On its first run it authenticates with static AWS access keys (no OIDC role exists yet) and creates the stack — which provisions the Forgejo OIDC provider and the deployer role. From then on every run authenticates through OIDC.
It uses the AWS_DEPLOYER_ROLE_ARN repository variable as the bootstrap signal:
- unset → not bootstrapped → deploy with access keys (creates the role)
- set → bootstrapped → deploy through OIDC
-
Provide AWS access keys
In Settings → Actions → Secrets add
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYfor an IAM principal that can create everything in the stack (S3 bucket, KMS key, IAM roles/policies and the OIDC provider) — e.g. the root account or an admin user. They are only needed for the first run. -
Set the Forgejo instance host
Use
env.FORGEJO_SERVER_URLby default, overridable withMY_FORGEJO_PUBLIC_URL— the host of the Forgejo instance, without scheme or trailing slash (e.g.codeberg.org). -
Run the workflow (bootstrap)
Trigger it with a push to
masteror a manual dispatch. SinceAWS_DEPLOYER_ROLE_ARNis not set yet, it runs in bootstrap mode with the access keys and creates the stack and deployer role. -
Point the workflow at the deployer role
Copy the
DeployerRoleArnvalue fromjust outputsand set it as the repository variableAWS_DEPLOYER_ROLE_ARN. -
Done
Every subsequent run authenticates through OIDC. You can now delete the
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYsecrets.
Alternative: you can also bootstrap locally with
just deploy <host> <repo> <bucket name>using credentials configured in your AWS profile, then setAWS_DEPLOYER_ROLE_ARNand let the workflow take over.Trust boundary: the deployer role manages the whole stack, including its own permissions policy. Anyone who can push to
ForgejoBranchcan therefore change what the role is allowed to do — protect that branch accordingly.
Optional: OIDC admin role for other repositories
The stack can also create an IAM role with AdministratorAccess that other Forgejo repositories assume through the same OIDC provider — useful when a project's CI/CD simply needs broad AWS access and you don't want to manage per-project permissions. It is off by default and is only created once you configure it.
-
Allow the repositories
Set the repository variable
FORGEJO_ADMIN_SUBJECTSto a comma-separated list of Forgejo OIDCsubclaims, each in the fullrepo:<owner>/<name>:ref:refs/heads/<branch>form (the branch is pinned per repo, so each entry trusts exactly one branch of one repo):repo:alex/projectA:ref:refs/heads/main,repo:alex/projectB:ref:refs/heads/main -
Deploy
Push to
master(or dispatch the workflow). The deploy step passes the variable to the stack only when it is set; the roleterraform-state-bucket-admin(and itsAdminRoleName/AdminRoleArnoutputs) are then created. Read the ARN:just outputs | grep -A 1 AdminRoleArn -
Assume it from the trusted repository
In each listed repository's workflow:
enable-openid-connect: true jobs: deploy: steps: - uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: <AdminRoleArn> aws-region: us-east-2
Trust boundary: this role grants full account access and is gated only by push access to a trusted branch of a listed repository — anyone who can push to one of those branches effectively has AWS admin. Protect those branches (required reviews, restricted pushers), and leave
FORGEJO_ADMIN_SUBJECTSunset to keep the role absent entirely.
Using the backend from Terraform
terraform {
backend "s3" {
bucket = "<bucket name>" # `just outputs | grep -A 1 BucketName` to fetch the bucket name
key = "project-name/terraform.tfstate"
region = "us-east-2"
use_lockfile = true
encrypt = true
kms_key_id = "alias/terraform-state"
}
}
Development
Prerequisites
- cfn-lint — CloudFormation template linter
- lefthook — Git hooks manager (already configured in
lefthook.yml)
Setup
Install the Git hooks so cfn-lint runs on every commit:
lefthook install
Lint manually
just lint
References
- Terraform S3 backend: https://developer.hashicorp.com/terraform/language/backend/s3
- Forgejo Actions OIDC: https://forgejo.org/docs/latest/user/actions/security-openid-connect/
- Forgejo Actions reference: https://forgejo.org/docs/next/user/actions/reference/
- AWS CloudFormation
AWS::S3::Bucket: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html