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 URL
should be set to the address of HCP Terraform (e.g., https://app.terraform.io without a trailing slash), and theaudience
should be set toaws.workload.identity
or 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.identity
unless a non-default audience has been specified in TFCORG_NAME: The organization name this policy will apply to, such as
my-org-name
PROJECT_NAME: The project name that this policy will apply to, such as
my-project-name
WORKSPACE_NAME: The workspace name this policy will apply to, such as
my-workspace-name
RUN_PHASE: The run phase this policy will apply to, currently one of
plan
orapply
.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
StringLike
instead ofStringEquals
for 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_ARN
andTFC_AWS_APPLY_ROLE_ARN
are provided.
Optional Variables:
TFC_AWS_WORKLOAD_IDENTITY_AUDIENCE
: Defaults toaws.workload.identity
. Used as theaud
claim for the identity token.TFC_AWS_PLAN_ROLE_ARN
: Specify the ARN of the role for the plan phase. Defaults toTFC_AWS_RUN_ROLE_ARN
if not provided.TFC_AWS_APPLY_ROLE_ARN
: Specify the ARN of the role for the apply phase. Defaults toTFC_AWS_RUN_ROLE_ARN
if 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