Use AWS Dynamic credentials with Terraform

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.

  1. We need to rotate keys every 90 days or so to follow the best practices principle.

  2. Using non-expiring keys in itself is a significant security risk.

  3. 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 -:

  1. Dynamic credentials have an expiry attached to them. The default is 3600 sec.

  2. There is no need to rotate or keep a check on these keys.

  3. Only TF can use the role as a claim on this IDP, which will only be supported via Terraform.

  4. Nothing changes from your existing configuration.

  5. We can even give granular access at the workspace at the Phase (PLAN or APPLY) level.

Steps:

  1. 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 the audience should be set to aws.workload.identity or the value of TFC_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 TFC

  • ORG_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 or apply.

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 of StringEquals for the sub and include a * after run_phase: to perform a wildcard match:

💡
Here ORG_NAME is the most important field that allow only our tf cloud org to authenticate with this role. This also prevents from you becoming a prey to the Confused Deputy Problem
  1. Give this role whatever permission policy you want.

  2. Configuring Workspace Variables for AWS Integration in Terraform Cloud

    1. Required Variables:

      • TFC_AWS_PROVIDER_AUTH: Set to true. 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 if TFC_AWS_PLAN_ROLE_ARN and TFC_AWS_APPLY_ROLE_ARN are provided.

    2. Optional Variables:

      • TFC_AWS_WORKLOAD_IDENTITY_AUDIENCE: Defaults to aws.workload.identity. Used as the aud claim for the identity token.

      • TFC_AWS_PLAN_ROLE_ARN: Specify the ARN of the role for the plan phase. Defaults to TFC_AWS_RUN_ROLE_ARN if not provided.

      • TFC_AWS_APPLY_ROLE_ARN: Specify the ARN of the role for the apply phase. Defaults to TFC_AWS_RUN_ROLE_ARN if not provided.

  3. 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