
August 29, 2026
13 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Static AWS access keys in a GitLab CI variable have a way of outliving the engineer who created them. When that key leaks—or sits in a repo for three years—you get an incident, not a deploy. Workload Identity Federation Without Long-Lived Keys replaces those secrets with short-lived tokens minted by your CI platform or Kubernetes cluster and exchanged for cloud credentials at runtime. If you already run GitLab CI pipelines for Laravel, this is the upgrade path that removes the highest-risk secret from your pipeline without rewriting your application.
What is Workload Identity Federation Without Long-Lived Keys?
Workload identity federation is a trust bridge. Your cloud provider trusts a specific external identity provider (IdP)—GitHub Actions, GitLab CI, Google Kubernetes Engine, Azure AD—and accepts signed OIDC tokens from that IdP as proof of who is calling. The cloud then issues temporary credentials bound to a role with explicit permissions and a short expiry, typically 15 minutes to one hour.
The pattern solves a problem every small team recognises: long-lived keys are easy to copy, hard to rotate, and almost impossible to audit per workload. A GitHub Actions runner that uploads assets to S3 should not carry the same key as your production Laravel queue worker. Federation scopes access to this pipeline, this branch, this repository—not to everything the key ever touched.
In my experience maintaining Deployer 7 + GitLab CI pipelines on shared EC2 infrastructure, the first security win is not fancy encryption—it is deleting the `AWS_ACCESS_KEY_ID` variable entirely. Federation makes that deletion technically straightforward once the trust relationship is configured.
Core components you need to understand
- Identity provider (IdP): GitHub Actions, GitLab CI, CircleCI, or a Kubernetes service account issuer that signs OIDC tokens.
- Identity pool / OIDC provider: The cloud-side resource that registers the IdP's issuer URL and validates token signatures.
- Attribute mapping: Maps JWT claims (`sub`, `repository`, `ref`, `aud`) to cloud principal attributes.
- IAM role / service account binding: The target role your workload assumes after token validation.
- Condition keys: Trust-policy constraints that limit which repos, branches, or environments can assume the role.
How does OIDC-based workload identity federation work?
The flow is the same across AWS, Google Cloud, and Azure even though the console labels differ. A workload requests an OIDC token from its platform. The token is a signed JWT containing claims about the caller. The workload presents that JWT to the cloud provider's Security Token Service (STS) or equivalent. The cloud validates the signature against the IdP's JWKS endpoint, checks audience and expiry, evaluates trust-policy conditions, and returns temporary credentials.
JWT claims that matter for CI/CD
Trust policies should reference specific claims, not wildcards. On GitHub Actions, `sub` looks like `repo:org/repo:ref:refs/heads/main`. GitLab CI exposes `project_path`, `ref`, and `environment`. Always pin the `aud` (audience) claim—AWS expects `sts.amazonaws.com` for role assumption; GCP expects your workload identity provider resource name.
A common mistake is trusting the issuer but not constraining `sub`. That lets any repository in the organisation assume your production role. Tighten conditions before you merge the pipeline change.
How do you configure AWS IAM roles for OIDC without access keys?
AWS calls this IAM Roles for OIDC Identity Providers. GitHub and GitLab each have a well-known issuer URL. You register it once per AWS account, create a role with a trust policy scoped to your repo, and configure your pipeline to request the OIDC token and call `sts:AssumeRoleWithWebIdentity`.
Step 1: Create the OIDC identity provider in AWS
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab063fa8120a90c3970c9eb877743 For GitLab.com, the issuer URL is https://gitlab.com. Self-hosted GitLab uses your instance URL. Thumbprints change rarely; verify against current AWS documentation if creation fails.
Step 2: Write a tight trust policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:myorg/laravel-app:ref:refs/heads/main"
}
}
}
]
} Attach a permissions policy granting only what the deploy needs—S3 sync to a single bucket prefix, ECR push to one repository, or SSM parameter read. Never attach `AdministratorAccess` to a CI role because federation feels "safer." Scoped beats broad every time.
Step 3: GitHub Actions workflow (no keys in secrets)
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy-main
aws-region: ap-south-1
- run: aws s3 sync public/build s3://my-bucket/assets/ The companion article on deploying to AWS from GitHub Actions with OIDC walks through a full Laravel asset-upload pipeline. The same pattern applies to Terraform, ECS task registration, and RDS snapshot exports—anything the AWS CLI or SDK can do with temporary credentials.
Step 4: GitLab CI equivalent
deploy:
id_tokens:
AWS_ID_TOKEN:
aud: sts.amazonaws.com
script:
- >
export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
$(aws sts assume-role-with-web-identity
--role-arn arn:aws:iam::123456789012:role/gitlab-deploy
--role-session-name gitlab-${CI_PIPELINE_ID}
--web-identity-token ${AWS_ID_TOKEN}
--query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]"
--output text))
- aws s3 sync public/build s3://my-bucket/assets/ GitLab's `id_tokens` keyword (available in GitLab 16.0+) is the clean path. For older runners, use the gitlab-actions OIDC flow or migrate the runner version before deleting static keys.
How does Workload Identity Federation compare across AWS, GCP, and Azure?
All three major clouds support federation without long-lived keys, but the naming and setup steps differ enough to trip up multi-cloud teams. The table below summarises what you configure on each platform in 2026.
| Platform | Feature name | IdP registration | Credential type | Typical CI integration |
|---|---|---|---|---|
| AWS | IAM OIDC Identity Provider | IAM → Identity providers | STS temporary access keys (15 min–12 hr) | GitHub Actions, GitLab CI, CircleCI |
| Google Cloud | Workload Identity Federation | IAM → Workforce / Workload Identity Pools | OAuth 2.0 access token for impersonation | GitHub Actions, GKE Workload Identity |
| Azure | Federated identity credentials | App registration → Certificates & secrets → Federated credentials | Azure AD token → ARM / storage access | GitHub Actions, Azure Pipelines OIDC |
Google Cloud Workload Identity Federation
GCP separates workload identity pools (external IdP trust) from service account impersonation (what the workload can do). Create a pool, add an OIDC provider, map attributes, then grant `roles/iam.workloadIdentityUser` on the target service account.
gcloud iam workload-identity-pools create "github-pool" \
--location="global" \
--display-name="GitHub Actions Pool"
gcloud iam workload-identity-pools providers create-oidc "github-provider" \
--location="global" \
--workload-identity-pool="github-pool" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" In GitHub Actions, use google-github-actions/auth@v2 with workload_identity_provider and service_account inputs. The action exchanges the OIDC token for a GCP access token without a JSON key file.
Azure federated credentials
Azure attaches federated credentials directly to an app registration or managed identity. You specify the issuer (`https://token.actions.githubusercontent.com`), subject (`repo:org/repo:environment:production`), and audience (`api://AzureADTokenExchange`). GitHub's azure/login@v2 action handles token exchange. For Azure Pipelines, enable OIDC on the service connection and remove the service principal secret.
What are common mistakes when replacing long-lived cloud keys?
Federation removes static keys but does not remove misconfiguration risk. These failures show up repeatedly in production audits and incident postmortems.
- Over-broad trust policies: Using `StringLike` with `repo:myorg/*` when you meant a single repository. Any repo in the org inherits deploy access.
- Missing audience checks: Skipping `aud` validation allows token replay from unintended clients. Always set and verify audience on both sides.
- Keeping old keys "just in case": Dual-running federation and static keys defeats the purpose. Delete unused IAM users and rotate any remaining keys on a 90-day schedule until gone.
- Administrator roles for CI: Terraform plans do not need `iam:*`. Grant least privilege per pipeline stage—plan role read-only, apply role write scoped to one state bucket and one DynamoDB lock table.
- Ignoring self-hosted runners: Self-hosted GitHub or GitLab runners are part of your trust boundary. Harden the host, restrict who can execute jobs, and use environment protection rules. See self-hosted CI runners setup and security for the full checklist.
- No secret scanning in parallel: Federation prevents new keys from entering CI, but old keys may still sit in git history. Run Gitleaks or similar scanning in CI until history is clean.
Kubernetes workloads on AWS and GCP
Container workloads use the same federation concept through pod-level identity. On AWS EKS, configure IAM Roles for Service Accounts (IRSA) by associating an OIDC provider with the cluster and annotating the Kubernetes service account with a role ARN. On GKE, enable Workload Identity to bind a Kubernetes service account to a GCP service account. Neither approach requires embedding keys in pod specs or ConfigMaps—both are federation under a different label.
How do you migrate CI/CD pipelines from static keys to federated identity?
Treat migration as a phased cutover, not a big-bang secret deletion. A pattern I've used on production Laravel deployments:
- Inventory existing keys: List IAM users and access keys with CloudTrail or IAM credential reports. Note which pipelines, cron jobs, and developers hold copies.
- Create federated roles in parallel: Build new roles with identical or tighter permissions. Test on a staging branch before touching production deploys.
- Update one pipeline at a time: Start with the lowest-risk job—asset upload to S3, not database migrations. Confirm CloudTrail shows `AssumeRoleWithWebIdentity` events.
- Add branch and environment guards: Production roles should require `environment:production` or an equivalent protected-environment claim. Staging roles get a separate trust condition.
- Disable and delete old keys: Deactivate the access key, wait one full deploy cycle, then delete the IAM user if it exists only for CI. Document the change in your runbook.
- Enable continuous auditing: Alert on any `CreateAccessKey` API call. Pair federation with OAuth and token security best practices if your pipeline also calls third-party APIs.
Terraform for reproducible trust relationships
Define OIDC providers and roles in Terraform so staging and production stay consistent. Pin provider versions per your existing module standards.
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab063fa8120a90c3970c9eb877743"]
}
resource "aws_iam_role" "deploy" {
name = "github-deploy-production"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRoleWithWebIdentity"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:myorg/laravel-app:environment:production"
}
}
}]
})
} Store Terraform state in an encrypted S3 bucket with a DynamoDB lock table—also accessed via federation, not a personal admin key. That closes the loop: your infrastructure code and your deploy pipeline share the same zero-long-lived-key model.
When static keys are still acceptable
Federation is not universal. Legacy on-prem software that cannot perform OIDC token exchange, some third-party SaaS integrations, and local developer machines often still use access keys or named profiles. The goal is containment: static keys belong in a password manager or AWS Secrets Manager with rotation, scoped to one human user, never checked into git. Workloads that can federate should federate.
Cost and operational overhead
Federation itself is free on AWS, GCP, and Azure—you pay for the API calls your workload makes after authentication, not for the trust relationship. Setup time for a single-repo GitHub-to-AWS pipeline is typically one to two hours including testing. Multi-environment setups with separate roles add another hour. Compare that to the cost of a leaked production key: incident response, key rotation across every service, potential data exposure, and client notification. For Nepal-based startups operating on tight budgets (Rs 5,000–15,000/month hosting, roughly USD 37–110), federation is cheap insurance.
Ready to remove long-lived keys from your pipelines?
Workload Identity Federation Without Long-Lived Keys is the baseline security posture for CI/CD in 2026—not an optional hardening step for enterprise teams. Configure OIDC trust on your cloud account, scope roles to specific repositories and environments, migrate one pipeline at a time, and delete the static keys once CloudTrail confirms federated assumption. If you want help wiring federation into a Laravel deploy pipeline on AWS or auditing existing IAM users on a production VPS, get in touch—I set this up regularly on client projects and sister-site infrastructure.

