← Back to list

How We Eliminated JSON Service Account Keys Using Workload Identity Federation on GCP

A hands-on walkthrough of our security migration from static credentials to short-lived, keyless authentication

MNSR Nithin · 2026-05-23 10:27 · 0 claps · 4.3 min read
#workload-identity #gcp #cloud-security #devsecops
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud

How We Eliminated JSON Service Account Keys Using Workload Identity Federation on GCP

A hands-on walkthrough of our security migration from static credentials to short-lived, keyless authentication

Static JSON service account keys are one of those things that feel harmless until they aren’t. You generate one, tuck it into an environment variable or a secret manager, and move on. But here’s the uncomfortable truth: that key doesn’t expire. If it leaks — through a misconfigured repo, a careless log line, or a compromised CI runner — it’s valid forever, until someone manually hunts it down and revokes it.

At my organisation, our InfoSec team made the call to eliminate JSON keys entirely. The mandate was clear: all external workloads must migrate to Workload Identity Federation (WIF). I was part of the team that implemented it, and this post walks you through exactly what we did, why it works, and how you can replicate it.

Why JSON Keys Are a Problem

Let me put it plainly:

  • Long-lived: A JSON key has no TTL. It stays valid until you rotate or delete it.
  • Portable: It’s just a file. Anyone who gets it can use it from anywhere.
  • Easy to leak: Developers check them into repos, paste them in Slack, or leave them in Docker images more often than anyone would like to admit.
  • Hard to audit: “Where is this key being used?” is a surprisingly difficult question to answer.

Workload Identity Federation solves all of this. Instead of distributing a secret file, you establish a trust relationship between GCP and your external identity provider — in our case, Microsoft Azure AD. Workloads authenticate using their existing identity and receive short-lived, auto-expiring tokens. No file to leak. No rotation schedule to miss. No manual revocation.

How WIF Actually Works

Here’s the flow at a high level:

  1. Your external workload (a GitHub Actions runner, a GitLab CI job, an on-prem server) authenticates with Azure AD and receives a JWT.
  2. That JWT is sent to GCP’s Security Token Service (STS), which validates it against a configured Workload Identity Pool and Provider.
  3. GCP issues a short-lived STS token.
  4. That token is used to impersonate a GCP Service Account, granting access to whatever resources that service account can reach (like Secret Manager, Cloud Storage, etc.).

No key file. No static secret. The entire chain is identity-based and time-bounded.

What We Built: The Setup

Step 1 — Azure App Registration

We started by registering an application in Azure Active Directory and collecting three things:

  • TENANT_ID
  • CLIENT_ID
  • A client secret (for local development only — in production, we use Managed Identity to eliminate even this)

Step 2 — Create a Workload Identity Pool in GCP

gcloud iam workload-identity-pools create wif-pool \
  --project=your-gcp-project \
  --location=global \
  --display-name="Azure Federation Pool"

The pool is the top-level trust boundary. All providers (identity sources) you add will sit inside it.

Step 3 — Create a Workload Identity Provider

gcloud iam workload-identity-pools providers create-oidc azure-provider \
  --project=your-gcp-project \
  --location=global \
  --workload-identity-pool=wif-pool \
  --issuer-uri="https://sts.windows.net/<TENANT_ID>/" \
  --attribute-mapping="google.subject=assertion.sub,attribute.aud=assertion.aud,attribute.iss=assertion.iss"

The provider tells GCP: “Trust JWTs issued by this Azure tenant.” The attribute-mapping translates OIDC claims into GCP IAM attributes, which you'll use to scope access precisely.

Step 4 — Create a Service Account

gcloud iam service-accounts create sa-wif-federated \
  --project=your-gcp-project \
  --display-name="Federated Service Account"

Step 5 — Bind the Service Account

This is the binding that connects your federated identity to the service account:

gcloud iam service-accounts add-iam-policy-binding \
  sa-wif-federated@your-gcp-project.iam.gserviceaccount.com \
  --project=your-gcp-project \
  --role="roles/iam.serviceAccountTokenCreator" \
  --member="principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/wif-pool/subject/YOUR_AZURE_APP_ID"

Note the principal:// prefix — this binds a specific Azure identity (your App's subject), not all identities in the pool. This is the principle of least privilege in action.

Step 6 — Grant Resource Access

Finally, grant the service account only the permissions it needs. For Secret Manager access:

gcloud projects add-iam-policy-binding your-gcp-project \
  --member="serviceAccount:sa-wif-federated@your-gcp-project.iam.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor"

Don’t give it roles/editor. Give it exactly what it needs.

Validation: Making Sure It Actually Works

We validated the full federation flow with a Python script that:

  1. Requests a JWT from Azure AD using the app’s client credentials
  2. Sends that JWT to GCP’s STS endpoint to exchange for a short-lived token
  3. Uses the STS token to impersonate the service account
  4. Accesses a secret in Secret Manager to confirm end-to-end success

Seeing that secret returned without a single JSON key anywhere in the pipeline was genuinely satisfying.

Security Fine-Tuning We Added

We didn’t stop at the basic setup. Here’s what we tightened after the initial PoC:

Lock the issuer. Always specify your exact Azure tenant in the provider’s issuer URI. This prevents tokens from other tenants from being accepted, even accidentally.

Restrict the audience. Set the allowed audience to your App Registration’s Client ID. This prevents tokens issued for other applications in the same tenant from working.

Add attribute conditions. Rather than accepting any token that passes issuer and audience checks, we added an explicit condition:

--attribute-condition="attribute.appid=='YOUR_APP_ID' && attribute.iss=='https://sts.windows.net/YOUR_TENANT_ID/'"

Both must match. Belt and suspenders.

Avoid wildcard bindings. It’s tempting to bind principalSet://.../* to allow all subjects in a pool. Don't, unless you have a very specific reason. Bind the exact subject.

Enable Cloud Audit Logs. Every STS exchange and service account impersonation is logged. Set up alerts on unexpected impersonation patterns.

The Results

After migrating our first workloads:

  • Zero JSON keys in our CI pipelines
  • No rotation schedules to manage
  • No risk of keys being committed to source control
  • Audit logs showing exactly which workloads are impersonating which service accounts, and when

The InfoSec team’s mandate wasn’t just compliance theatre. This is a genuinely better model for machine-to-machine authentication.

Key Takeaways

If you’re still using JSON service account keys for any external workload — CI/CD pipelines, GitHub Actions, on-prem servers hitting GCP APIs — WIF is worth the one-time setup cost. The security properties are strictly better, and once it’s running, there’s nothing to maintain.

Start with a proof of concept in a sandbox project. Get the Python validation script working. Then migrate your most sensitive workloads first.

The days of static, manually-rotated credential files should be numbered. WIF is how you number them.

Have questions about the migration, the Python validation flow, or handling WIF for non-Azure identity providers? Drop them in the comments.


메타데이터
post_id
3cfa7a57ea44
slug
how-we-eliminated-json-service-account-keys-using-workload-identity-federation-on-gcp-3cfa7a57ea44
url
https://medium.com/@mnsrnithin9/how-we-eliminated-json-service-account-keys-using-workload-identity-federation-on-gcp-3cfa7a57ea44
canonical_url
https://medium.com/@mnsrnithin9/how-we-eliminated-json-service-account-keys-using-workload-identity-federation-on-gcp-3cfa7a57ea44
author_url
https://medium.com/@mnsrnithin9
status
ok
fetched_at
2026-06-09 15:37:30