I was recently needing to access GCP resources from a github actions workflow in order to run an end-to-end test of gcp-kms-issuer. The old way of doing this would be to create a GCP service account and then export a long-lived authentication keyfile that you'd then stuff into an actions environment secret.

Today, however, the promise of OIDC federation is upon us, and when it works it is truly magical and much more secure. Both the google cloud blog and the github documentation sites already have excellent articles on how to set up this federation using clickops and cliops, but I'm using kubernetes config connector and gitops, so I'm going to show you how I translated these instructions thusly.

Enabling keyless authentication from GitHub Actions | Google Cloud Blog
Authenticate from GitHub Actions to create and manage Google Cloud resources using Workload Identity Federation.
Configuring OpenID Connect in Google Cloud Platform - GitHub Docs
Use OpenID Connect within your workflows to authenticate with Google Cloud Platform.

The gist of the setup is that you need to create a workload identity pool in GCP to hold a provider which is configured to issue GCP workload identities in exchange for a github actions JWT, this also requires enabling permission to create this JWT on the github actions side.

Claim assertions piped from the JWT through the provider can then be bound via an IAM policy to assign roles to the workload identity of actions running in a specific github repo/branch/etc.

When the workflow runs, one of the earliest actions should be google-github-actions/auth. This will get the signed github JWT and use the federation provider to exchange it for a GCP workload identity.

This identity can then be used by the other steps in the workflow to interact with GCP APIs in accordance with permissions granted via roles assigned in IAM.

Boss! The Code! The Code!

All of this code is live and running in my public gitops repo and I'll link to the resources and code directly in the text that describes them.

To create the pool and provider, config connector provides straight-forward resources that we can drop in the gitops repo:

apiVersion: iam.cnrm.cloud.google.com/v1beta1
kind: IAMWorkloadIdentityPoolProvider
metadata:
  name: github
spec:
  projectRef:
    external: projects/gptops-playground
  location: global
  workloadIdentityPoolRef:
    name: github
  displayName: github
  description: Github repo actions workload identity federation
  disabled: false
  attributeMapping:
    google.subject: assertion.sub
    attribute.repository: assertion.repository
  attributeCondition: assertion.repository_owner == 'drzzlio'
  oidc:
    issuerUri: https://token.actions.githubusercontent.com
---
apiVersion: iam.cnrm.cloud.google.com/v1beta1
kind: IAMWorkloadIdentityPool
metadata:
  name: github
spec:
  projectRef:
    external: projects/gptops-playground
  location: global
  displayName: github
  description: Identity pool for github actions workloads
  disabled: false

In the provider here we're using attributeMapping to bind the custom attribute repository to the repository claim in github's JWT, which will be a value like drzzlio/kms-issuer for my example case.

The attributeCondition is a whitelist policy that only allows handling identities for repos under my github org. Without this, any github action could request an identity from our provider, which is not great even though it may not have any permissions in IAM.

The oidc.issuerUri property tells the provider where it can get the well-known openid JWKS metadata necessary for authenticating the JWT created by github.

The pool is just a container and doesn't take much configuration itself.

On the github actions side we set permissions to create an id-token and add the google auth action:

jobs:
  e2e:
    permissions:
      id-token: write # This is required for requesting the JWT for GCP OIDC federation
      contents: read  # This is required for actions/checkout
    steps:
      - uses: actions/checkout@v4.1.1

      - name: Authenticate to Google Cloud
        id: gcpauth
        uses: google-github-actions/auth@v2.1.2
        with:
          workload_identity_provider: projects/726581429530/locations/global/workloadIdentityPools/github/providers/github

# -- snip creating kind cluster --

      - name: Install GCP auth secret
        run: kubectl create -n kms-issuer-system secret generic kmsissueradc --from-file=gadcreds.json=${GOOGLE_APPLICATION_CREDENTIALS}

# -- snip running tests --

By default, the auth action will create a credentials file compatible with google ADC when provided a workload identity pool to retrieve an identity from. It also sets various environment variables in the workflow that ADC clients look at, like GOOGLE_APPLICATION_CREDENTIALS.

In my case I need the credentials to allow the kms-issuer controller running in a kind cluster for e2e testing to reach out to my GCP project, so I inject a secret containing the contents of the credentials file created by the auth action, which it has conveniently pointed at using the above environment variable.

Finally, we need to bind the action's workload identity for some repo to roles in IAM. What permissions are necessary will be quite specific to the particular use-case, but in mine I needed to allow the controller access to a KMS key for signing.

We can do this simply with an IAMPolicy KCC resource, of course, in the gitops repo:

apiVersion: iam.cnrm.cloud.google.com/v1beta1
kind: IAMPolicy
metadata:
  name: kmsissuer-test-keypolicy
spec:
  resourceRef:
    apiVersion: kms.cnrm.cloud.google.com/v1beta1
    kind: KMSCryptoKey
    name: kmsissuer-test
  bindings:
    - role: roles/cloudkms.signerVerifier
      members:
        - principalSet://iam.googleapis.com/projects/726581429530/locations/global/workloadIdentityPools/github/attribute.repository/drzzlio/kms-issuer
    - role: roles/cloudkms.viewer
      members:
        - principalSet://iam.googleapis.com/projects/726581429530/locations/global/workloadIdentityPools/github/attribute.repository/drzzlio/kms-issuer

Using IAMPolicy we can ensure this is the only policy that applies to the key (it will overwrite any bindings not in this list, very powerful in combatting drift when combined with self-healing gitops tooling).

The principalSet: prefix used for the member entries let us tell google that the role binding is referencing a workload identity issued by our federated pool, with the value after attribute.repository/ binding against the custom attribute that we passed through in the identity provider attribute mapping.

In this case the member binding means that only a workload identity created from a cryptographically-verified JWT with a claim that github says it issued to an action in the drzzlio/kms-issuer repo will be able to make use of these roles.

For my needs binding on the repo is sufficient, but the same capability can bind against any claims that github includes in its JWT, like environment, actor, workflow, branch, sha, etc.

Workload Identity Federation via Service Account

What I've described above uses the direct method of identity federation, which is the preferred and simpler method for binding federated identities directly to roles in IAM. Unfortunately, some GCP services do not support this method of role binding and require impersonation of a GCP service account.

Another reason you might not be able to use direct identity federation is if your workflow logic needs access to OAuth access tokens or ID tokens from the auth plugin, as this is only supported when mediated by a GCP service account identity.

The main difference to the above setup is the addition of a GCP service account and a policy that grants the workloadIdentityUser role on it to the repo using the same principalSet: as above. The policies for roles that the actions need access to would then be granted to the service account instead of directly to the workload identity.

Adding the service_account property to the google auth action config will then cause it to use its workload identity to impersonate the specified service account instead of using the workload identity directly to access GCP services.

Conclusion

I think one of the nicest things about KCC, besides having your IaC eventually consistent and all unified under k8s controllers, is the way it is architected to allow composing its resources in such a way that elides much of the repitition and cross-referencing noise that is so prevalent in the definitions of other IaC tooling.

With a large portion of the GCP API surface covered by KCC, many tasks, even somewhat complicated ones like identity federation become kind of a joy to work with.

Take your newfound knowledge: go forth and control all the things!