Cloud workload identity federation eliminating long-lived credentials

Cloud Workload Identity Federation: Eliminating Long-Lived Credentials in CI/CD

When Our Secret Audit Found a Three-Year AWS Key

A GitHub repository had an AWS access key in its secrets that had been active for 3 years without rotation, and I found it during a git history audit after we expanded our CI/CD checks across our manufacturing security environment. The key still worked. It had enough permission to publish artifacts into an S3 bucket, update a small Lambda function, and read deployment metadata that I did not want sitting behind a static secret.

My first assumption was wrong: I thought we were looking at a sloppy one-off key someone had dropped into GitHub during a rushed release. The truth was more ordinary and more dangerous. The key was created for a specific CI/CD use case, the developer who created it left, and nobody knew the key existed until the audit. That is exactly how long-lived credentials survive.

That key had outlived two build runners, one repository rename, and a migration from older Ubuntu images to Ubuntu 22.04. We were running FortiOS 7.4.3 at the edge, Python 3.11 for our audit scripts, and modern cloud controls everywhere else, yet one forgotten access key still had a clean path into production-adjacent systems.

That bothered me.

Replacing Static Secrets With OIDC Token Exchange

Workload Identity Federation changed the shape of the problem for my team. Instead of storing an AWS access key or a GCP service account key in a CI/CD secret store, our pipeline asks GitHub for an OpenID Connect token, presents that token to the cloud provider, and receives temporary credentials scoped to a role or service account. The trust moves from “who knows the secret” to “which workload is currently running under a verified identity.”

The flow is short and strict. GitHub issues an OIDC token with claims such as repository, branch, workflow, actor, and audience. AWS STS or GCP Security Token Service validates those claims against a configured trust policy. If the claims match, the provider returns temporary credentials with a lifetime measured in minutes or hours, not years.

Expiration is the control.

What I did not expect was how much simpler incident response became. With static keys, we had to assume that any copied secret could still be useful until we rotated it everywhere. With OIDC exchange, a stolen token from a completed job has a tiny abuse window, and the next job has to prove its identity again. I prefer controls that fail closed without asking my team to remember every old exception.

Wiring GitHub Actions Into AWS and GCP

For AWS, I created an IAM OIDC identity provider for https://token.actions.githubusercontent.com, then tied an IAM role to specific GitHub claims. I avoided broad repository wildcards unless the repository was dedicated to one deployment path. In our environment, branch and workflow restrictions mattered because release pipelines and validation pipelines did not deserve the same blast radius.

For GCP, I used Workload Identity Federation with a workload identity pool, a GitHub provider, and service account impersonation. The pattern felt different from AWS, but the goal was the same: no JSON key files in GitHub secrets, no copied credentials in deployment scripts, and no emergency spreadsheet tracking which secret belonged to which repository.

name: deploy-production

permissions:
  id-token: write
  contents: read

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

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-prod-deploy
          aws-region: us-east-1

      - name: Deploy
        run: ./scripts/deploy.sh

The hard part was not the YAML. The hard part was forcing each pipeline owner to name the exact cloud action the job needed, because Workload Identity Federation exposes messy permission habits fast. I think that discomfort is useful.

Building Trust Policies We Could Defend

I treated each trust policy like a firewall rule. My team would never create a FortiOS 7.4.3 policy that allowed any source to reach any destination just because it was convenient, so I did not accept cloud trust rules that allowed any repository, any branch, or any workflow to assume production roles. The same operational discipline applies.

  • I bound production roles to protected branches only.
  • I separated build, test, staging, and production roles.
  • I required explicit repository names in trust conditions.
  • I kept cloud permissions narrower than workflow permissions.
  • I logged every role assumption into our SIEM pipeline.

In AWS, that meant conditions on token.actions.githubusercontent.com:sub and aud. In GCP, that meant attribute mappings for repository, ref, and workflow, then IAM bindings that allowed only matching principals to impersonate the target service account. We tested failures on purpose: wrong branch, renamed workflow, forked repository, and missing audience.

You may also find this useful: Check out our guide on Python Network Config Backup: Automating Multi-Vendor Device Snapshots for more practical tips.

Break the happy path first.

After migrating 45 CI/CD pipelines to OIDC-based credential exchange, zero long-lived cloud credentials remain in any CI/CD secret store. Before the migration, we had 17 long-lived AWS access keys and 9 GCP service account JSON keys across GitHub Actions secrets, environment secrets, and inherited organization-level secrets. Afterward, that number was exactly 0. My opinion is blunt: if a deployment job can federate, a stored cloud key is technical debt with a breach clock attached.

Audit Every Pipeline Before Trusting the Migration

I did not start by deleting secrets. I started by inventorying them. We used Python 3.11 scripts against the GitHub API, AWS IAM credential reports, GCP service account key listings, and our own repo scanning output. I wanted a list that showed secret name, repository, owner, last observed workflow, cloud account, and whether the credential had been used recently.

The awkward findings were the valuable ones. Some secrets were dead. Some were duplicated under different names. Some were used only during manual dispatch jobs that nobody had run in a year. A few were tied to vendor integrations that could not support federation yet, so we moved those into a separate exception register with owners, rotation dates, and compensating monitoring.

I dislike quiet exceptions.

For existing CI/CD environments, I now look for four signals before I call the work done: no cloud keys in repository or organization secrets, no active IAM users dedicated to automation where a federated role can replace them, no service account JSON keys attached to build jobs, and logs proving that successful deployments used OIDC-backed role assumption. Anything less leaves too much room for the same three-year key to reappear under a cleaner name.

My strongest view after this migration is that CI/CD identity should be earned at runtime, not remembered in a secret store. Long-lived credentials made sense when our pipelines were simpler, but our environment is too connected now, and manufacturing downtime is too expensive for forgotten keys to be part of the deployment model.

Further Reading: For more in-depth information, refer to the official Fortinet Documentation.

A credential that expires before an attacker can use it is better than a secret we promise to rotate someday.

·

·