Use AWS Dynamic credentials with Terraform
Hello security, bye bye! key rotation

As we all have been heavily using Terraform and Terraform Cloud as our main IAC, the main problem we sometimes face is the authentication between our cloud providers and Terraform. I have mostly seen people using access keys as the medium of auth, which is all the more wrong from a security and management point of view.
However, most of us would think our processes are going on smoothly, and we haven’t faced any difficulty in the past, but using access keys as the authentication method with our cloud provider has caveats of its own.
We need to rotate keys every 90 days or so to follow the best practices principle.
Using non-expiring keys in itself is a significant security risk.
Provide unlimited access to any who can see TF variable sets to any account as the keys have administrator privileges.
The solution to the problem:
We can start using Dynamic credentials via an IDP OIDC provider with Terraform. It provides various benefits over static creds -:
Dynamic credentials have an expiry attached to them. The default is 3600 sec.
There is no need to rotate or keep a check on these keys.
Only TF can use the role as a claim on this IDP, which will only be supported via Terraform.
Nothing changes from your existing configuration.
We can even give granular access at the workspace at the Phase (PLAN or APPLY) level.
Steps:
- Create an OIDC Identity Provider:
The
provider URLshould be set to the address of HCP Terraform (e.g., https://app.terraform.io without a trailing slash), and theaudienceshould be set toaws.workload.identityor the value ofTFC_AWS_WORKLOAD_IDENTITY_AUDIENCE, if configured.
Create a Role with the following Trust Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "OIDC_PROVIDER_ARN"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"SITE_ADDRESS:aud": "AUDIENCE_VALUE",
"SITE_ADDRESS:sub": "organization:ORG_NAME:project:PROJECT_NAME:workspace:WORKSPACE_NAME:run_phase:RUN_PHASE"
}
}
}
]
}
OIDC_PROVIDER_ARN: The ARN from the OIDC provider resource created in the previous step
SITE_ADDRESS: The address of HCP Terraform with
https://stripped, (e.g.,app.terraform.io)AUDIENCE_VALUE: This should be set to
aws.workload.identityunless a non-default audience has been specified in TFCORG_NAME: The organization name this policy will apply to, such as
my-org-namePROJECT_NAME: The project name that this policy will apply to, such as
my-project-nameWORKSPACE_NAME: The workspace name this policy will apply to, such as
my-workspace-nameRUN_PHASE: The run phase this policy will apply to, currently one of
planorapply.Note: if different permissions are desired for plan and apply, then two separate roles and trust policies must be created for each of these run phases to properly match them to the correct access level. If the same permissions will be used regardless of run phase, then the condition can be modified like the below to use
StringLikeinstead ofStringEqualsfor the sub and include a*afterrun_phase:to perform a wildcard match:
Give this role whatever permission policy you want.
Configuring Workspace Variables for AWS Integration in Terraform Cloud
Required Variables:
TFC_AWS_PROVIDER_AUTH: Set totrue. Required for HCP Terraform to authenticate to AWS (v1.7.0 or later).TFC_AWS_RUN_ROLE_ARN: Specify the ARN of the role to assume in AWS. Optional ifTFC_AWS_PLAN_ROLE_ARNandTFC_AWS_APPLY_ROLE_ARNare provided.
Optional Variables:
TFC_AWS_WORKLOAD_IDENTITY_AUDIENCE: Defaults toaws.workload.identity. Used as theaudclaim for the identity token.TFC_AWS_PLAN_ROLE_ARN: Specify the ARN of the role for the plan phase. Defaults toTFC_AWS_RUN_ROLE_ARNif not provided.TFC_AWS_APPLY_ROLE_ARN: Specify the ARN of the role for the apply phase. Defaults toTFC_AWS_RUN_ROLE_ARNif not provided.
Try to run a plan to test that TF can assume the designated role.
You can also use this cloud formation template to make this quick!
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "CloudFormation template to create OIDC provider and IAM role with administrator access for Terraform Cloud",
"Parameters": {
"RoleName": {
"Type": "String",
"Description": "Name of the IAM role to be created",
"Default": "Terraform-service-account-role"
},
"ManagedPolicyArn": {
"Type": "String",
"Description": "ARN of the managed policy to attach to the role",
"Default": "arn:aws:iam::aws:policy/AdministratorAccess"
},
"OIDCClientId": {
"Type": "String",
"Description": "Client ID(s) (audience) to be added to the OIDC provider",
"Default": "aws.workload.identity"
}
},
"Resources": {
"OIDCProvider": {
"Type": "AWS::IAM::OIDCProvider",
"DeletionPolicy": "Delete",
"Properties": {
"Url": "https://app.terraform.io",
"ClientIdList": [
{
"Ref": "OIDCClientId"
}
]
}
},
"AdministratorRole": {
"Type": "AWS::IAM::Role",
"DeletionPolicy": "Delete",
"DependsOn": [
"OIDCProvider"
],
"Properties": {
"RoleName": {
"Ref": "RoleName"
},
"Description": "Administrator role for Terraform Cloud OIDC federation",
"MaxSessionDuration": 3600,
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": {
"Fn::Sub": "arn:aws:iam::${AWS::AccountId}:oidc-provider/app.terraform.io"
}
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"app.terraform.io:aud": "aws.workload.identity"
},
"StringLike": {
"app.terraform.io:sub": "organization:<YOUR ORG NAME>:project:*:workspace:*:run_phase:*"
}
}
}
]
},
"ManagedPolicyArns": [
{
"Ref": "ManagedPolicyArn"
}
]
}
}
},
"Outputs": {
"OIDCProviderArn": {
"Description": "ARN of the created OIDC Provider",
"Value": {
"Fn::GetAtt": [
"OIDCProvider",
"Arn"
]
}
},
"RoleArn": {
"Description": "ARN of the created IAM Role",
"Value": {
"Fn::GetAtt": [
"AdministratorRole",
"Arn"
]
}
}
}
}
Conclusion
This will significantly improve your IAC security posture, as infinite validity keys can be a huge problem in the long term. Now, with this, you don’t even have to rotate your keys every now and then, as these are dynamic keys that are created when requested with a default TTL of 1hr.
This approach can also be used for other cloud providers like Azure, GCP, etc.
You can read the official documentation here




