Many Android and non-Android projects automate the task of building and deploying software using continuous integration, CI for short. In the context an Android app, an app might be distributed to the Play Store or Firebase App distribution, or integration tests might be run on Firebase Test Lab. All of these steps interact with Google APIs which require authentication.

A common way to authenticate the CI machine to these Google or Google Cloud Platform APIs is using a service account, with its credential being stored as CI secrets. However, this poses a potential security risk: these credentials do not expire and if they get obtained or leaked, a third party could potentially abuse these credentials to their own benefit.

In this post we’ll explore a better solution called workflow identity federation and we’ll go through the steps on how to set this up with Github Actions, although steps for other CI providers should be similar. Eventhough workflow identity federation is not strictly just for CI machines, we’ll focus on that for now.

What is Workflow Identity Federation?

Workflow Identity Federation is the Google Cloud term for the idea that the job on your CI machine runs on under some kind of account and is therefore authenticated and authorized to do things like check out your source code.

Instead adding a separate credential to “login” to Google APIs, Workflow Identity Federation allows us to authenticate with the Github Actions credential and exchange this credential for another credential that is valid for Google APIs.

This has the following benefits:

  • We don’t have to setup a separate credential anymore in Github Action secrets
  • The credential generated by Workflow Identity Federation is short lived, it expires after a configurable time (default: 1 hour). If an attacker obtains the credential it is of limited use, compared to a service account that never expires.

The google-github-actions/auth action documents how to configure this, however reading its documentation is a bit intimidating.

In fact, I didn’t even realise that workflow identity federation could replace all hardcoded service account usages, until Carter Jernigan (thanks!) pointed this out. The docs also use the gcloud command line, which is fine, but it’s hard to tell how this relates to the GCP console.

In this post I’ll use the GCP console for the setup that is documented there, so you can pick and choose the method you prefer.

Recap: what is a service account?

In Google APIs, a service account is used when a machine, not a human, needs to authenticate with an API or service. A service account is what GCP calls a principal (or: a user) which you can assign roles to, just like other users in GCP.

For Github Actions, you’d typically setup a new service account with the minimum amount of roles required. For example, if your CI job deploys an app to the Play Store, it would only have the roles to do that and not, for example, spin up a VM.

It is essential that your service account is limited in this way, also when using workflow identity federation, to reduce the risk of someone abusing these credentials. You should never grant a role like editor to a service account, since that effectively allows the service account to do anything on your Google Cloud project.

Setting up Workflow Identity Federation with Github Actions

The auth documentation mentions two ways to setup workflow identity federation:

  • Direct workflow identity federation, which is marked as preferred
  • Impersonating a service account

For our purposes, we will be impersonating a service account. This means that we’ll get a credential that will “login” as the service account. We’ll do this for two reasons:

  1. The APIs we’re dealing with require service accounts to create new resources or update resource and not just access to existing resources
  2. It makes migration easier with an existing service account in place + managing roles on a service account doesn’t change.

Setup the Workflow Identity Federation pool

We first need a workflow identity federation pool for the GCP project. The pool holds various identity providers, e.g. a provider for Github Actions and possibly providers for other services you might use.

A pool is a unit for management. You’d probably create a new pool for CI, but you can also use an existing one if appropriate. Create a new pool from the console here and give it a name like “Github” or whatever you prefer.

Add the identity provider

Now create the provider using the add provider button.

Create the identity provider

  • For the provider type, select OpenID Connect. This is what Github uses.
  • For the issuer url use https://token.actions.githubusercontent.com

Click continue to configure the attribute mappings.

What this does is mapping the claims, or attributes from the Github OpenID token. By default you need to provde a mapping for the google.sub attribute only, which should map to assertion.sub.

The Github token claims are documented here and you can map additional ones in the mapping screen. I recommend to add at least two additional attributes:

  • attribute.repository_owner to assertion.repository_owner
  • attribute.repository to assertion.repository

Setup attribute mappings

Adding these two mappings allows you to limit access to the provider and allows you to limit the usage of the service account we are going to map to specific repositories or repository owners, like your Github organisation.

Note that the google-github-actions/auth docs adds additional mappings in their examples.

As a final step, you should restrict the usage of the provider so that it can only be used by your own Github organisation or user. In the field Attribute Conditions add assertion.repository_owner == 'my-org' for this purpose.

Here we check that the repository_owner matches our my-org Github organisation. This means that all repositories for this organisation can use this particular provider. You can create more complex conditions or setup multiple providers for more fine-grained control. You could also use the repository claim that we mapped to restrict the use of the provider to a single repository.

The auth action documentation has additional security considerations documented to use the repository_id and repository_owner_id claims instead of the owner name and repository name. These ids are stable, which means that when the repository or owner is renamed the conditions on these attributes will still work and the names can not be squatted.

Grant access to the service account

We’re almost there! As a final step the provider needs to have access to the service account so that it can be impersonated.

From the details of the provider you just created, click the grant access button and select Grant access using Service Account impersonation. Pick the service account, and the attribute name and value. For example the attribute name can be repository with your particular repository name as the value.

Grant service account access

This means that even though the provider can be used for our entire organisation, the service account can only be used for a particular repository.

You can repeat this step to grant access for other repositories, map other attributes or grant access to the entire Github organisation using the repository_owner attribute.

Phew! This was a lot, but we made it…for the GCP part.

Setup or change the Github Actions workflow

To actually use workflow identity federation in new or existing workflows, we need to take these steps:

  • Grant access to the Github token in the workflow
  • Add the google-github-actions/auth action
  • Remove any existing service account credentials from Github Action Secrets and revoke the keys.

Using the Github token

Add the following permissions to the workflow to fetch the id token, this usually goes in the jobs block for your job.

jobs:
  build:
    permissions:
      contents: "read"
      id-token: "write"

Add the auth action

Add a step in your workflow to authenticate to Google Cloud. You’ll need to specify your GCP project id, the name of your service account and the workload_identity_provider string.

- name: "Setup GCP auth"
  uses: "google-github-actions/auth@v2"
  id: auth
  with:
    token_format: "access_token"
    project_id: "your-project-id"
    workload_identity_provider: "projects/123456789/locations/global/workloadIdentityPools/github/providers/github"
    service_account: "github@your-project-id.iam.gserviceaccount.com"

Finding the workload_identity_provider value and getting it right can be tricky. You can get it through gcloud but you can also navigate to the details of your provider (note: not the pool!). Under Default audience you’ll find a string similar to https://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/<pool>/providers/<provider>. Just copy the project/... part over to the workflow file.

Remove whatever step you were using to get the stored service account. If the workflow runs succesfully you can now remove the service account secret and revoke the key from the GCP console. Yay! We did it!

Summary

  • We’ve setup a work flow identity provider pool with a new provider for Github.
  • We made sure the pool can only be used by our Github organisation, a specific user or repository.
  • We granted access to our existing service account based on our Github organisation, a specific user or repository
  • We updated the existing workflow to let it read the id-token and added google-github-actions/auth action to exchange that token with a temporary credential to login to Google APIs impersonating the existing service account.
  • We finally removed the service account credentials from Github Action Secrets and revoked the service account json key.

Happy securing your CI credentials!