Thursday, December 5, 2024
No menu items!
HomeCloud ComputingConfiguring Workload Identity Federation for GitHub actions and Terraform Cloud

Configuring Workload Identity Federation for GitHub actions and Terraform Cloud

Join us as we build on the concept and use cases of Workload Identity Federation, showcasing the security benefits of “keyless authentication.” We will dive into how Workload Identity Federation can be used in the context of CI/CD pipelines and tools that are commonly found in enterprise environments. 

Workload Identity Federation can be integrated with external providers, such as Gitlab, GitHub actions, and Terraform Cloud. We will show how the tokens issued by the external providers can be mapped to various attributes and how it can be used to evaluate conditions to restrict which identities can authenticate. 

Prerequisites

A Google Cloud project

A GitHub repository

A GitHub Actions workflow

Terraform Cloud Account/ Workspace

Scenario 1:

Objective: Demonstrate how two separate GitHub repositories and their corresponding CI/CD pipelines use separate service accounts to access respective Google Cloud resources in the least-privileged access manner.

In this scenario, we will configure the workload identity pool to check for a custom attribute “repository” and validate that the corresponding service account is used for a given GitHub repository.

Instructions

1. Create a workload identity pool

It is recommended to create and manage workload identity pools from a single dedicated project as per best practices.

code_block[StructValue([(u’code’, u’gcloud iam workload-identity-pools create gitHub-actions-pool \rn–location=”global” \rn–description=”The pool to authenticate GitHub actions.” \rn–display-name=”GitHub Actions Pool”‘), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3ece896b8f10>)])]

2. Create a workload identity pool provider 

Note the purpose of some arguments as explained below. 

a. The issuer-uri identifies GitHub as the identity provider and lets workload identity discover the OIDC metadata and JSON Web Key Set(JWKs) endpoints. Google Cloud uses these endpoints to validate tokens. 

b. GitHub issues a unique token for each workflow job. This token contains claims that describe the identity of the workflow. By using an Attribute mapping, we translate the claims in the token so that we can use them in principal/principalSet identifiers to grant access to service accounts, or to create an attribute condition. 

c. Attribute-condition: After mapping the attributes, we are asserting conditions for authentication. In this case, we only allow authentication requests from GitHub Actions workflows on any repository from the “sec-mik” GitHub organization. Either assertions or previously mapped attributes can be used to build the condition.

