This is me, André König - a software engineer from Hamburg, Germany. André König

Workload Identity Federation for Secure Access to Google Cloud Platform via GitHub Actions

Learn how to secure your interaction between GitHub Actions and the Google Cloud Platform with keyless authentication.

When deploying your service to the Google Cloud Platform via GitHub Actions, you have to make sure that the runner is authenticated against the platform. In the past, you might have used a service account, given it the proper roles, and created a key file, which you then mounted into your GitHub Actions flow via a repository secret.

Although this approach works, it comes with a major (security) downside: The key file is like a password that gets shared between you, the GCP and GitHub. You're a security-sensitive engineer; I know that, but I guess you also ran into the situation in which you forgot to rotate such a key file in the past, right? It happens to the best.

Luckily, there is an alternative approach – an approach where the runner will authenticate against the Google Cloud Platform without messing around with credentials at all.

This keyless authentication mechanism is called Workload Identity Federation, and we will dive into this topic in this article.

Welcome to the Cloud Club!

Imagine you are hosting a grand party at a posh club. You have guests coming in from all corners of the city. But there's a catch - the club has a strict entry policy. Each guest needs to show their club membership card to get in.

Now, some of your friends are members of different clubs. They have their own membership cards, but not for the club where you're hosting the party. Wouldn't it be great if your club could recognize and accept some other membership cards too?

This is essentially what the Workload Identity Federation does on the Google Cloud Platform. It's like the bouncer of your 'Cloud Club', allowing workloads (your party guests) from other cloud platforms or on-premises systems (other clubs) to authenticate themselves in Google Cloud using their own platform's identity system (their own club cards).

It's a way of saying, "Hey GCP, my application is already authenticated in its current environment. Can you trust that and let it in?"

This way, your applications can easily communicate with Google Cloud resources without the need for service account keys. It's simpler, safer, and much more efficient!

With that said, back to our GitHub Actions runner: This buddy is also a party guest from the 'GitHub Club' - a club we trust.

Direct or Indirect?

Workload Identity Federation comes in two flavors:

Indirect Workload Identity Federation (through a Service Account)

In this approach, external identities (like those from a different cloud provider, on-premises system, or even a different Google Cloud project) are mapped to a service account in your Google Cloud project. When these external identities authenticate, they assume the identity of the service account.

This is akin to a guest at your party (the external identity) wearing a fancy mask (the service account) to gain access. The club bouncer (GCP) recognizes the mask, not the guest behind it. This method requires the management of service account permissions and can be used with a wider range of Google Cloud services.

Direct Workload Identity Federation

In this newer approach, no service account is involved. Instead, Google Cloud directly recognizes the external identity. Using our party analogy, it's like the club bouncer (GCP) letting your guest (the external identity) in because they recognize them directly; no mask is needed.

This approach simplifies the management overhead as you don't have to create and manage service accounts.

Which one to choose?

As everything in computer science – it depends. The "better" approach with less management overhead is the direct workload identity federation. Please take this with a grain of salt though as not all services on the Google Cloud Platform are supporting this mechanism yet. To stick with our party analogy above: The bouncer can let you in, but not grant you access to all areas of the party.

Direct Workload Identity Federation in Action

Because direct workload identity federation might play a vital role in the future, we'll dive deeper into it and implement the following use case:

A GitHub Actions workflow should be able to build a container image and publish it to the Artifact Registry. When done, it should deploy a container via Google Cloud Run.

Prerequisites

You need:

  1. A Google Cloud Platform project.
  2. The Google Cloud SDK: The good news is that you don't have to mess around with installing it. Just open the Cloud Shell, it comes with gcloud pre-installed.

And some environment variables:

export PROJECT_ID=<your-gcp-project-id> # e.g. pool-party-v1
export REGION=<your-gcp-region> # e.g. europe-west1
export GITHUB_ORG=<github-org> # e.g. getactions
export GITHUB_REPO_NAME=<github-org/repo-name> # e.g. getactions/app
export ARTIFACT_REPOSITORY_NAME=<name> # e.g. my-awesome-app
export CLOUD_RUN_SERVICE_NAME=<name> # e.g. my-awesome-app
export DEFAULT_COMPUTE_SERVICE_ACCOUNT=<email> # e.g. <id>[email protected]
  • GITHUB_REPO_NAME is required to make sure that only GitHub Actions workflows from this particular repository are allowed to authenticate.
  • DEFAULT_COMPUTE_SERVICE_ACCOUNT: The email address of the default compute service account can be found in the service account overview.

