Secure Auth: Workload Identity Federation (Keyless APIs)
Scenario
Your GitHub Actions CI/CD pipeline needs to deploy files to Google Cloud Storage. Previously, you would generate a JSON Service Account Key and store it in GitHub Secrets. This is a massive security risk, as keys are easily leaked. Workload Identity Federation solves this. It establishes trust between GitHub's OIDC servers and GCP IAM natively. GitHub requests a short-lived token from GCP on-the-fly without any JSON keys.
- Create a google_iam_workload_identity_pool.
- Create a google_iam_workload_identity_pool_provider mapped to GitHub's OIDC issuer (https://token.actions.githubusercontent.com).
- Configure the provider's attribute mapping, linking Google's google.subject to GitHub's assertion.sub.
- Create a dedicated Workload Identity Service Account in GCP.
- Grant the GitHub repository identity permission to impersonate the GCP Service Account via a specific IAM Role Binding (roles/iam.workloadIdentityUser).
Additional Context
How it works: The pipeline in GitHub Actions runs a step to authenticate. GitHub signs a JWT token proving "I am repository userX/repoY running job Z". The pipeline sends this token to GCP IAM. GCP validates the signature against GitHub's public OIDC keys, checks the attribute condition (e.g. "Only allow repoY"), and issues a temporary 1-hour GCP access token.
Attribute Mappings: This is the most crucial security step. If you misconfigure the condition to just `attribute.repository`, any GitHub repository on the planet could potentially assume your Service Account. You must bind it strictly to your specific `{owner}/{repo}` string.
variables.tf
variable "project_id" { type = string }
variable "github_repository" {
type = string
description = "The target github repository string e.g. 'Octocat/hello-world'"
default = "ethernetdude/infrastructure-repo"
}main.tf
provider "google" {
project = var.project_id
}
# 1. The custom Service Account the pipeline will assume
resource "google_service_account" "github_worker" {
account_id = "github-actions-worker"
display_name = "GitHub Actions Deployment SA"
}
# (Give the service account permissions to actually do things, like Storage Admin)
resource "google_project_iam_member" "sa_storage" {
project = var.project_id
role = "roles/storage.admin"
member = "serviceAccount:${google_service_account.github_worker.email}"
}
# 2. The Identity Pool representing the external IdP ecosystem
resource "google_iam_workload_identity_pool" "github_pool" {
workload_identity_pool_id = "github-actions-pool"
display_name = "GitHub Actions Pool"
description = "Identity pool for automating GitHub CI/CD"
}
# 3. The Provider tying the Pool to GitHub specifically
resource "google_iam_workload_identity_pool_provider" "github_provider" {
workload_identity_pool_id = google_iam_workload_identity_pool.github_pool.workload_identity_pool_id
workload_identity_pool_provider_id = "github-oidc-provider"
display_name = "GitHub OIDC"
# Connect to GitHub's OIDC servers
oidc {
issuer_uri = "https://token.actions.githubusercontent.com"
}
# Map the claims GitHub sends in the JWT to GCP claims
attribute_mapping = {
"google.subject" = "assertion.sub" # The unique subject from GitHub
"attribute.actor" = "assertion.actor" # The GitHub username triggering the workflow
"attribute.repository" = "assertion.repository" # owner/repo string
"attribute.repository_owner" = "assertion.repository_owner"
}
# WARNING: Condition ensuring ONLY this specific repository can use this provider
attribute_condition = "assertion.repository == '${var.github_repository}'"
}
# Ensure data structure exists for extracting the project number cleanly
data "google_project" "env" {
project_id = var.project_id
}
# 4. The IAM Binding: Allow the specific GitHub Identity to masquerade as the GCP Service Account
resource "google_service_account_iam_member" "workload_impersonation" {
service_account_id = google_service_account.github_worker.name
role = "roles/iam.workloadIdentityUser"
# This highly structured string defines exactly which external principal is permitted
member = "principalSet://iam.googleapis.com/projects/${data.google_project.env.number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.github_pool.workload_identity_pool_id}/attribute.repository/${var.github_repository}"
}Workflow Commands
terraform init
terraform apply \
-var="project_id=my-gcp-project-123" \
-var="github_repository=my-github-org/my-gcp-repo"
# On the GitHub Side, your .github/workflows/main.yml file would use the official Action:
# - id: 'auth'
# uses: 'google-github-actions/auth@v1'
# with:
# workload_identity_provider: 'projects/123/locations/global/workloadIdentityPools/github-actions-pool/providers/github-oidc-provider'
# service_account: '[email protected]'
terraform destroy -var="project_id=my-gcp-project-123"
