Github OIDC Integration with GCP -Workload Identity Federation
OpenID Connect, or OIDC in short, is an authentication extension on top of the well known OAuth 2.0 authorization standard. Where OAuth 2.0 ensures that you can do what you may do, OIDC will make sure that you are who you say you are.
OIDC solves both the identity problem (who is trying to access my data) and the key storage problem by generating short lived tokens for only the people that you trust. These short lived keys are regenerated each time the client tries to read the data so storing them is pointless. This directly eliminates the need for secure, long term storage.
Checking the identity of the caller in the OIDC protocol is done via an OIDC capable Identity Provider (IdP) you trust. This IdP will generate a JSON Web Token (JWT) which contains data about your identity together with a cryptographic signature so other parties can check the validity of the token.
The receiving end, in our case Google’s security token service (STS), can then check the token and determine if that person may have access to the data. If so, it will receive a temporary token which can be used in the next session for a limited amount of time.
Since no keys need to be manually exchanged nor stored you don’t have to worry anymore about malicious users stealing your long living keys.
Configuring access to your GCP environment
In the Google Cloud ecosystem the usage of multiple identities of multiple identity providers is called workload identity federation. To enable it you have to create a workload identity pool. This pool contains all the identity providers you trust and will be checked each time a person tries to get a short lived access token by the Google STS. The final piece of the puzzle is giving the workload identity pool access to impersonate one or more Google Cloud service accounts with the correct permissions to get the job done.
How does it work?
The flow here is that when a request comes into your pool the JWT token is validated and the claims are parsed. You can set up mappings between JWT token claims and WIF custom attributes.
JWT claims are the key/value pairs you’ll find set within a JWT token. They’re generated by GitHub and contain metadata about the workflow that the auth request originates from.
Custom Attributes are a lot like the JWT claims, but they exist on the GCP side. They’re key/value pairs that get assigned to an incoming WIF request that can be used to match against rules that allow access to service accounts.
What do the JWT claims look like?
Below you can see an example of the decoded claims from a GitHub Actions OIDC token. It’s really useful to be able to examine them in order to help you construct meaningful mappings in your WIF pool.
{
"actor": "github-bot",
"actor_id": "123450642",
"aud": "https://github.com/bbeesley",
"base_ref": "",
"enterprise": "bbeesley",
"environment": "staging",
"environment_node_id": "EN_kwDOI1Vzn84yRhwA",
"event_name": "deployment",
"exp": 1677506298,
"head_ref": "",
"iat": 1677505998,
"iss": "https://token.actions.githubusercontent.com",
"job_workflow_ref": "bbeesley/gql-federated-graph-explorer/.github/workflows/deployment.yml@d06e49a10fcaf3c8f71f9428949e6fedb9f07b09",
"job_workflow_sha": "d06e49a10fcaf3c8f71f9428949e6fedb9f07b09",
"jti": "ef405b0e-5507-4aa6-8685",
"nbf": 1677505398,
"ref": "",
"ref_type": "branch",
"repository": "bbeesley/gql-federated-graph-explorer",
"repository_id": "592302719",
"repository_owner": "bbeesley",
"repository_owner_id": "21031",
"repository_visibility": "private",
"run_attempt": "1",
"run_id": "4283094502",
"run_number": "34",
"runner_environment": "github-hosted",
"sha": "d06e49a10fcaf3c8f71f9428949e6fedb9f07b09",
"sub": "repo:bbeesley/gql-federated-graph-explorer:environment:staging",
"workflow": "Deployment 🚀",
"workflow_ref": "bbeesley/gql-federated-graph-explorer/.github/workflows/deployment.yml@d06e49a10fcaf3c8f71f9428949e6fedb9f07b09",
"workflow_sha": "d06e49a10fcaf3c8f71f9428949e6fedb9f07b09"
}
The easiest way to observe these claims while setting up your WIF mappings is to add the following steps to your workflows so you can see the generated claims:
permissions:
id-token: write # This is required for requesting the JWT
steps:
- name: Checkout actions-oidc-debugger
uses: actions/checkout@v3
with:
repository: github/actions-oidc-debugger
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
path: ./.github/actions/actions-oidc-debugger
- name: Debug OIDC Claims
uses: ./.github/actions/actions-oidc-debugger
with:
audience: 'projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/YOUR_PROJECT_ID/providers/YOUR_WIF_PROVIDER_POOL_NAME'
This will spit out the claims for the job so you can confirm any values match your expectations.
Some users might want to run terraform code in order to create resources on Google cloud Platform through their Github CI/CD pipeline. While others might want to run ‘gcloud’ commands in order to make certain configurations. Both of these options need to call GCP APIs. And GCP apis need authentication and authorisation when you call them. One possible (but not recommended) way can be to download GCP Service Account Keys and store them in Github repos as secrets or on Github private runners as files. These service account keys then can be used to make GCP apis calls. Securing, storing, distributing, rotating, monitoring these keys in the Github environment can be a very challenging and hectic task in itself. That’s why it’s not recommended. GCP provides a safer way to achieve the same using Workload Identity Federation. In this article I will try to describe how GCP WIF works with Github Provider using a step wise step approach.
Step 1
In this, the user asks GCP to trust Github Provider. This is done by creating Workload Identity Pool, Workload Identity Provider and IAMs. Let’s see each one of these separately.
Workload Identity Pool: Workload identity pools are used to organise and manage external identities. It is recommended to create a new pool for different non google cloud environments. Below command can be used to create the same:
gcloud iam workload-identity-pools create github-wif-pool --location="global" --project <project_id>
Workload Identity Provider: Workload Identity Provider describes the relationship between an external identity such as Github and Google Cloud. It basically establishes trust between external identity and GCP. It provides attribute mapping that applies the attributes from an external token to a Google token. This lets IAM use tokens from external providers to authorize access to Google Cloud resources. This is basically a way to translate external tokens into GCP equivalent tokens. Below command can be used to create the same:
gcloud iam workload-identity-pools providers create-oidc githubwif \
--location="global" --workload-identity-pool="github-wif-pool" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="attribute.actor=assertion.actor,google.subject=assertion.sub,attribute.repository=assertion.repository" \
--project <project_id>
Service Account and IAMs: We need a service account with relevant permissions assigned. WIF will impersonate this service account. We also add permissions to allow authentications from the Workload Identity Provider provided identity to impersonate the desired Service Account. It allows Github action to impersonate the service account and get a token. Please note the use of attribute ‘repository’ in member name. It allows IAM to authenticate only requests coming from the repository ‘PradeepSingh1988/gcp-wif’. If we have not used this, then all the github repos which are using a specific workload identity provider can authenticate against IAM. Below command can be used to create the same:
gcloud iam service-accounts create test-wif \
--display-name="Service account used by WIF POC" \
--project <project_id>
gcloud projects add-iam-policy-binding <project_id> \
--member='serviceAccount:test-wif@<project_id>.iam.gserviceaccount.com' \
--role="roles/compute.viewer"gcloud iam service-accounts add-iam-policy-binding test-wif@<project_id>.iam.gserviceaccount.com \
--project=<project_id> \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/<project_number>/locations/global/workloadIdentityPools/github-wif-pool/attribute.repository/PradeepSingh1988/gcp-wif"
There are options to restrict authentication from specific branches too. For that we need to use the principal like below.
principal://iam.googleapis.com/projects/<project_number>/locations/global/workloadIdentityPools/POOL_ID/subject/repo:PradeepSingh1988/gcp-wif:ref:refs/heads/main
This ensures that only the main branch of repo PradeepSingh1988/gcp-wif can authenticate.
Step 2
For this step I would like to give an example of a simple github workflow in my repo which has below steps.
name: wif-ci
on:
push:
branches:- 'main'
jobs:
build:
name: "Test WIF"
runs-on: ubuntu-latest
timeout-minutes: 90
permissions:
contents: 'read'
id-token: 'write'
steps:
- name: Checkout
uses: actions/checkout@v2
- id: auth
uses: google-github-actions/auth@v0.4.0
with:
token_format: "access_token"
create_credentials_file: true
activate_credentials_file: true
workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER_ID }}
service_account: ${{ secrets.SERVICE_ACCOUNT }}
access_token_lifetime: '100s'
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v0.3.0
- name: set crdential_file
run: gcloud auth login --cred-file=${{steps.auth.outputs.credentials_file_path}}
- name: Run gcloud
run: gcloud compute instances list --zones us-east4-c
Here we are using some secrets like WORKLOAD_IDENTITY_PROVIDER_ID and SERVICE_ACCOUNT. We need to create these two secrets in the github repo. We can get their values from step 1.
In this step Github action ‘google-github-actions/auth’ is first calling Github OIDC provider to get OIDC token. To get an OIDC token it must have permissions to make API calls against the OIDC provider. That’s where the `id-token: ‘write’` line becomes necessary. It provides ‘google-github-actions/auth’ action necessary permissions to make API calls to OIDC provider.
Github action makes a call to Github OIDC provider. The call is equivalent to the below curl request.
OIDC_TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://iam.googleapis.com/${{ secrets.WORKLOAD_IDENTITY_PROVIDER_ID }}" | jq '.value')
OIDC provider returns token , it is in jwt format and and if decoded successfully, looks something like below:
{
"typ": "JWT",
"alg": "RS256",
"x5t": "example-thumbprint",
"kid": "example-key-id"
}{
"jti": "example-id",
"sub": "repo:PradeepSingh1988/gcp-wif:ref:refs/heads/main"
"environment": "",
"aud": "https://iam.googleapis.com/projects/<project_number>/locations/global/workloadIdentityPools/<pool_id>/providers/<provider_id>",
"ref": "refs/heads/main",
"sha": "example-sha",
"repository": "PradeepSingh1988/gcp-wif",
"repository_owner": "PradeepSingh1988",
"actor_id": "23",
"repository_id": "45",
"repository_owner_id": "67",
"run_id": "example-run-id",
"run_number": "101",
"run_attempt": "21",
"actor": "PradeepSingh1988",
"workflow": "example-workflow",
"head_ref": "",
"base_ref": "",
"event_name": "workflow_dispatch",
"ref_type": "branch",
"job_workflow_ref":"PradeepSingh1988/gcp-wif/.github/workflows/ci.yml@refs/heads/main",
"iss": "https://token.actions.githubusercontent.com",
"nbf": 1632494000,
"exp": 1632494900,
"iat": 1632494600
}
Step 3
In this step Github action ‘google-github-actions/auth’ exchanges OIDC provided jwt token with GCP security Token Service to get federated access token. The HTTPs call made by Github action is equivalent to the below curl request.
STS_RESPONSE=$(curl -0 -X POST https://sts.googleapis.com/v1/token \
-H 'Accept: application/json' -H 'Content-Type: application/json' -d "$(cat <<EOF
{
"audience": "//iam.googleapis.com/${{ secrets.WORKLOAD_IDENTITY_PROVIDER_ID }}",
"grantType": "urn:ietf:params:oauth:grant-type:token-exchange",
"requestedTokenType" : "urn:ietf:params:oauth:token-type:access_token",
"scope": "https://www.googleapis.com/auth/cloud-platform",
"subjectTokenType": "urn:ietf:params:oauth:token-type:jwt",
"subjectToken": $OIDC_TOKEN
}
EOF
)"
)
STS_TOKEN=$(jq '.access_token' <<< "$STS_RESPONSE")
Here it is passing two important information, audience and subjectToken. After receiving the request STS verifies the token with the help of Workload Identity Provider. Workload Identity Provider does all the condition checks, attribute mapping specified during provider creation. It checks whether the ‘iss’ field in the token is the same as ‘issuer-uri’ passed during creating the Workload Identity provider. The ‘aud’ field in the token is equal to “https://iam.googleapis.com/projects/<project_number>/locations/global/workloadIdentityPools/<pool_id>/providers/<provider_id>” or not and many more. All the steps are described here in detail.
Once the request is verified successfully, STS returns a federated token. This token is a kind of GCP identity with all the necessary information necessary for impersonating a service account.
Step 4
In this step Github action ‘google-github-actions/auth’ exchanges federated token received in previous step to get IAM access token. The HTTPs call made by Github action is equivalent to the below curl request.
IAM_RESPONSE=$(curl -0 -X POST \
https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${{ \
secrets.SERVICE_ACCOUNT }}:generateAccessToken \
-H "Authorization: Bearer $STS_TOKEN" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' -d "$(cat <<EOF
{
"scope": [ "https://www.googleapis.com/auth/cloud-platform" ]
}
EOF
)"
)
ACCESS_TOKEN=$(jq '.accessToken' <<< "$IAM_RESPONSE")
IAM validates the federated token and checks if it belongs to the correct principal, if yes then it issues an oauth token, which can be used to make GCP api calls.
Step 5
Using the access token received in step 4 workflow makes an API request to GCP for listing instances. This token comes with a short lifespan. Once expired we need to refresh it again.
Wrapping Up
The benefits of using OIDC with WIF are pretty amazing.
- Removing the requirement to manage secrets and secret rotation in GitHub actions is a big win both in terms of security and reduction of maintenance (secret rotation).
- Using custom attribute mappings and principal sets allows you to tightly control GCP access not just at the repo level, but based on the branch, event, environment, etc that the action is running on.
Once you understand the shape and meaning of the JWT token claims, as well as how to use attribute mappings to create principal sets you should be able to manage GCP access from your workflows with as much or as little granularity as you see fit.