LAB39_GCP_Secure Auth: Workload Identity Federation (Keyless APIs)

Secure Auth: Workload Identity Federation (Keyless APIs)

☁ GCP 🔥 Intermediate Workload Identity Security IAM GitHub Actions

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.

Your Objectives
  • 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"