Skip to main content

Updating Plan and Apply Roles

info

This guide is specifically for infrastructure-live repositories that use the terragrunt-scale-catalog. For repositories using the Account Factory, see Configuring IAM Roles.

When Pipelines is installed, it creates two roles in your cloud account: a Plan role and an Apply role. The Plan role is used during pull/merge requests and provides read-only access to your cloud resources. The Apply role provides write access and is only used on your Deploy Branch. Pipelines enforces this separation through the OIDC handshake between your repository and the cloud, so Terragrunt can only authenticate with the Apply role when running on the Deploy Branch.

This guide walks through how to modify these roles when you need to grant Pipelines access to different types of cloud resources.

Identifying existing roles

Your plan and apply roles are managed by the bootstrap stack in your repository. Replace <ACCOUNT_NAME> with the name of the account you are working with. This stack is initially created at:

<ACCOUNT_NAME>/_global/bootstrap/terragrunt.stack.hcl

This stack sources the pipelines-bootstrap stack from the Terragrunt Scale Catalog (GitHub | GitLab).

The role names are determined by the oidc_resource_prefix stack value, which defaults to pipelines. This creates two IAM roles in your AWS account:

  • <oidc_resource_prefix>-plan (e.g., pipelines-plan)
  • <oidc_resource_prefix>-apply (e.g., pipelines-apply)
note

The oidc_resource_prefix is frequently customized to include a random prefix to prevent naming collisions with existing resources in your account.

The IAM policies attached to these roles are controlled by the bootstrap_iam_policy stack value defined in the pipelines-bootstrap stack. This value defaults to default, which tells the stack to load default_plan_iam_policy.json (GitHub | GitLab) and default_apply_iam_policy.json (GitHub | GitLab) as the role policies. A restrictive option is also available, which provides a more locked-down set of permissions.

You can also override the policies entirely by passing custom JSON directly via the plan_iam_policy and apply_iam_policy stack values.

Immutable subject claims (GitHub only)

GitHub has rolled out an immutable subject-claim format for Actions OIDC tokens as of July 15, 2026. Repositories that opt in embed numeric, immutable owner/repo IDs in the sub claim (e.g. repo:acme@1234567/infrastructure-live-root@7654321:ref:refs/heads/main) instead of relying on the repository name alone. This closes a gap where a deleted repository's name could be reused by an unrelated repository, which would otherwise inherit trust intended for the original one.

info

Existing repositories can opt in to this format at any time. Repositories created, renamed, or transferred after July 15, 2026 automatically use the immutable format. No action is required for existing repositories that don't opt in — they continue to use the name-based sub claim shown above.

If you're onboarding through Account Factory or Terragrunt Scale, this is detected and configured automatically on a per-repository basis when the PIPELINES_FEATURE_EXPERIMENT_IMMUTABLE_OIDC_SUBJECT_CLAIMS feature flag is enabled (the default) — no manual action is required. If that flag is disabled, or you're managing your bootstrap stack directly, set github_org_id and github_repo_id alongside the existing github_org_name/github_repo_name (or platform equivalent) once your repository has opted in:

<ACCOUNT_NAME>/_global/bootstrap/terragrunt.stack.hcl
values = {
# ... your existing values ...

github_org_name = "acme"
github_repo_name = "infrastructure-live-root"

# Leave both blank only before the repository has opted in to immutable
# subject claims. Once set, do not clear either value afterward — the
# bootstrap stack would fall back to a legacy name-based trust policy that
# won't match GitHub's immutable sub claim.
github_org_id = "1234567"
github_repo_id = "7654321"
}

Both github_org_id and github_repo_id must be set together — if either is left blank, the bootstrap stack falls back to the legacy name-based sub claim. Once you've set both, don't clear either one afterward: the generated trust policy would revert to the legacy name-based form, which won't match GitHub's immutable sub claim, breaking the OIDC handshake. You can look up the numeric owner/repo IDs for a repository with the GitHub CLI:

gh api repos/{owner}/{repo} --jq '{owner_id: .owner.id, repo_id: .id}'

Customizing roles

