Trovella Wiki

Workload Identity Federation

How GitHub Actions authenticates to GCP without service account keys using OIDC-based Workload Identity Federation.

Workload Identity Federation (WIF) allows GitHub Actions to authenticate to GCP without long-lived service account keys. Instead, GitHub Actions presents an OIDC token to GCP, which exchanges it for short-lived credentials scoped to a specific service account.

How It Works

GitHub Actions runner
  |
  | (1) Request OIDC token from GitHub
  v
GitHub OIDC provider (token.actions.githubusercontent.com)
  |
  | (2) JWT with repository, ref, actor claims
  v
GCP Workload Identity Pool ("github-actions")
  |
  | (3) Validate token, check attribute_condition
  v
GCP Workload Identity Provider ("github-oidc")
  |
  | (4) Exchange for short-lived SA credentials
  v
Service Account (github-actions-ci@trovella-shared)
  |
  | (5) Use credentials for cross-project operations
  v
trovella-prod / trovella-staging resources

Terraform Configuration

The WIF resources are defined in infra/modules/wif/main.tf and instantiated from infra/environments/shared/main.tf.

Pool and Provider

resource "google_iam_workload_identity_pool" "github" {
  project                   = var.project_id
  workload_identity_pool_id = "github-actions"
  display_name              = "GitHub Actions"
}

resource "google_iam_workload_identity_pool_provider" "github" {
  project                            = var.project_id
  workload_identity_pool_id          = "github-actions"
  workload_identity_pool_provider_id = "github-oidc"

  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.actor"      = "assertion.actor"
    "attribute.repository" = "assertion.repository"
    "attribute.ref"        = "assertion.ref"
  }

  attribute_condition = "assertion.repository == \"kyleolson512/Trovella\""

  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}

The attribute_condition restricts token exchange to workflows running in the kyleolson512/Trovella repository only. Forked repositories or other repositories cannot obtain credentials.

Service Account

resource "google_service_account" "github_actions" {
  project      = var.project_id
  account_id   = "github-actions-ci"
  display_name = "GitHub Actions CI/CD"
}

The service account lives in trovella-shared. It has roles/iam.workloadIdentityUser bound to the WIF pool, allowing token exchange.

Cross-Project IAM Grants

The service account has permissions across all three projects:

RoleProjectsPurpose
roles/editorprod, staging, sharedDeploy and manage resources
roles/secretmanager.secretAccessorprod, stagingRead secrets during build and deploy
roles/iap.tunnelResourceAccessorprod, stagingSSH to VMs via IAP tunnel
roles/compute.instanceAdmin.v1prod, stagingSSH access and instance management

The shared project only gets roles/editor (for Artifact Registry pushes). It does not get secret accessor or IAP tunnel roles because there are no secrets or VMs in the shared project.

CI Workflow Usage

The CI workflow (.github/workflows/ci.yml) uses WIF in three jobs:

migrate-prod

permissions:
  contents: read
  id-token: write

steps:
  - uses: google-github-actions/auth@v2
    with:
      workload_identity_provider: ${{ vars.WIF_PROVIDER }}
      service_account: ${{ vars.WIF_SERVICE_ACCOUNT }}

The id-token: write permission is required for the runner to request an OIDC token from GitHub. The vars.WIF_PROVIDER and vars.WIF_SERVICE_ACCOUNT are stored as GitHub repository variables (not secrets -- they are not sensitive).

After authentication, the job can:

  • Read DATABASE_URL from Secret Manager
  • Start Cloud SQL Auth Proxy (which uses the SA's cloudsql.client role)

build-push

Authenticates to push Docker images to Artifact Registry in trovella-shared:

- run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet

Also reads SENTRY_DSN from Secret Manager for the Docker build args.

deploy-prod

Authenticates to SSH into the production VM via IAP tunnel:

- run: |
    gcloud compute scp ... --tunnel-through-iap
    gcloud compute ssh ... --tunnel-through-iap

Outputs

The WIF module exports three values used as GitHub repository variables:

OutputDescriptionGitHub Variable
provider_nameFull resource name of the OIDC providerWIF_PROVIDER
service_account_emailEmail of the CI service accountWIF_SERVICE_ACCOUNT
pool_nameFull resource name of the identity pool(not used in CI)

Security Properties

  • No long-lived keys -- credentials are generated per-workflow-run and expire automatically
  • Repository-scoped -- only kyleolson512/Trovella can obtain credentials
  • Least privilege (by project) -- the SA has different roles per project; shared project has no secret access
  • Audit trail -- every token exchange is logged in Cloud Audit Logs

Quota Project Gotcha

The WIF service account lives in trovella-shared, so API calls made with its credentials are attributed to the shared project by default. This caused failures when the Cloud SQL Auth Proxy tried to call the Cloud SQL Admin API -- the API was enabled in trovella-prod but not in trovella-shared.

The fix is the --quota-project trovella-prod flag on the proxy. See Data & Storage -- Migrations -- CI Deployment for the full hardening history.

On this page