Building a Pool

The first thing you have to do is creating a so-called Workload Identity Pool. In Google Cloud, a Workload Identity Pool is like a container for storing configurations that define how external identities (from other clouds, on-premises systems, or even different Google Cloud projects) can be recognized and authenticated in your Google Cloud environment.

The general recommendation is to create a new pool for each non-Google Cloud environment that needs access to resources on the Google Cloud Platform.

Here, we create a pool for our GitHub Actions runners:

gcloud iam workload-identity-pools create "github-actions" \
--project="$PROJECT_ID" \
--location="global" \
--display-name="The GitHub Actions pool"

Now that the pool is created, we have to define the provider, which represents a configuration that tells the GCP how to communicate with an external identity provider.

Each provider configuration includes details like:

  • The issuer's URL of the external identity provider
  • The mapping of attributes from the external identity provider's token to Google's token.
  • Optional but highly recommended: An attribute-based access control condition.

So let's tell Google how to communicate with the GitHub identity provider:

gcloud iam workload-identity-pools providers create-oidc "github" \
--project="$PROJECT_ID" \
--location="global" \
--workload-identity-pool="github-actions" \
--display-name="GitHub" \
--attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
--attribute-condition="assertion.repository_owner == '$GITHUB_ORG'" \
--issuer-uri="https://token.actions.githubusercontent.com"

Looks pretty complex. Let's break it down:

  • --workload-identity-pool: We associate this provider with the pool from above.
  • --attribute-mapping: Here, we tell Google how to map the attributes from the GitHub token to the Google token.
  • --attribute-condition: We only allow users who are part of the $GITHUB_ORG. All others will be bounced.

Briefing the Bouncer

Cool, the actual authentication is in place. Now, we have to tell the bouncer what the guests can do. The rule is that every guest will be thrown into the cold water of the pool. After they jump out of the pool, they are allowed to push container images and deploy containers. Sounds like a great party, eh?

In order to detect which guest was in the pool, the bouncer needs to be aware of the actual pool ID, which they can determine via:

export POOL_ID=$(gcloud iam workload-identity-pools providers describe "github" \
--project="$PROJECT_ID" \
--location="global" \
--workload-identity-pool="github-actions" \
--format="value(name)")
echo $POOL_ID

Allow to push container images

Next up, we'll define an IAM policy binding stating that all guests who jumped out of the icy pool are allowed to push container images to the Artifact Registry.

Important: Please make sure that the Artifact Registry repository was created before.

gcloud artifacts repositories add-iam-policy-binding "$ARTIFACT_REPOSITORY_NAME" \
--project="$PROJECT_ID" \
--location="$REGION" \
--role="roles/artifactregistry.writer" \
--member="principalSet://iam.googleapis.com/$POOL_ID/attribute.repository/$GITHUB_REPO_NAME"

As you can see, the value of the member is not an actual user ID, but a construct with the esoteric name principalSet. It basically says: Users need to be authenticated via the GitHub Workload Identity Pool and have the specific repo name in their token. The GitHub Actions workflow will be performed within a particular repository, and this statement defines that only the authenticated GitHub user who is executing the workflow is allowed to push to the specific Artifact Registry repository.

Allow to deploy container images

The last two steps allow the guests to deploy container images to Google Cloud Run.

gcloud run services add-iam-policy-binding "$CLOUD_RUN_SERVICE_NAME" \
--project="$PROJECT_ID" \
--region="$REGION" \
--role="roles/run.developer" \
--member="principalSet://iam.googleapis.com/$POOL_ID/attribute.repository/$GITHUB_REPO_NAME"

Pretty similar to the definition for the Artifact Registry, we say that the user who is performing the GitHub Actions workflow is allowed to deploy via Google Cloud Run.