code_block[StructValue([(u’code’, u’gcloud iam workload-identity-pools providers create-oidc GitHub-actions-oidc \rn–workload-identity-pool=”GitHub-actions-pool” \rn–issuer-uri=”https://token.actions.GitHubusercontent.com/” \rn–attribute-mapping=”google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner,attribute.branch=assertion.sub.extract(‘/heads/{branch}/’)” \rn–location=global \rn–attribute-condition=”assertion.repository_owner==’sec-mik'”‘), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3ece7cc96950>)])]

In this case, we are mapping the subject, repository, repository owner, and branch attributes from the claims in the token to GCP assertions. CEL (Common Expression Language) expression is used to extract the branch from the subject claim in the token and map it to the custom attribute “branch.” 

3. Create a service account for each repository and assign them appropriate IAM permissions

In this scenario, one repository/service account will be for an application team that will deploy a Hello World app to Cloud Run and one repository will be owned by the networking team.

code_block[StructValue([(u’code’, u’gcloud iam service-accounts create example-app-sa –display-name=”Example Application Service Account” –description=”manages the application resources”rnrngcloud iam service-accounts create networking-sa –display-name=”Networking Service Account” –description=”manages the networking resources”‘), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3ece8886f250>)])]

As a result, the networking-sa service account will have “networking” related IAM permissions and the application team will have cloud run and associated IAM permissions.

4. Add IAM bindings for the workload pool 

We will utilize previously mapped attributes to create principals/principalSets, and then use them in IAM bindings to grant permissions to create short-lived credentials for the appropriate service account. In this case, we want to map the networking service account (“networking-sa”) with the networking GitHub repository that is encapsulated in the “repository” attribute.

code_block[StructValue([(u’code’, u’gcloud iam service-accounts add-iam-policy-binding [email protected] \rn –role=”roles/iam.workloadIdentityUser” \rn–member=”principalSet://iam.googleapis.com/projects/987654321/locations/global/workloadIdentityPools/GitHub-actions-pool/attribute.repository/sec-mik/networking-repo”‘), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3ece7c9cc290>)])]

In accordance with Google Cloud best practices, we recommend that you use the system-generated IDs such as repository_id, rather than repository name, because it is immutable and does not change between renames of entities such as renaming a repository.

Next, we will repeat the same process to associate the application team’s service account with the “application-repo” repository. In this case, we will map against a “subject” assertion that represents a specific repository name and branch, instead of a custom attribute. The “sub” claim in the JWT is comprehensive and predictable, as it is assembled from concatenated metadata, and is therefore the preferred mapping option in most cases.

code_block[StructValue([(u’code’, u’gcloud iam service-accounts add-iam-policy-binding [email protected] \rn –role=”roles/iam.workloadIdentityUser” \rn–member=”principal://iam.googleapis.com/projects/987654321/locations/global/workloadIdentityPools/GitHub-actions-pool/subject/repo:sec-mik/application-repo:ref:refs/heads/main”‘), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3ece8a8b4b90>)])]

Once the IAM bindings are applied, we will observe it like this from Google Cloud Console workload identity provider page.

5. Update the GitHub Actions workflow to use the workload identity pool to authenticate to Google Cloud.

a. The workload identity pool (WORKLOAD_IDENTITY_PROVIDER) is configured as “projects/987654321/locations/global/workloadIdentityPools/GitHub-actions-pool/providers/GitHub-actions-oidc” and service account email (SERVICE_ACCOUNT_EMAIL) as “[email protected]“. 

b. Once authenticated, our pipeline will run two jobs, which are oriented towards a networking and application deployment.

code_block[StructValue([(u’code’, u’name: apply on mergern # Allows you to run this workflow on mergern push:rn branches:rn – $default-branchrnenv:rn REGION: us-central1 # TODO: update Cloud Run service regionrn rn# A workflow run is made up of one or more jobs that can run sequentially or in parallelrnjobs:rn GitHub-actions-wif:rn # Allow the job to fetch a GitHub ID tokenrn permissions:rn id-token: writern contents: readrnrn runs-on: ubuntu-latestrnrn steps:rn – uses: actions/checkout@v3rn rn – id: ‘auth’rn name: ‘Authenticate to Google Cloud’rn uses: ‘google-GitHub-actions/auth@v1’rn with:rn create_credentials_file: truern workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}rn service_account: ${{ secrets.SERVICE_ACCOUNT_EMAIL }}rn rn – name: ‘Networking activities’rn run: |-rn gcloud auth login –brief –cred-file=”${{ steps.auth.outputs.credentials_file_path }}”rn gcloud compute firewall-rules list –format=”table(rn name,rn network,rn direction,rn priorityrn )”rn continue-on-error: truern rn – id: ‘deploy-cloud-run’rn name: ‘Deploy Cloud Run Hello World Python App’rn uses: ‘google-GitHub-actions/deploy-cloudrun@v1’rn with:rn service: ‘hello-world’rn region: ‘us-central1’rnrnrn – name: ‘Use output’rn run: ‘curl “${{ steps.deploy-cloud-run.outputs.url }}”‘rn continue-on-error: true’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3ece8a728910>)])]

We run this GitHub action from the application-repo where the cloud run deployment succeeds, but the networking command (listing firewalls) fails.

6. Analyzing the Cloud Audit Logs

We need to enable IAM and STS Data Access Logs on the Audit Logsof all the projects that contain workload identity pools and service accounts used for workload identity federation; to detect any IAM violations and observe how token exchange works between GitHub and STS API as shown below. 

First, the `auth` action uses the STS API to retrieve a short lived federated access token. This token exchange may fail due to attribute condition mismatch or configuration error.

The following is a log of the token exchange.

Please observe the following parameters in the image:

protoPayload.authenticationInfo.principalSubject: The subject from the GitHub JWT token i.e. repo:sec-mik/networking-repo:ref:refs/heads/main

protoPayload.metadata.mapped_principal: The subject of the token, using IAM syntax to identify the principal i.e. principal://iam.googleapis.com/projects/987654321/locations/global/workloadIdentityPools/GitHub-actions-pool/subject/repo:sec-mik/networking-repo:ref:refs/heads/main

Next, the `auth` action uses the IAM Credentials API to obtain a short-lived credential for a service account. Failure may occur due to either the absence of the workloadIdentityUser IAM role or the incorrect principal attempting to create the token.

The following is a log of the creation of short-lived credentials for service accounts.

Please observe the following parameters in the image:

protoPayload.authenticationInfo.principalSubject: The subject of the federated token. i.e. principal://iam.googleapis.com/projects/987654321/locations/global/workloadIdentityPools/GitHub-action-pool/subject/repo:sec-mik/application-repo:ref:refs/heads/main

metadata.identityDelegateChain: The service account for which short-lived credentials are generated, such as [email protected]

Refer to log examples for more details.

Summary

To summarize, we saw how a GitHub repository was mapped as a principal to authenticate with Google Cloud using a GitHub OIDC token, which was subsequently exchanged for Google Cloud credentials. Once authenticated, the IAM bindings for the corresponding service account performed authorization checks and was granted access accordingly.

Scenario 2

Objective: Demonstrate how two Terraform Cloud workspaces can use two separate but corresponding service accounts for provisioning. 

In this scenario, we will explore another popular tool in the IaC space that is commonly used as an orchestrator, Hashicorp Terraform Cloud and see how workload identity federation can be used in a similar fashion. 

Below is an overview of what we will demonstrate.

Mappings (GitHub repo → Terraform Cloud Workspace → Google Cloud Service Account → Google Cloud project )

app-repo-dev → app-ws-dev → app-sa-dev → dev-secmik 

app-repo-prod → app-ws-prod → app-sa-prod → production-secmik

Instructions

1. Create a workload identity pool for dev and prod environment

In this scenario, we will create a separate workload identity pool to follow Google Cloud best practices which recommends having one-to-one mapping between external identity provider and workload identity pool to prevent subject collisions. The format of the subject is “organization:my-org:project:Default Project:workspace:my-workspace:run_phase:apply” and the reference to workspace will be different for dev and prod environments in our scenario.

code_block[StructValue([(u’code’, u’gcloud iam workload-identity-pools create tf-cloud-oidc-dev \rn–location=”global” \rn–description=”The pool to authenticate TF cloud dev workspaces.” \rn–display-name=”Terraform Cloud Dev Pool”rnrngcloud iam workload-identity-pools create tf-cloud-oidc-prod \rn–location=”global” \rn–description=”The pool to authenticate TF cloud prod workspaces.” \rn–display-name=”Terraform Cloud Prod Pool”‘), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3ece89718410>)])]

2. Create a workload identity pool provider 

The next step is to configure the provider within the workload identity pool. Before this, it is important to review the various claims that are part of the Terraform JWT token. The few important claims from the JWT token are:

iss : The issuer of the token. 

terraform_full_workspace : The full workspace name.

terraform_project_id : The ID of the project that the workspace is associated with.

terraform_workspace_id : The ID of the workspace.

In the dev and prod workload identity pool configuration, the CEL condition will limit access to identity tokens from a specific workspace (corresponding to dev/ prod) within a Terraform Cloud organization and Project.

code_block[StructValue([(u’code’, u’gcloud iam workload-identity-pools providers create-oidc tf-cloud-oidc-provider \rn–workload-identity-pool=”tf-cloud-oidc-dev” \rn–issuer-uri=”https://app.terraform.io” \rn–attribute-mapping=”google.subject=assertion.sub,attribute.terraform_workspace_id=assertion.terraform_workspace_id, attribute.terraform_full_workspace=assertion.terraform_full_workspace” \rn–location=global \rn–attribute-condition=”assertion.terraform_organization_id=’secmik’ && terraform_workspace_name.startsWith(‘app-ws-dev’)”‘), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3ece897249d0>)])]
code_block[StructValue([(u’code’, u’gcloud iam workload-identity-pools providers create-oidc tf-cloud-oidc-provider \rn–workload-identity-pool=”tf-cloud-oidc-prod” \rn–issuer-uri=”https://app.terraform.io” \rn–attribute-mapping=”google.subject=assertion.sub,attribute.terraform_workspace_id=assertion.terraform_workspace_id, attribute.terraform_full_workspace=assertion.terraform_full_workspace” \rn–location=global \rn–attribute-condition=”assertion.terraform_organization_id=’secmik’ && terraform_workspace_name.startsWith(‘app-ws-prod’)”‘), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3ece89724990>)])]

3. Create Google Cloud Service accounts

Create two service accounts that will be used by separate workspaces.

main workspace: “app-sa-prod” 

develop workspace: “app-sa-dev”

4. Add IAM bindings for the workload pool

Add IAM bindings for the workload pool providers that were created earlier. In this scenario, IAM verifies the workspace ID attribute before authorizing access to impersonate a service account. Workspace ID is immutable, meaning it cannot be changed, so if the workspace is deleted and recreated with the same name, no accidental access will be granted as per best practice from Google.

code_block[StructValue([(u’code’, u’gcloud iam service-accounts add-iam-policy-binding [email protected] \rn –role=”roles/iam.workloadIdentityUser” \rn–member=”principal://iam.googleapis.com/projects/987654321/locations/global/workloadIdentityPools/tf-cloud-oidc-dev/attribute.terraform_workspace_id/ws-XXXXXXXXXXXXXXXX”‘), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3ece89724d50>)])]
code_block[StructValue([(u’code’, u’gcloud iam service-accounts add-iam-policy-binding [email protected] \rn –role=”roles/iam.workloadIdentityUser” \rn–member=”principal://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/tf-cloud-oidc-prod/attribute.terraform_workspace_id/ws-ZZZZZZZZZZZZZZZZ”‘), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3ece897dabd0>)])]

Once the IAM bindings are applied, we will observe it like this on Google Cloud Console’s workload identity provider page.

5. Terraform Cloud Configuration

We will specify the workload pool related configuration as a workspace variable as shown below. The variables can be classified as sensitive to prevent them from being viewed on the UI or in logs. For further information, please refer to HashiCorp’s public documentation.

TFC_GCP_PROVIDER_AUTH : Terraform Cloud will use dynamic credentials to authenticate to GCP.

TFC_GCP_RUN_SERVICE_ACCOUNT_EMAIL: The service account email address that Terraform Cloud will use to authenticate to Google Cloud.

TFC_GCP_WORKLOAD_PROVIDER_NAME: The canonical name of the workload identity provider. Format is : projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_NAME/providers/PROVIDER_NAME

If the credential exchange fails due to a condition check, one would see an error similar to one below:

If the token generation fails due to permissions, one would see an error similar to one below

A successful deployment would result in something like this:

6. Auditing Workload Identity Federation service accounts

To obtain an organization-wide view of all service accounts that have been provisioned to use Workload Identity Federation, follow these steps:

In the Policy Analyzer, select your organization.

Select the Workload Identity User role as a parameter.

Do not select any checkboxes.

Click Analyze.

The results will be all service accounts that have been provisioned to be used with Workload Identity Federation, including their attribute conditions.

Get started today

To get started with Workload Identity Federation with CI/CD pipelines, see Configure workload identity federation with deployment pipelines.

Related Article

Enabling keyless authentication from GitHub Actions

Authenticate from GitHub Actions to create and manage Google Cloud resources using Workload Identity Federation.

Read Article

Cloud BlogRead More

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments