Keep each environment's roles to its own state
Optional: hand this page to your coding agentThe steps work by hand too.Show the whole prompt
Read https://intentius.io/terragucci/guides/scope-state-access/.
In this repo, add oidc.roles to terragucci.yml with one glob per environment the roots show, using placeholder role ARNs, and run `npx terragucci config check`.
Write the IAM policy each role needs, from the state keys config check lists, as a file for me to review. Open a pull request.
Do not create a role, change a trust policy or touch a state.
Never apply, approve (a pull request review or `terragucci approve`), override a policy denial (`terragucci override`), use `--mode apply`, or merge; never touch `.chant/allowed_signers` or `chant/lifecycle`.Result
Section titled “Result”Each root runs every command, steps included, with its environment’s roles. In dev those are the dev roles, which your IAM policies limit to dev’s state keys.
| Where | What it shows |
|---|---|
terragucci config check |
each role, the roots that take it and the state keys it needs; a warning for each role that reaches another environment’s state |
| the plan and apply jobs | TERRAGUCCI_ROOT_ROLES, the stage’s roles by glob; each root’s binary gets its role as AWS_ROLE_ARN |
| the threat model | what a role per environment stops, and what it does not |
Prerequisites
Section titled “Prerequisites”| You need | Why |
|---|---|
oidc set up with an IAM OIDC provider for your forge |
each role trusts the forge’s token, as plan_role and apply_role do |
Roots with an s3 backend whose bucket and key are in the code |
config check reads the state keys from the backend blocks |
| Plain roots | a Terragrunt repo sets terragrunt.credentials instead, which works the same way per unit |
-
Name a pair of roles per environment, by the roots’ paths. The first glob a root matches wins; a root no glob matches takes
plan_roleandapply_role, which you can leave out when every root matches a glob.oidc:roles:"envs/dev/**": { plan: arn:aws:iam::444455556666:role/dev-plan, apply: arn:aws:iam::444455556666:role/dev-apply }"envs/prod/**": { plan: arn:aws:iam::111122223333:role/prod-plan, apply: arn:aws:iam::111122223333:role/prod-apply } -
Run
terragucci config check. It lists each role with the state keys its roots’ backends name:terragucci.yml: okapproval: ledger (the default)state access:arn:aws:iam::444455556666:role/dev-plan (plan, envs/dev/**): s3://acme-state/dev/app.tfstatearn:aws:iam::444455556666:role/dev-apply (apply, envs/dev/**): s3://acme-state/dev/app.tfstatearn:aws:iam::111122223333:role/prod-plan (plan, envs/prod/**): s3://acme-state/prod/app.tfstatearn:aws:iam::111122223333:role/prod-apply (apply, envs/prod/**): s3://acme-state/prod/app.tfstate -
Scope each role’s policy to those keys. The plan role only reads. With
use_lockfilethe apply role also writes and deletes the lock file beside the state it reads and writes:{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::acme-state", "Condition": { "StringLike": { "s3:prefix": ["prod/*"] } } },{ "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"], "Resource": ["arn:aws:s3:::acme-state/prod/app.tfstate", "arn:aws:s3:::acme-state/prod/app.tfstate.tflock"] }]}Trust each pair as you trust
plan_roleandapply_role. Plan trusts the pull-request and default branch subjects; apply trusts only the default branch’s. -
Read the warnings.
config checkprints them on stderr and still exits 0:Warning What to do a role is the role of two environments give each environment a pair of its own a root reads the state of another environment’s root the reading root’s roles need that state key too; move the output it needs, or grant that one key read-only and accept the reach a root matches no glob and oidcnames no pairadd a glob for it, or set plan_roleandapply_role -
Run
terragucci initand merge the new pipeline. The plan and apply jobs fetch the token as before and carry the stage’s roles; the stage setsAWS_ROLE_ARNfor each root. A state migration’s reads and writes, and the state version an apply records, use the root’s role too.
GCP and Azure
Section titled “GCP and Azure”oidc.gcp.roles gives a service account pair per glob, and oidc.azure.roles a client pair. A root no glob matches takes the job’s own pair (plan_service_account and apply_service_account, plan_client_id and apply_client_id), which stay required.
oidc:
gcp:
workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/forge/providers/ci
plan_service_account: tg-plan@acme.iam.gserviceaccount.com
apply_service_account: tg-apply@acme.iam.gserviceaccount.com
roles:
"envs/prod/**": { plan: prod-plan@acme.iam.gserviceaccount.com, apply: prod-apply@acme.iam.gserviceaccount.com }
azure:
tenant_id: 7d2c0b4e-0000-4000-8000-00000000a2e1
subscription_id: 00000000-0000-4000-8000-000000000001
plan_client_id: client-plan
apply_client_id: client-apply
roles:
"envs/prod/**": { plan: client-prod-plan, apply: client-prod-apply }| Cloud | Each root’s binary gets | The identity needs |
|---|---|---|
| GCP | GOOGLE_APPLICATION_CREDENTIALS, a copy of the job’s credentials file that impersonates the root’s service account |
Workload Identity User for the pool’s principal, as the job’s service accounts have |
| Azure | ARM_CLIENT_ID, the root’s client |
a federated credential for the job’s subjects |
config check lists each service account with the gcs states its roots keep, and each client with the azurerm ones, and warns as it does for AWS roles. Grant each identity its environment’s bucket prefix or container alone.
Limits
Section titled “Limits”A job’s token can be exchanged for every role whose trust names the job’s subject, so code in a root that reads the token file can assume another environment’s role itself. The roles keep each root’s own runs to its environment’s state, and keep a backend pointed at the wrong key from reading it; they do not separate two environments’ code planned in one job. The threat model has the full table.
- Credentials for the trust each role needs on each forge.
config checkfor every warning.
These docs count page views and clicks with PostHog. They set no cookies, store nothing in your browser, and send nothing when your browser asks not to be tracked.