Introducing OpenID Connect identity tokens in CircleCI jobs! This token enables your CircleCI jobs to authenticate with cloud providers that support OpenID Connect like AWS, Google Cloud Platform, and HashiCorp Vault. In this blog post, we’ll introduce you to OpenID Connect, explain its usefulness in a CI/CD system, and show how it can be used to authenticate with AWS and GCP, letting your CircleCI job securely interact with your account without any static credentials.

If you are interested in using OIDC to authenticate with third-party credential managers, you can also view our tutorial on integrating CircleCI with HashiCorp Vault using OIDC.

What is OpenID Connect?

OpenID Connect (OIDC) is an authentication protocol that allows cloud services to verify the identity of end users. It adds an identity layer to OAuth2.0, an authorization protocol for providing single sign-on (SSO) access to cloud resources. Since its introduction in 2014, cloud providers have widely adopted OpenID Connect as the standard for providing secure access to protected resources.

According to the OpenID Connect Foundation, OpenID “allows Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server.” When you use CircleCI’s OpenID Connect token, the client is a cloud provider such as AWS. The end user is your job running in CircleCI. The authorization server is CircleCI itself.

Putting that all together, this means that a cloud provider like AWS can verify the identity of your job and allow it to take actions as if it were an authenticated user. This means your job can securely interact with AWS.

Why is OpenID Connect useful in CI/CD?

Physical key vs OIDC tokens

In your CI workflow, you may want to upload binaries, test artifacts, or logs to cloud storage. Perhaps you want to submit a package to an artifact repository. Or maybe you may want to deploy to your production environment. If you want to securely access any of these cloud resources, your job needs to have credentials to authenticate with your cloud provider. CircleCI’s contexts let you securely store credentials and use them in jobs throughout your organization.

Additional OIDC token documentation

However, these credentials are static. A physical key grants access to a space until the locks are changed, and analogously these static credentials are valid until you rotate them which you may want to do as a security best practice. You can rotate keys in the CircleCI web UI, with the CircleCI CLI or with the CircleCI API for Contexts.

Rotation requires a time investment, which reduces your engineering capacity. OpenID Connect tokens avoid this downside. When you configure your cloud provider to trust OpenID Connect tokens from your CircleCI jobs, your cloud provider will recognize CircleCI’s signature and treat your job’s requests as authentic. Your jobs securely interact with your cloud provider, so that you can upload to cloud storage, change the state of production, or whatever else you decide to permit.

OIDC token documentation

When your CircleCI job starts, CircleCI signs an OpenID Connect token and makes it available to your job. Your job can present this token to your cloud provider, which verifies its authenticity, grants your job temporary credentials, and permits it to take some actions.

You can read about the structure of CircleCI’s OIDC token in our documentation.

Using the OpenID Connect token to interact with AWS from a CircleCI job

Here’s an example of how you can use CircleCI’s OpenID Connect token to interact with AWS. First, we’ll do a one-time setup to configure your AWS account to trust CircleCI’s OpenID Connect tokens. Then, we’ll run a job that uses the token to interact with AWS and upload an image to ECR.

Setting up AWS

First, create an IAM identity provider and an IAM role in AWS. This allows your AWS account to trust CircleCI’s OpenID Connect tokens. You only have to do this once.

In IAM, create an OpenID Connect identity provider. You’ll need your organization ID, which you can find by going to your CircleCI organization settings and copying the Organization ID from the Overview page.

For the Provider URL, provide https://oidc.circleci.com/org/<organization-id>, where <organization-id> is the ID of your CircleCI organization.

For the audience, provide the same organization ID.

In IAM, create a role. For the trusted entity, select Web Identity then choose the identity provider that you created earlier. For audience, choose the only option, which is the organization ID you created earlier. On the Add permissions page, you can specify what your CircleCI jobs can and cannot do. Choose only permissions that your job will need, which is an AWS best practice.

Note: You may find it useful to create your own policy.

Interacting with AWS from a CircleCI job

Now that you’ve set up an IAM role, you’re ready to write a CircleCI job that interacts with AWS. The simplest way to accomplish this is to use CircleCI’s AWS CLI orb to generate temporary keys and configure a profile that uses OIDC.

Orbs are reusable packages of YAML configuration that condense repeated pieces of config into a single line of code. In this case, the AWS CLI orb enables you to generate a temporary session token, AWS access key ID, and AWS secret access key in just six lines of code or fewer in your config. CircleCI has several orbs that support a number of AWS services. You can find them all in our developer hub.

First, choose the CircleCI jobs in your workflow where you want to use OIDC. Make sure each of these jobs use a valid CircleCI context. The OpenID Connect token is available only to jobs that use at least one context. The context may contain no environment variables. You can read about CircleCI’s OIDC tokens in our OIDC token documentation.

In your config, be sure to import the aws-cli orb. Next, run the aws-cli/setup command in your job before interacting with any AWS services. You will need to provide the aws-cli/setup command with the role-arn associated with the role you’ve created in the step above along with your aws-region. You can optionally provide a profile-name, role-session-name, and session-duration.

