Workload Identity Federation: Keyless Authentication from AWS and Azure to GCP

Eliminating service account keys by federating workload identity from AWS, Azure, GitHub Actions, and Kubernetes to GCP using OIDC and SAML protocols.

#gcp#workload-identity#security#iam
Cover image for the article: Workload Identity Federation: Keyless Authentication from AWS and Azure to GCP

Service account keys are the passwords of cloud infrastructure — static credentials that get committed to repos, stored in plaintext, leaked in logs, and never rotated. Workload Identity Federation eliminates them entirely by allowing external workloads to authenticate to GCP using their native identity — no keys, no secrets, no rotation schedules.

After eliminating 34 service account keys across our multi-cloud infrastructure, our security posture improved dramatically. Zero static credentials means zero credential rotation failures, zero key leaks, and zero unauthorized access from compromised keys.

The Problem with Service Account Keys

Every service account key is a liability:

  • No automatic rotation: Keys are valid until manually revoked (up to 10 years)
  • Difficult to audit: Which system is actually using which key?
  • Exfiltration risk: JSON key files get stored in CI/CD systems, developer machines, config management
  • Blast radius: A leaked key provides full service account permissions until discovered

Our audit found 34 active keys, 8 of which hadn't been rotated in over a year, and 3 that were stored in CI/CD environment variables with overly broad access to the project.

Service Account Key Risk Surface

How Workload Identity Federation Works

Instead of a static key, the external workload presents a token from its own identity provider. GCP validates that token against a pre-configured trust relationship and issues a short-lived GCP access token.

External Workload (AWS/Azure/GitHub)
         │
         │ 1. Present OIDC/SAML token
         ▼
   Workload Identity Pool
         │
         │ 2. Validate token claims
         │ 3. Map to GCP identity
         ▼
   Service Account Impersonation
         │
         │ 4. Issue short-lived access token (1 hour)
         ▼
   GCP Resources (GCS, BigQuery, etc.)

The federation trust validates:

  • Issuer: Token comes from a trusted identity provider
  • Audience: Token was issued for this specific workload identity pool
  • Claims: Subject, groups, or custom attributes match conditions

Implementation: AWS to GCP Federation

For AWS workloads (EC2, ECS, Lambda) accessing GCP resources:

# terraform/workload-identity-aws.tf

# Create the workload identity pool
resource "google_iam_workload_identity_pool" "aws_pool" {
  workload_identity_pool_id = "aws-production"
  display_name              = "AWS Production Workloads"
  description               = "Federation for AWS production services"
  project                   = var.project_id
}

# Configure AWS as an identity provider
resource "google_iam_workload_identity_pool_provider" "aws_provider" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.aws_pool.workload_identity_pool_id
  workload_identity_pool_provider_id = "aws-provider"
  display_name                       = "AWS Provider"

  aws {
    account_id = var.aws_account_id  # "123456789012"
  }

  # Restrict which AWS roles can federate
  attribute_condition = <<-EOT
    attribute.aws_role.contains("arn:aws:sts::${var.aws_account_id}:assumed-role/gcp-federation")
  EOT

  attribute_mapping = {
    "google.subject"        = "assertion.arn"
    "attribute.aws_role"    = "assertion.arn"
    "attribute.aws_account" = "assertion.account"
  }
}

# Allow the federated identity to impersonate a service account
resource "google_service_account_iam_binding" "aws_federation_binding" {
  service_account_id = google_service_account.data_sync.name
  role               = "roles/iam.workloadIdentityUser"

  members = [
    "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.aws_pool.name}/attribute.aws_role/arn:aws:sts::${var.aws_account_id}:assumed-role/gcp-federation"
  ]
}

AWS-side code to authenticate:

# aws_to_gcp_client.py
"""Authenticate from AWS to GCP using Workload Identity Federation."""

import json
import boto3
from google.auth import identity_pool
from google.cloud import storage


def get_gcp_credentials():
    """Create GCP credentials from AWS identity without any keys."""
    # Configuration file approach (recommended for applications)
    credential_config = {
        "type": "external_account",
        "audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/aws-production/providers/aws-provider",
        "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
        "token_url": "https://sts.googleapis.com/v1/token",
        "credential_source": {
            "environment_id": "aws1",
            "region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
            "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
            "regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
        },
        "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/data-sync@my-project.iam.gserviceaccount.com:generateAccessToken"
    }

    credentials = identity_pool.Credentials.from_info(credential_config)
    return credentials