The last, but pretty important, step is to make sure that the user who deploys the container can act as the default compute service account during deployment. This service account is allowed to deploy on the managed Cloud Run platform. You can read more about this step (and possible alternatives) in the Cloud Run IAM documentation.

gcloud iam service-accounts add-iam-policy-binding \
$DEFAULT_COMPUTE_SERVICE_ACCOUNT \
--member="principalSet://iam.googleapis.com/$POOL_ID/attribute.repository/$GITHUB_REPO_NAME" \
--role="roles/iam.serviceAccountUser"

Using Workload Identity Federation in GitHub Actions

The Google Cloud Platform is prepared for welcoming your GitHub Actions workflow runs. The following describes an example workflow which authenticates, builds and publishes a container image and deploys the container on Google Cloud Run.

It is completely keyless, and you don't have to worry about rotating key files anymore. To make the workflow more readable, I moved the dynamic parts into secrets, although most of them are not really "secrets":

  • REGION: Your GCP region where your Artifact Registry repository lives and where you want to deploy your container.
  • PROJECT_ID: The ID of your Google Cloud Platform project.
  • APP_NAME: The name of your application (also name of the container image).
  • POOL_ID: The ID of your authentication pool (see above).
name: Deploy to Google Cloud Run
on:
push:
branches:
- main
env:
REGION: ${{ secrets.REGION }}
IMAGE_URL: ${{ secrets.REGION }}-docker.pkg.dev/${{ secrets.PROJECT_ID }}/${{ secrets.REPO_NAME }}/${{ secrets.APP_NAME }}:latest
jobs:
build-deploy:
name: Build and Deploy
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- name: Authenticate with Google Cloud
uses: google-github-actions/auth@v2
with:
project_id: ${{ secrets.PROJECT_ID }}
workload_identity_provider: ${{ secrets.POOL_ID }}
export_environment_variables: 'true'
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
with:
project_id: ${{ secrets.PROJECT_ID }}
- name: Configure Docker for Google Artifact Registry
run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev
- name: Build container image
run: docker build . --tag ${{ env.IMAGE_URL }}
- name: Push container image
run: docker push ${{ env.IMAGE_URL }}
- name: Deploy to Cloud Run
run: |
gcloud run deploy ${{ secrets.APP_NAME }} \
--image ${{ env.IMAGE_URL }} \
--platform managed \
--region ${{ env.REGION }} \
--allow-unauthenticated

Conclusion

Implementing Workload Identity Federation on the Google Cloud Platform through GitHub Actions is a major step forward in enhancing security and efficiency. The traditional approach of sharing key files between platforms has been replaced by a keyless authentication mechanism, much like a club recognizing membership cards from other clubs. The two ways of the Workload Identity Federation, namely the Indirect Workload Identity Federation and the Direct Workload Identity Federation, offer flexibility in the management of service accounts and the level of service support. The choice between the two depends largely on your specific needs and the services you intend to use on the Google Cloud Platform.

In the practical implementation of Direct Workload Identity Federation, we walked through creating a Workload Identity Pool, defining the provider, setting the authentication, and defining what the authenticated users can do. This process allows your GitHub Actions workflow to build a container image, publish it to the Artifact Registry, and deploy a container to Google Cloud Run, all without the need for service account keys.

This transition to a more secure and efficient mechanism of authentication is a testament to the continuous evolution of cloud technology. As we continue to explore and implement these advancements, we are not only improving our workflows, but also securing our systems and data against potential threats.

Remember, like any good party host, it's essential to ensure that your 'guests' (in this case, your applications) are who they say they are and that they have the appropriate 'access' to your 'party' (your Google Cloud resources). This is where Workload Identity Federation shines, ensuring your cloud 'party' is always secure, efficient, and well-managed.


Thank You

I hope that you found this article insightful and valuable for your journey. If so, and you learned something new or would like to give feedback then let's connect on X at @ItsAndreKoenig. Additionally, if you need further assistance or have any queries, feel free to drop me an email or send me an async message.

One last thing!

Let me know how I'm doing by leaving a reaction.


You might also like these articles