A Terraform Cloud workspace holding AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as sensitive variables gives every run, and anyone who can edit that workspace, a long-lived key that usually carries near-administrator rights. HCP Terraform (the current name for Terraform Cloud) can instead mint a signed workload identity token for each plan and apply, which AWS STS exchanges for temporary role credentials that are discarded when the run ends. This page is the setup runbook: create the IAM OIDC provider, write trust policies that pin your organisation, project, workspace and run phase, set the TFC_AWS_* variables, test, roll back and delete the old keys. It is for platform engineers who own both the HCP Terraform organisation and the AWS accounts. For the wider platform hardening picture see the Terraform Cloud and HCP Security Guide; this page focuses on doing the migration.
This is an independent guide from NHI Mgmt Group, based on HashiCorp's and AWS's public documentation as checked in November 2026. It is not sponsored or reviewed by HashiCorp or AWS. Confirm settings against the current vendor documentation before changing production.
Key takeaways
- The trust policy is the only gate. HashiCorp warns that if you do not check at least the audience and the organisation name, other HCP Terraform organisations could assume your role. Every customer shares the same issuer,
https://app.terraform.io. - Only
audandsubare usable in AWS. AWS maps generic OIDC providers to the condition keysaud,sub,amr,emailandoaud, so the richterraform_workspace_idclaims cannot be checked in the trust policy. All scoping happens in thesubstring. - Split plan and apply. The subject ends with
run_phase:planorrun_phase:apply, so a read-only plan role and a write apply role are a two-line change withTFC_AWS_PLAN_ROLE_ARNandTFC_AWS_APPLY_ROLE_ARN. - Names, not IDs, are in the subject. Organisation, project and workspace names can change. A rename breaks the trust, and a workspace recreated with the old name inherits it, so control who can create and rename workspaces.
- Finish by deleting keys. The migration is done only when the IAM user's access keys are deleted and the static variables removed from every workspace and variable set.
When you need this
- Static AWS keys sit in workspace variables or a variable set. A variable set shared across many workspaces is the worst case: one key, one blast radius, many writers. This is the long-lived secret pattern OWASP lists as NHI7 Long-Lived Secrets.
- The Terraform IAM user has broad rights. Provisioning identities often hold
AdministratorAccessbecause nobody scoped them. Federation is the moment to split plan from apply and scope per workspace. - You cannot tell which workspace changed what. With a shared IAM user every run looks the same in CloudTrail. Federated sessions record the token subject, which names the workspace and phase.
- Rotation is manual or overdue. HashiCorp's own rationale is that static credentials carry risk even when rotated regularly; per-run credentials leave nothing to rotate.
Credentials stored in Terraform state are a separate problem that federation does not solve. See How to Find secrets in terraform state.
Before you start
| Item | What you need |
|---|---|
| AWS permissions | Rights to create an IAM OIDC identity provider and IAM roles in each target account |
| HCP Terraform permissions | Rights to manage variables on the target workspaces or variable sets; organisation owner if you will restrict who can create workspaces |
| Issuer URL | https://app.terraform.io for HCP Terraform, no trailing slash. For Terraform Enterprise, your instance URL, which AWS must be able to reach to read its discovery document and keys |
| Audience | aws.workload.identity by default, or your own value set with TFC_AWS_WORKLOAD_IDENTITY_AUDIENCE |
| Subject plan | Organisation, project and workspace names for each role, and whether plan and apply need separate roles |
| Agents | Self-hosted HCP Terraform agents must run v1.7.0 or later, and v1.12.0 or later for multiple configurations |
| Permissions plan | The AWS actions each workspace really uses, from CloudTrail history for the existing IAM user |
How the subject is built
For a workspace run the token's sub claim is organization:ORG_NAME:project:PROJECT_NAME:workspace:WORKSPACE_NAME:run_phase:RUN_PHASE, where the phase is plan or apply. Project names may contain spaces: the default project is literally Default Project, and the subject keeps that space. Module test runs use a different form, organization:ORG_NAME:module:MODULE_NAME:operation:test_run, and are always read-only.
| Trust scope | sub condition | Use it for | Watch out for |
|---|---|---|---|
| One workspace, one phase | StringEquals organization:acme:project:payments:workspace:payments-prod:run_phase:apply | Production apply roles | Breaks if the workspace or project is renamed |
| One workspace, both phases | StringLike ending run_phase:* | Low-risk workspaces where plan and apply need the same rights | Plans, which run on every speculative change, get write rights |
| One project, all workspaces | StringLike organization:acme:project:sandbox:workspace:*:run_phase:plan | Read-only roles across a project | Anyone who can create a workspace in that project inherits the role |
| Whole organisation | StringLike organization:acme:* | Avoid for anything except tightly limited read roles | Every workspace, including future ones, can assume the role |
Step-by-step
- Read the current state first (no changes). List existing providers with
aws iam list-open-id-connect-providers; there can only be one provider per URL in an account. In HCP Terraform, list every workspace and variable set that holdsAWS_ACCESS_KEY_ID, and record which IAM user each key belongs to withaws sts get-caller-identityfrom a controlled shell. - Create the OIDC identity provider once per account.
AWS verifies the issuer's TLS certificate against its library of trusted root certificate authorities and uses a thumbprint only as a fallback. HashiCorp's example Terraform for this step uses theaws iam create-open-id-connect-provider \ --url https://app.terraform.io \ --client-id-list aws.workload.identityaws_iam_openid_connect_providerresource with atls_certificatedata source to populate the thumbprint, which does no harm. - Write the apply role trust policy. Save as
trust-apply.json:
Write{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/app.terraform.io"}, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "app.terraform.io:aud": "aws.workload.identity", "app.terraform.io:sub": "organization:<ORG>:project:<PROJECT>:workspace:<WORKSPACE>:run_phase:apply" } } }] }trust-plan.jsonthe same way withrun_phase:plan. Note that HashiCorp's public example repository usesStringLikewithrun_phase:*for brevity; for production, prefer two roles with exact matches. - Create the roles and attach least-privilege permissions.
Give the apply role a policy built from the services the workspace actually manages. The AWS managedaws iam create-role --role-name tfc-payments-prod-apply \ --assume-role-policy-document file://trust-apply.json --max-session-duration 3600 aws iam create-role --role-name tfc-payments-prod-plan \ --assume-role-policy-document file://trust-plan.json --max-session-duration 3600 aws iam attach-role-policy --role-name tfc-payments-prod-plan \ --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccessReadOnlyAccesspolicy is a quick starting point for plans, but it is broad; replace it with a policy limited to the resources the refresh actually reads. See the AWS IAM Policies and Roles Guide. - Set the workspace variables. As environment variables on the workspace (or a variable set scoped to exactly the workspaces the roles trust):
None of these are secrets, but mark the workspace variables as managed through code or restricted to administrators: whoever can change a role ARN can point the run at another role the subject is trusted by.
Variable Value Notes TFC_AWS_PROVIDER_AUTHtrueRequired; without it HCP Terraform does not attempt AWS authentication TFC_AWS_PLAN_ROLE_ARNPlan role ARN Falls back to TFC_AWS_RUN_ROLE_ARNif unsetTFC_AWS_APPLY_ROLE_ARNApply role ARN Falls back to TFC_AWS_RUN_ROLE_ARNif unsetTFC_AWS_RUN_ROLE_ARNSingle role ARN Required only if the two phase-specific variables are not both set TFC_AWS_WORKLOAD_IDENTITY_AUDIENCECustom audience Optional; must match the provider client ID and the policy aud - Clean the provider block. Keep a
regionargument (or setAWS_REGION) and removeaccess_key,secret_key,profileandassume_rolesettings that existed for the static key. HashiCorp warns that other authentication arguments may interfere with dynamic credentials. - Handle multiple accounts with aliases. Where one workspace deploys to several accounts, use tagged variables (for example
TFC_AWS_PROVIDER_AUTH_PRODandTFC_AWS_RUN_ROLE_ARN_PROD), declare a Terraform variable namedtfc_aws_dynamic_credentials, and point each aliased provider'sshared_config_filesatvar.tfc_aws_dynamic_credentials.aliases["PROD"].shared_config_file. Each target account needs its own OIDC provider and role. - Pilot on a non-production workspace. Leave the old key variables in place but rename them (for example prefix with
DISABLED_) so the provider cannot find them, then queue a plan and an apply.
Verify it worked
- Run log: the run completes without the static variables present. A temporary
data "aws_caller_identity"output shows anassumed-roleARN for the plan role during plan and the apply role during apply. - CloudTrail: look for
AssumeRoleWithWebIdentityfromsts.amazonaws.comwithuserIdentity.typeofWebIdentityUser.responseElements.subjectFromWebIdentityTokenholds the full HCP Terraform subject, so you can see the workspace and phase. Global endpoint requests are logged in us-east-1, regional endpoint requests in their own region. - Negative tests: trigger a plan from a workspace the policy does not name, and confirm a plan cannot use the apply role. Both must fail with an
AssumeRoleWithWebIdentityaccess denial. - Old identity:
aws iam get-access-key-last-usedon the old key shows no use after the cut-over date.
| CloudTrail field | What it tells you for HCP Terraform |
|---|---|
userIdentity.identityProvider | The app.terraform.io provider ARN that vouched for the caller |
responseElements.subjectFromWebIdentityToken | Organisation, project, workspace and run phase |
responseElements.audience | Should always be your configured audience |
requestParameters.roleArn | Plan or apply role used |
additionalEventData.identityProviderConnectionVerificationMethod | IAMTrustStore or Thumbprint |
To trace a session back to a run, match the CloudTrail event time and subject with the run in HCP Terraform; the token also carries a terraform_run_id claim (for example run-X3n1AUXNGWbfECsJ) that HashiCorp recommends for logging, though AWS does not surface custom claims in the event.
Rollback and common errors
| Symptom | Likely cause | Fix |
|---|---|---|
| "Not authorized to perform sts:AssumeRoleWithWebIdentity" | sub mismatch: project name wrong (often Default Project), workspace renamed, or phase not allowed | Compare subjectFromWebIdentityToken from a failed attempt with the policy, character for character |
| Provider asks for credentials or uses an unexpected identity | TFC_AWS_PROVIDER_AUTH missing or not true, or leftover static keys or profile settings | Set the variable; remove other auth settings from the provider block and workspace |
| Audience error | Provider client ID, policy aud and TFC_AWS_WORKLOAD_IDENTITY_AUDIENCE disagree | Make all three the same value |
| Works on HCP-hosted runs, fails on agents | Agent older than v1.7.0, or older than v1.12.0 with aliases | Upgrade the agent pool |
Rollback: during the pilot, rename the disabled key variables back and remove TFC_AWS_PROVIDER_AUTH; nothing in AWS needs undoing. After cut-over, deactivate the old IAM user's keys (aws iam update-access-key --status Inactive), keep them inactive for at least one full release cycle, then delete the keys, the variables and, if nothing else uses it, the IAM user. See How to find unused AWS IAM roles and access keys. To withdraw access from a workspace, edit or delete the role's trust policy; sessions already issued last until they expire.
Choosing a role layout
| Layout | Best for | Trade-off |
|---|---|---|
| One plan and one apply role per workspace | Production, regulated accounts | More roles to manage; clearest audit trail and smallest blast radius |
| One role per workspace, both phases | Small teams with few workspaces | Every speculative plan, including those from pull requests, holds write rights |
| Shared read role per project, per-workspace apply roles | Large estates with many similar workspaces | Project membership becomes part of the trust; control workspace creation |
| Vault-backed dynamic credentials | Organisations already brokering AWS access through Vault | Adds Vault as a dependency; AWS trust is to Vault rather than to HCP Terraform |
Small organisations can start with one role per workspace and split phases later. Large organisations should manage the roles themselves in a bootstrap workspace with its own tightly held apply role, so that no ordinary workspace can edit the trust policies that govern it.
Hardening and monitoring afterwards
- Protect names. Restrict who can create, rename or move workspaces and projects, because the subject trusts names. Alert when a workspace whose name appears in a trust policy is deleted or renamed.
- Use
StringEqualsfor apply roles. KeepStringLikewildcards for read-only roles, and never wildcard the organisation segment. - Keep the bootstrap separate. The workspace that manages OIDC providers and CI roles should not be trusted by any role it creates.
- Alert on drift:
UpdateAssumeRolePolicyontfc-*roles,CreateOpenIDConnectProviderorAddClientIDToOpenIDConnectProvideroutside change windows, andAssumeRoleWithWebIdentitywith an unexpected subject. - Review variable sets. A variable set applied globally that sets
TFC_AWS_RUN_ROLE_ARNhands the same role to every workspace; scope sets explicitly.
| Metric | Target |
|---|---|
Workspaces or variable sets holding AWS_SECRET_ACCESS_KEY | Zero |
Apply roles whose sub uses StringLike | Zero, or documented exceptions |
| Workspaces with separate plan and apply roles | All production workspaces |
| Trust policy changes without a linked change record | Zero |
| Federated sessions with an unexpected subject | Zero; investigate every one |
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| MITRE ATT&CK | T1552.001 Credentials In Files; T1078.004 Cloud Accounts; T1098.003 Additional Cloud Roles | Static keys exposed in variables or code; abuse of valid cloud roles; tampering with role trust |
| NIST SP 800-53 Rev. 5 | IA-5, IA-5(7), AC-6, AU-6 | Authenticator management, no embedded static authenticators, least privilege, log review |
| NIST CSF 2.0 | PR.AA-01, PR.AA-05, DE.CM-03 | Credential management, least privilege, monitoring |
| OWASP NHI Top 10 2025 | NHI6 Insecure Cloud Deployment Configurations; NHI7 Long-Lived Secrets | Static CI credentials and loose OIDC conditions |
Practitioner checklist
- List every workspace and variable set holding AWS keys and the IAM users behind them.
- Create the
app.terraform.ioOIDC provider in each target account with the agreed audience. - Create separate plan and apply roles, with
StringEqualsonaudand an exactsubfor apply. - Check the project name in every subject, including
Default Project. - Set
TFC_AWS_PROVIDER_AUTHand the role ARN variables; remove other auth settings from provider blocks. - Upgrade self-hosted agents to a version that supports dynamic credentials and aliases.
- Run positive and negative tests and confirm the subject in CloudTrail.
- Deactivate, then delete, the old keys, variables and IAM user.
- Restrict workspace creation and renaming, and alert on trust policy changes.
Standards and references
- HashiCorp: Use dynamic credentials with the AWS provider
- HashiCorp: Workload identity tokens
- HashiCorp: Dynamic provider credentials overview
- HashiCorp: Dynamic credentials setup examples
- AWS: Create an OpenID Connect identity provider in IAM
- AWS: IAM and AWS STS condition context keys
- AWS: Logging IAM and AWS STS API calls with AWS CloudTrail
- OWASP: Non-Human Identities Top 10
Related NHI Mgmt Group resources: Terraform and IaC Pipeline Credential Security Guide · How to set up workload identity federation for GitHub Actions to AWS · How to federate Bitbucket Pipelines to AWS · Multi-Cloud Workload Identity Troubleshooting Guide · AWS STS and AssumeRole Guide