The following example walks through adding permissions for a new cloud service to your plan and apply roles.

In this example, we'll add SageMaker permissions, but the same approach works for any AWS service.

1. Copy the default policy files

Start by downloading the default policy files from the Terragrunt Scale Catalog into your bootstrap directory. We'll rename them to custom_ to indicate they're your own versions:

curl -o <ACCOUNT_NAME>/_global/bootstrap/custom_plan_iam_policy.json \
https://raw.githubusercontent.com/gruntwork-io/terragrunt-scale-catalog/main/stacks/aws/github/pipelines-bootstrap/default_plan_iam_policy.json

curl -o <ACCOUNT_NAME>/_global/bootstrap/custom_apply_iam_policy.json \
https://raw.githubusercontent.com/gruntwork-io/terragrunt-scale-catalog/main/stacks/aws/github/pipelines-bootstrap/default_apply_iam_policy.json

2. Add SageMaker permissions

Add read-only SageMaker access to your plan role policy. Open custom_plan_iam_policy.json and add a new entry to the Statement array alongside the existing statements:

<ACCOUNT_NAME>/_global/bootstrap/custom_plan_iam_policy.json
{
"Version": "2012-10-17",
"Statement": [
// ... existing statements ...
{
"Sid": "SageMakerReadOnlyAccess",
"Effect": "Allow",
"Action": [
"sagemaker:Describe*",
"sagemaker:List*",
"sagemaker:Get*"
],
"Resource": "*"
}
]
}

Then add full SageMaker access to your apply role policy. Open custom_apply_iam_policy.json and add a new entry to the Statement array:

<ACCOUNT_NAME>/_global/bootstrap/custom_apply_iam_policy.json
{
"Version": "2012-10-17",
"Statement": [
// ... existing statements ...
{
"Sid": "SageMakerAccess",
"Effect": "Allow",
"Action": [
"sagemaker:*"
],
"Resource": "*"
}
]
}

3. Load your custom policies in the bootstrap stack

Update your terragrunt.stack.hcl to pass the custom policy files to the bootstrap stack. Set plan_iam_policy and apply_iam_policy in the values block, using templatefile to load the JSON (the ${state_bucket_name} template variable is used in the policies for state bucket access):

<ACCOUNT_NAME>/_global/bootstrap/terragrunt.stack.hcl
stack {
# ... your existing stack configuration ...
# Your locals block should already contain:
# account_hcl = read_terragrunt_config(find_in_parent_folders("account.hcl"))

values = {
# ... your existing values ...

plan_iam_policy = templatefile("${get_terragrunt_dir()}/custom_plan_iam_policy.json", {
state_bucket_name = local.account_hcl.locals.state_bucket_name
})
apply_iam_policy = templatefile("${get_terragrunt_dir()}/custom_apply_iam_policy.json", {
state_bucket_name = local.account_hcl.locals.state_bucket_name
})
}
}

4. Deploy the changes via Pipelines

  1. Create a new branch with your changes.
  2. Open a pull/merge request targeting your Deploy Branch. Pipelines will run a plan showing the IAM policy updates.
  3. Review the plan output to confirm the policy changes match what you expect.
  4. Merge the pull/merge request. Pipelines will apply the updated IAM policies to your plan and apply roles.

Inspecting roles in the console

Replace <OIDC_RESOURCE_PREFIX> with your oidc_resource_prefix value from the bootstrap stack (e.g., pipelines).

To verify your roles in the AWS console:

  1. Navigate to IAM > Roles in the AWS Management Console.
  2. Search for your prefix. You should see two roles:

IAM Roles in the AWS ConsoleIAM Roles in the AWS Console

  • <OIDC_RESOURCE_PREFIX>-plan
  • <OIDC_RESOURCE_PREFIX>-apply
  1. Click on either role, select the Permissions tab, and expand the policy under Permissions policies. After deploying your changes, you should see the new permissions (e.g., SageMaker actions) reflected here.
  2. Select the Trust relationships tab to verify the OIDC trust policy. The plan role uses a StringLike condition allowing any branch, while the apply role uses StringEquals restricting access to your Deploy Branch.