← All posts
· 5 min read ·
SecurityCI/CDAWSDevOpsZero Trust

Zero Static Credentials in CI/CD: SPIFFE, SPIRE, and OIDC Federation

GitHub Actions OIDC, AWS IAM OIDC federation, EKS Pod Identity, and SPIFFE/SPIRE now compose into a coherent architecture where no pipeline or workload holds a static credential. Here is how the pieces fit together.

Identity security and authentication diagram

Static credentials in CI/CD pipelines are an incident waiting to happen. AWS_SECRET_ACCESS_KEY stored as a GitHub secret, rotated annually when someone remembers, with read access to a production S3 bucket. When the secret is stolen - via a malicious action, a compromised runner, a developer’s exported environment - the attacker has persistent access until someone notices and rotates.

The alternative architecture is not new but has only recently become practical without a dedicated platform engineering team. GitHub Actions OIDC, AWS IAM OIDC federation, EKS Pod Identity, and SPIFFE/SPIRE now compose into a complete zero-static-credential pipeline that covers the CI build, the deployment, and the running workload.

The Building Block: OIDC Identity in GitHub Actions

GitHub Actions workflows have access to a short-lived OIDC JWT issued by GitHub’s identity service. This token contains cryptographically signed claims about the workflow: the repository, the branch, the environment, the workflow file, and the triggering event.

jobs:
  deploy:
    permissions:
      id-token: write  # enables OIDC token request
      contents: read
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b1a7af3c0318edc7b
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: eu-west-1

The configure-aws-credentials action requests a GitHub OIDC token, sends it to AWS STS via AssumeRoleWithWebIdentity, and AWS validates the token against GitHub’s OIDC discovery endpoint. If the token’s claims match the trust policy on the IAM role, STS issues short-lived credentials (15 minutes to 1 hour). No stored secret. No rotation. No AWS_SECRET_ACCESS_KEY in GitHub’s secret store.

The IAM trust policy controls exactly which workflows can assume the role:

{
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
      "token.actions.githubusercontent.com:sub": "repo:myorg/myapp:environment:production"
    }
  }
}

The sub claim scopes the trust to a specific repository and environment. A compromised workflow in a different repository cannot assume this role even with a valid GitHub OIDC token.

EKS Pod Identity: Eliminating kube2iam and IRSA Complexity

Inside the cluster, workloads also need AWS credentials. The traditional approaches - kube2iam (IMDS proxy), IRSA (IAM Roles for Service Accounts) - both work but have operational complexity. kube2iam requires DaemonSet deployment and has race conditions; IRSA requires annotating service accounts and maintaining the OIDC provider configuration per cluster.

EKS Pod Identity (GA since late 2023, widely deployed by 2026) simplifies this significantly. You create an association between an IAM role and a Kubernetes service account via the EKS API:

aws eks create-pod-identity-association \
  --cluster-name my-cluster \
  --namespace app-namespace \
  --service-account app-service-account \
  --role-arn arn:aws:iam::123456789012:role/app-role

Pods using that service account automatically receive short-lived AWS credentials via the EKS Pod Identity Agent, without requiring OIDC annotation on the service account or management of the OIDC provider thumbprint. The credentials are per-pod (not per-node, as with the instance profile approach) and expire every 15 minutes with automatic rotation.

The IAM role’s trust policy for Pod Identity uses a different principal than IRSA:

{
  "Principal": {
    "Service": "pods.eks.amazonaws.com"
  }
}

EKS Pod Identity is the recommended approach for new clusters. For existing clusters using IRSA, the migration path is straightforward and the two mechanisms can coexist during transition.

SPIFFE/SPIRE: Identity Beyond AWS

OIDC federation and Pod Identity handle AWS credential issuance cleanly. They do not help when workloads need to authenticate to each other, to non-AWS services, or across clouds and on-premise environments.

SPIFFE (Secure Production Identity Framework for Everyone) defines a standard for workload identity: each workload gets a SVID (SPIFFE Verifiable Identity Document) - an X.509 certificate or JWT with a standardised URI identity (spiffe://trust-domain/path/to/workload). SPIRE (SPIFFE Runtime Environment) is the reference implementation that issues and rotates SVIDs.

The SPIRE architecture:

  • A SPIRE Server holds the signing certificate authority
  • SPIRE Agents run on each node and perform workload attestation - verifying that a process requesting an SVID is actually the workload it claims to be (via pod UID in Kubernetes, process ID on VMs, or node characteristics)
  • Workloads receive their SVID via the Workload API and use it for mTLS to other workloads or to exchange for AWS credentials at STS

Red Hat shipped SPIRE-based workload identity as GA on OpenShift in early 2026, which signals that this architecture is mature enough for enterprise production use.

The AWS STS integration closes the loop: a workload presents its SVID to STS, which validates it against the SPIRE trust root and issues short-lived AWS credentials. This means even workloads that are not on AWS (on-premise, GCP, another cloud) can obtain scoped AWS credentials using the same identity mechanism as Kubernetes workloads.

The Complete Picture

The architecture eliminates static credentials at every point in the delivery chain:

GitHub Actions workflow
  -> GitHub OIDC JWT
  -> AWS STS AssumeRoleWithWebIdentity
  -> Short-lived build/deploy credentials (15-60 min)

Kubernetes workload (EKS)
  -> EKS Pod Identity Agent
  -> Per-pod short-lived AWS credentials (15 min, auto-rotated)

Non-AWS workload (any environment)
  -> SPIRE Agent attestation
  -> SVID (X.509, 1 hour, auto-rotated)
  -> AWS STS via SVID federation
  -> Short-lived AWS credentials

At no point in this chain does any system hold a credential with a lifetime beyond what is needed for the current operation. A compromised runner, container, or process yields credentials that expire within an hour at most, cannot be renewed without re-attestation, and are scoped to the specific operations that workload requires.

What This Does Not Solve

The zero-static-credential architecture does not protect against:

Overly broad IAM permissions. A short-lived credential with s3:* on * is still dangerous. Least-privilege IAM scoping is still required; OIDC federation does not substitute for it.

Privilege escalation within the session. If the IAM role can call iam:PassRole or sts:AssumeRole to more privileged roles, a compromised workload can escalate. Permissions boundaries and SCPs contain this.

Credential use during the valid window. A 15-minute credential can still be used to download a database dump in 15 minutes. The shorter lifetime reduces the attack window but does not eliminate it.

These are orthogonal controls that must be applied alongside the zero-static-credential architecture, not instead of it. The architecture solves the “stolen credentials that persist for months” problem. IAM scoping, permissions boundaries, and network controls handle the remaining surface.

The Datadog DevSecOps 2026 Report notes that 40% of services have exploitable vulnerabilities. Credentials stored statically in those services are available to any exploit that achieves code execution. Credentials that expire in 15 minutes and require live attestation to renew are not. That is the meaningful security improvement this architecture provides.

← All posts