def sync_data_to_gcs():
    """Example: Upload data from AWS to GCS without any static credentials."""
    credentials = get_gcp_credentials()
    client = storage.Client(credentials=credentials, project="my-project")

    bucket = client.bucket("data-sync-bucket")
    blob = bucket.blob("aws-export/latest.parquet")
    blob.upload_from_filename("/tmp/export.parquet")
    print(f"Uploaded to gs://data-sync-bucket/aws-export/latest.parquet")

Implementation: GitHub Actions to GCP Federation

This eliminates the need to store GCP service account keys as GitHub secrets:

# terraform/workload-identity-github.tf

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

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-provider"

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

  # Restrict to specific repository and branch
  attribute_condition = <<-EOT
    assertion.repository_owner == "my-org" &&
    assertion.repository == "my-org/my-repo" &&
    assertion.ref == "refs/heads/main"
  EOT

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

GitHub Actions workflow using federation:

# .github/workflows/deploy.yml
name: Deploy to GCP
on:
  push:
    branches: [main]

permissions:
  id-token: write   # Required for OIDC token request
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - id: auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-actions/providers/github-provider'
          service_account: 'deploy@my-project.iam.gserviceaccount.com'

      - name: Deploy to Cloud Run
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: my-service
          region: us-central1
          image: us-central1-docker.pkg.dev/my-project/production/my-service:${{ github.sha }}

Implementation: Azure to GCP Federation

# terraform/workload-identity-azure.tf

resource "google_iam_workload_identity_pool_provider" "azure_provider" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.multi_cloud.workload_identity_pool_id
  workload_identity_pool_provider_id = "azure-provider"

  oidc {
    issuer_uri        = "https://sts.windows.net/${var.azure_tenant_id}/"
    allowed_audiences = ["api://gcp-federation"]
  }

  attribute_condition = <<-EOT
    assertion.tid == "${var.azure_tenant_id}" &&
    assertion.oid in ["${var.azure_managed_identity_object_id}"]
  EOT

  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.tenant_id"  = "assertion.tid"
    "attribute.object_id"  = "assertion.oid"
  }
}

Security Comparison

Security PropertyService Account KeyWorkload Identity Federation
Credential lifetimePermanent (until revoked)1 hour (auto-refresh)
Rotation requiredManual, often forgottenAutomatic (no credentials to rotate)
Storage riskKeys stored in multiple systemsNo secrets to store
Audit granularityKey ID onlyFull identity chain (who, from where)
Revocation speedHours (propagation delay)Immediate (condition update)
Blast radius of compromiseFull SA permissionsLimited by attribute conditions
Compliance (SOC2, ISO27001)Finding: static credentialsPassing: ephemeral tokens

Attribute Conditions: Fine-Grained Access Control

Attribute conditions are the policy layer that makes federation secure. Without them, any workload from the federated provider could access your resources:

# Bad: Too permissive - any AWS role in the account can federate
attribute_condition = "true"

# Good: Specific role restriction
attribute_condition = <<-EOT
  attribute.aws_role == "arn:aws:sts::123456789012:assumed-role/gcp-data-sync/session"
EOT

# Better: Role + time-based restriction (maintenance windows only)
attribute_condition = <<-EOT
  attribute.aws_role == "arn:aws:sts::123456789012:assumed-role/gcp-data-sync/session" &&
  request.time.getHours("America/New_York") >= 2 &&
  request.time.getHours("America/New_York") <= 6
EOT

Migration Results

MetricBeforeAfterChange
Active service account keys340-100%
Key rotation failures/year60-100%
Mean credential lifetime14 months1 hour-99.99%
Security audit findings (credentials)80-100%
CI/CD secrets stored120-100%
Time spent on key management/month8 hours0-100%

Credential Lifetime Reduction

Common Pitfalls

Forgetting attribute conditions. Without conditions, the federation trust is too broad. Always restrict by specific roles, repositories, or identities.

Token audience mismatch. The audience in the provider configuration must exactly match what the external identity provider issues. Debugging 401 errors usually starts here.

Service account impersonation permissions. The federated identity needs roles/iam.workloadIdentityUser on the target service account. This is frequently missed during setup.

Clock skew. OIDC tokens have tight validity windows. Ensure NTP is configured correctly on external workloads, especially on-premises servers.

Conclusion

Workload Identity Federation eliminated all 34 service account keys from our infrastructure, reduced credential lifetime from months to hours, and removed an entire category of security findings from our compliance audits. The implementation is straightforward for AWS, Azure, and GitHub Actions — each taking approximately 2 hours to configure and test.

If you have any service account key JSON files in your infrastructure, they should be your next migration target. Every day a static key exists is a day it could be compromised.

Comments

    No comments yet. Be the first to share your thoughts.