Securing authentication between Terraform Cloud and AWS using OIDC
Terraform is a very famous Infrastructure as Code management tool that allows you to manage almost all of your infrastructure resources like VMs, databases, firewalls, networking, etc. You can not only manage your infrastructure but also other aspects like Kubernetes, Helm, Vault, Active Directory, etc using providers.
Terraform Cloud is a great tool to centralise your terraform runs so that they are visible to everyone rather than just the person running it. It provides both fully-managed and self-managed agent-based environment along with state management and versioning. Combined with Sentinel, you can prevent misconfigurations even before the infrastructure is provisioned.
Dynamic Credentials
Until the launch of Dynamic Providers Credential, passing long-lived credentials was the only option to allow Terraform Cloud to authenticate with AWS and manage the infrastructure resources but with the launch of this new feature, Terraform Cloud can now assume an IAM role and generate short-lived credentials to authenticate with AWS using OIDC protocol. Along with AWS, the Dynamic Provider Credentials is supported for Vault, Azure and GCP but in this article we will only focus on improving security between AWS and Terraform Cloud.
Let’s get started.
OIDC Provider
The very first task is to create an OIDC Provider in AWS that will be used in the IAM role trust policy. So, let’s head over to the Identity Providers page available within the IAM console and create our OIDC provider.
Click on Add provider button to proceed. Select OpenID Connect as Provider type, https://app.terraform.io as Provider URL and click on Get thumbprint button to fetch the fingerprint of the SSL certificate associated with the domain. Finally, provide aws.workload.identity as Audience and optionally tags before creating the provider.
Note: If you use a custom domain to access Terraform Cloud make sure to provide the same in the provider URL otherwise authentication will fail. Eg: If you use terraform.internal.acme.com instead of app.terraform.io to access Terraform Cloud, you need to provide terraform.internal.acme.com as the provider URL.
Note: Setting value of audience to aws.workload.identity is not a hard requirement. We can set it to whatever we want but, we need to make sure to pass the same value to the aud variable when creating the IAM role trust relationship policy and to the Terraform Cloud workspace via the environment variable TFC_AWS_WORKLOAD_IDENTITY_AUDIENCE.
Great, now that we have our OIDC provider created let’s create an IAM role that can be assumed by Terraform Cloud.
IAM Role
It’s time to create an IAM role that will allow Terraform Cloud to assume the role but before that let’s define the trust policy for IAM role.
oidc-trust-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "OIDC_PROVIDER_ARN"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"app.terraform.io:aud": "aws.workload.identity"
},
"StringLike": {
"app.terraform.io:sub": "organization:ORG_NAME:project:PROJECT_NAME:workspace:WORKSPACE_NAME:run_phase:PHASE_NAME"
}
}
}
]
}
While creating the trust policy make sure the below points are followed:
Replace OIDC_PROVIDER_ARN with the actual value.
If applicable, replace app.terraform.io present in the condition with the custom domain name used to access Terraform Cloud.
The value of app.terraform.io:aud variable in the condition must match the value of audience set earlier when creating the OIDC provider.
The value of app.terraform.io:sub variable contains multiple placeholders so make sure to update all of those:
Replace ORG_NAME with the actual organisation name. Avoid setting this to * otherwise any organisation using Terraform Cloud will be able to assume your role.
PROJECT_NAME can be either set to * or Default Project (this is the name of the project created by default by Terraform Cloud) or to a project where your workspace is hosted.
Replace WORKSPACE_NAME with the actual workspace name. You can also set it as * but it is recommended to create a separate role for each workspace to limit the blast radius.
PHASE_NAME can be either set to plan or apply or even * in case you want to use the same IAM role for both apply and plan phases.
Note: In the above trust policy we are using StringLike operator for app.terraform.io:sub variable but we can also use StringEquals operator if we decide not to use * (wildcard) for any of the placeholders.
It’s now time to create the role using the above trust policy.
aws iam create-role --role-name terraform-cloud-oidc-demo --assume-role-policy-document file://oidc-trust-policy.json
Now let’s attach a policy to the IAM role. To keep things simple but still slightly secure, let’s attach a AmazonS3FullAccess managed policy. This is because at a later stage we will be managing an S3 bucket using the Terraform to test our role.
aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess --role-name terraform-cloud-oidc-demo
Note: attach-role-policy can only be used if you want to attach a managed policy to the IAM role. In case you want to create an inline policy for the role you need to use put-role-policy API.
It’s time to update configuration of the Terraform Cloud workspace.
Terraform Cloud Configuration
Now that we are done creating the OIDC provider and IAM role, let’s add the required environment variables in our Terraform Cloud workspace to use IAM role rather than long lived credentials of an IAM user for authentication.
Following are the list of environment variables supported for AWS authentication:
Variable | Value | Description |
---|---|---|
TFC_AWS_PROVIDER_AUTH | true | Must be set to true otherwise Terraform Cloud will not authenticate to AWS using the IAM role |
TFC_AWS_RUN_ROLE_ARN | ARN of the IAM role to use for plan and apply operation | Either TFC_AWS_RUN_ROLE_ARN or both TFC_AWS_PLAN_ROLE_ARN and TFC_AWS_APPLY_ROLE_ARN must be specified |
TFC_AWS_WORKLOAD_IDENTITY_AUDIENCE | Defaults to aws.workload.identity | If custom value was set for aud variable under condition while creating the trust policy then you must set this variable to the same custom value |
TFC_AWS_PLAN_ROLE_ARN | ARN of the IAM role to use for plan operation | If you plan to use separate roles for plan and apply operation than set this value to the ARN of the IAM role you created for plan operation |
TFC_AWS_APPLY_ROLE_ARN | ARN of the IAM role to use for apply operation | If you plan to use separate roles for plan and apply operation than set this value to the ARN of the IAM role you created for apply operation |
Make sure to also set the region either by creating it as a terraform variable in the workspace or by adding it in the *.auto.tfvars file.
This completes our Terraform Cloud configuration to use IAM role for authentication with AWS. It’s now time to test the configuration so let’s run a simple terraform template.
terraform {
cloud {
organization = "skildops"
workspaces {
name = "oidc-demo"
}
}
}
variable "region" {}
provider "aws" {
region = var.region
}
resource "aws_s3_bucket" "oidc-demo" {
bucket = "skildops-oidc-demo"
}
Congratulations! You have successfully implemented and tested OIDC authentication setup between Terraform Cloud and AWS. To verify if the IAM role was used for authentication we can check the Last activity value and Access advisor tab on the IAM role page.
Covering the basics
-
OpenID Connect (OIDC) is an open-source authentication protocol built on top of OAuth 2.0 protocol. OIDC allows verification of end-user identity along with sharing basic profile information like name, email, profile picture, etc. with the requesting server.
-
OAuth2 is solely focused on authorisation whereas OIDC supports both authentication and authorisation and it runs on top of OAuth2.
-
Before the launch of Dynamic Credentials, the only option to pass AWS credentials to Terraform Cloud was to store the long-lived IAM user credentials as sensitive environment variables.