If you provide a profile-name, the temporary keys and token will be configured to the specified profile. You must use that same profile-name with the rest of your AWS commands. Otherwise, the keys and token will be configured to the default profile if none is provided. Additionally, if you do not provide a role-session-name or session-duration, their default values are ${CIRCLE_JOB} (your job’s name) and 3600 seconds, respectively.

Here’s an example of a complete config file with a job that configures a profile with OIDC and uses it to log into AWS ECR. You can use this same profile to run other AWS commands such as S3, EKS, ECS and more, as long as the role-arn has been configured with appropriate permissions.

version: '2.1'
orbs:
 # import CircleCI's aws-cli orb
 aws-cli: circleci/aws-cli@3.1
jobs:
 aws-example:
   docker:
     - image: cimg/aws:2022.06
   steps:
     - checkout
     # run the aws-cli/setup command from the orb
     - aws-cli/setup:
         role-arn: 'arn:aws:iam::123456789012:role/OIDC-ROLE'
         aws-region: "us-west-1"
         # optional parameters
         profile-name: "OIDC-PROFILE"
         role-session-name: “example-session”
         session-duration: 1800
     - run:
         name: Log-into-AWS-ECR
         command: |
           # must use same profile specified in the step above       
           aws ecr get-login-password --profile "OIDC-PROFILE"
workflows:
 OIDC-with-AWS:
   jobs:
     - aws-example:
         # must use a valid CircleCI context
         context: aws

Advanced usage

You can take advantage of the format of the claims in CircleCI’s OIDC token to limit what your CircleCI jobs can do in AWS. For example, if certain projects should only be able to access certain AWS resources, you can restrict your IAM role so that only CircleCI jobs in a specific project can assume that role.

To do this, edit your IAM role’s trust policy so that only an OIDC token from your chosen project can assume that role. The trust policy determines under what conditions the role can be assumed.

First, find your project’s Project ID under Project Settings > Overview.

Project settings overview

Then add the following condition to your role’s trust policy so that only jobs in your chosen project can assume that role:

"StringLike": {
  "oidc.circleci.com/org/<your organization ID>:sub": "org/<your organization ID>/project/<your project ID>/user/*"
}

This uses StringLike to match the sub claim of CircleCI’s OIDC token in your chosen project. Now, jobs in your other projects cannot assume this role.

Authenticating with GCP using CircleCI OIDC tokens

You will need to deploy some GCP infrastructure so that GCP recognizes tokens sent by CircleCI. There are three resources that need to be created:

  • Workload identity pool
  • Workload identity pool provider
  • Service account

You can either do this manually or programmatically using Terraform. We’ll cover both in this article. Before getting started you will need to retrieve your organization ID. You can find this by logging in to CircleCI and clicking Organization Settings. Your ID will be shown at the top of the page. Note your ID for use in later steps.

Manually creating GCP resources

To create the workload identity pool and its provider, navigate to the Workload Identity Pools page and click Create Pool. Name your pool and select Open ID Connect (OIDC) as the provider. Choose a provider name and provider ID. Next, set the issuer URL to https://oidc.circleci.com/org/<YOUR ORGANIZATION ID>. Click Allowed audiences and enter your organization ID. Under attribute mapping, configure the following attributes:

google.subject = assertion.sub
attribute.org_id = assertion.aud

Click Save to create the new workload identity pool and workload identity pool provider.

The next step is to bind a service account to the workload identity pool. Create a service account or use an existing one that has permission to perform the GCP actions required by your pipeline job. Next, select your newly created workload identity pool from the Workload Identity Pools page. Click Grant Access at the top of the page. Select the service account from the dropdown menu. Next, click Only identities matching the filter, select org_id for attribute name, enter your CircleCI organization ID for the attribute value, and then click Save. Dismiss the “Configure your application” prompt.

At this point, all of the necessary GCP resources should be in place.

Programmatically creating resources with Terraform

This GitHub repo contains a Terraform plan that will deploy the GCP infrastructure required to authenticate using CircleCI OIDC tokens. If you have not used Terraform with GCP before, you will need to install Terraform and configure it for use with GCP.

To use the plan, fill out the example terraform.tfvars file and then run terraform validate and terraform plan. If the output looks good, you can deploy the resources using terraform apply. Review the repo’s README for further details on how the Terraform plan works.

Authenticating with GCP from a CircleCI job using an OIDC token

This GitHub repo contains an example of how to authenticate with GCP using gcloud in a CircleCI job. The relevant config portions are shown below. The commands in this config expect four environment variables:

  • GCP_PROJECT_ID
  • GCP_WIP_ID
  • GCP_WIP_PROVIDER_ID
  • GCP_SERVICE_ACCOUNT_EMAIL

You can configure these in a context or at the project level. If you need to use multiple GCP projects, we recommend creating a context to hold the vars for each. If you need to use multiple service accounts within a GCP project, you can override the value of GCP_SERVICE_ACCOUNT_EMAIL using the environment key at the job level, as in this example. The verification steps in this example job assume that the service account has the permission to perform gcloud iam service-accounts get-iam-policy.

