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.

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.
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 Property | Service Account Key | Workload Identity Federation |
|---|---|---|
| Credential lifetime | Permanent (until revoked) | 1 hour (auto-refresh) |
| Rotation required | Manual, often forgotten | Automatic (no credentials to rotate) |
| Storage risk | Keys stored in multiple systems | No secrets to store |
| Audit granularity | Key ID only | Full identity chain (who, from where) |
| Revocation speed | Hours (propagation delay) | Immediate (condition update) |
| Blast radius of compromise | Full SA permissions | Limited by attribute conditions |
| Compliance (SOC2, ISO27001) | Finding: static credentials | Passing: 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
| Metric | Before | After | Change |
|---|---|---|---|
| Active service account keys | 34 | 0 | -100% |
| Key rotation failures/year | 6 | 0 | -100% |
| Mean credential lifetime | 14 months | 1 hour | -99.99% |
| Security audit findings (credentials) | 8 | 0 | -100% |
| CI/CD secrets stored | 12 | 0 | -100% |
| Time spent on key management/month | 8 hours | 0 | -100% |
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.
Recommended reading

Per-Team Cost Allocation in Shared Kubernetes Clusters: From Chaos to Clarity
Implementing accurate per-namespace cost allocation in multi-tenant Kubernetes clusters, covering request vs. usage attribution, shared resource amortization, and building showback dashboards that drive accountability.

Measuring and Eliminating Toil: From 40% to 12% of Engineering Time
A systematic approach to identifying, measuring, and automating toil—the repetitive operational work that scales linearly with service growth and prevents engineers from doing creative work.

Serverless Postgres in Production: Branching, Scale-to-Zero, and the End of Database Provisioning
Running Neon serverless Postgres in production for 8 months — covering database branching workflows, scale-to-zero economics, connection pooling, and migration from RDS.

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