version: "2.1"

orbs:
  gcp-cli: circleci/gcp-cli@2.4.1

commands:
  gcp-oidc-generate-cred-config-file:
    description: "Authenticate with GCP using a CircleCI OIDC token."
    parameters:
      project_id: 
        type: env_var_name
        default: GCP_PROJECT_ID
      workload_identity_pool_id: 
        type: env_var_name
        default: GCP_WIP_ID
      workload_identity_pool_provider_id: 
        type: env_var_name
        default: GCP_WIP_PROVIDER_ID
      service_account_email: 
        type: env_var_name
        default: GCP_SERVICE_ACCOUNT_EMAIL
      gcp_cred_config_file_path: 
        type: string
        default: /home/circleci/gcp_cred_config.json
      oidc_token_file_path: 
        type: string
        default: /home/circleci/oidc_token.json
    steps:
      - run:
          command: |
            # Store OIDC token in temp file
            echo $CIRCLE_OIDC_TOKEN > << parameters.oidc_token_file_path >>
            # Create a credential configuration for the generated OIDC ID Token
            gcloud iam workload-identity-pools create-cred-config \
                "projects/${<< parameters.project_id >>}/locations/global/workloadIdentityPools/${<< parameters.workload_identity_pool_id >>}/providers/${<< parameters.workload_identity_pool_provider_id >>}"\
                --output-file="<< parameters.gcp_cred_config_file_path >>" \
                --service-account="${<< parameters.service_account_email >>}" \
                --credential-source-file=<< parameters.oidc_token_file_path >>
  gcp-oidc-authenticate:
    description: "Authenticate with GCP using a GCP credentials file."
    parameters:
      gcp_cred_config_file_path: 
        type: string
        default: /home/circleci/gcp_cred_config.json
    steps:
      - run:
          command: |
            # Configure gcloud to leverage the generated credential configuration
            gcloud auth login --brief --cred-file "<< parameters.gcp_cred_config_file_path >>"
            # Configure ADC
            echo "export GOOGLE_APPLICATION_CREDENTIALS='<< parameters.gcp_cred_config_file_path >>'" | tee -a $BASH_ENV
jobs:
  gcp-oidc-defaults:
    executor: gcp-cli/default
    steps:
      - gcp-cli/install
      - gcp-oidc-generate-cred-config-file
      - gcp-oidc-authenticate
      - run:
          name: Verify that gcloud is authenticated
          environment:
            GCP_SERVICE_ACCOUNT_EMAIL: jennings-oidc-test@makoto-workbench.iam.gserviceaccount.com
          command: gcloud iam service-accounts get-iam-policy "${GCP_SERVICE_ACCOUNT_EMAIL}"
      - run:
          name: Verify that ADC works
          command: |
              ACCESS_TOKEN=$(gcloud auth application-default print-access-token)
              curl -f -i -H "Content-Type: application/x-www-form-urlencoded" -d "access_token=${ACCESS_TOKEN}" https://www.googleapis.com/oauth2/v1/tokeninfo

workflows:
  main:
    jobs: 
      - gcp-oidc-defaults:
          name: Generate Creds File and Authenticate
          context: 
          - gcp-oidc-dev

Advanced OIDC usage with GCP

So far, we have walked through the minimum necessary configuration to authenticate with GCP using OIDC tokens. In line with the principle of least privilege, we recommend configuring your workload identity pool provider to restrict access based on the OIDC token’s claims. This can be done by adding an attribute condition to the provider in the form of a CEL expression.

The CEL expression can use the claims contained in CircleCI tokens to limit who is able to impersonate service accounts. In addition to some common standard claims, the tokens also contain claims for project ID and contexts.

Here is an example of an expression that will restrict access to a specific org and user:

attribute.org_id=='01234567-89ab-cdef-0123-4567890abcde' && 
google.subject.matches('org/([\da-f]{4,12}-?){5}/project/([\da-f]{4,12}-?){5}/user/76543210-ba98-fedc-3210-edcba0987654')

Here is another example that will restrict access to users who are able to access a specific context (see our documentation for more information on how to restrict access to a context):

attribute.org_id=='01234567-89ab-cdef-0123-4567890abcde' && 
attribute.context_id=='76543210-ba98-fedc-3210-edcba0987654'

Here is an example that will restrict access to any user from a specific project:

attribute.org_id=='01234567-89ab-cdef-0123-4567890abcde' && 
attribute.project_id=='76543210-ba98-fedc-3210-edcba0987654'

You can use these claims to restrict access to jobs in a single project or to users that have access to a specific context, respectively. For more details on the CircleCI OIDC token claims, see our documentation.

Conclusion

Great work! Jobs that used to use a context now contain CircleCI’s OpenID Connect token. Use these tokens to securely interact with cloud providers from your jobs, without the burden of key rotation.