Beyond the Vault: Transitioning to Secretless CI/CD with SPIFFE/SPIRE and OIDC
Beyond the Vault: Transitioning to Secretless CI/CD with SPIFFE/SPIRE and OIDC
For years, the standard for securing CI/CD pipelines has been simple but flawed: secrets management. We store API keys, database passwords, and cloud credentials in "vaults" (like HashiCorp Vault, AWS Secrets Manager, or GitHub Secrets), and then inject them into our build runners as environment variables.
While this is better than hardcoding, it creates a new set of problems. Secrets must be rotated, they have no intrinsic identity, and if a CI/CD runner is compromised, the static token it holds is a skeleton key for your infrastructure.
In this article, we explore the cutting edge of DevSecOps: Secretless CI/CD. By leveraging the Secure Production Identity Framework for Everyone (SPIFFE) and its runtime environment (SPIRE), alongside OpenID Connect (OIDC), we can move toward a world where workloads authenticate based on who they are rather than what they know.
The Fundamental Flaw of Secret-Based Security
In a traditional pipeline, your CI runner (e.g., a GitHub Actions runner or a Jenkins agent) proves its identity to a cloud provider like AWS by presenting a static AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
This approach has three major architectural weaknesses:
- Secret Proliferation: Every new pipeline requires a new set of credentials, increasing the surface area for leaks.
- Lifecycle Management: Rotating secrets across hundreds of microservices is a logistical nightmare that often results in "never-expire" tokens.
- Lack of Context: A static token doesn't know if it's being used by a legitimate build process or a malicious actor who exfiltrated the variable.
The Solution: Workload Identity and Attestation
Instead of handing a runner a key, we should provide it with an Identity.
Workload identity relies on Attestation. This is the process where a trusted third party verifies the attributes of the runner—such as its cloud provider ID, its SHA-256 hash, or its git branch—and issues a short-lived, cryptographically signed document (a SVID or a JWT) that the runner can then exchange for access to resources.
Enter SPIFFE and SPIRE
SPIFFE is a set of open-source standards for software identity. It defines a standard for how services identify themselves. SPIRE is the reference implementation of SPIFFE.
SPIRE works by having a Server and several Agents. When a process starts, the SPIRE Agent performs "Workload Attestation." It looks at the process ID, the kernel metadata, and the environment. If the process matches the predefined criteria, SPIRE grants it a SPIFFE ID and a short-lived certificate (SVID).
Architecting a Secretless Pipeline with OIDC
While SPIRE is powerful for internal infrastructure, many modern CI/CD platforms like GitHub Actions, GitLab, and CircleCI now support OIDC (OpenID Connect) natively. This is the first step toward secretless architecture.
The OIDC Flow
- Request Identity: The CI runner requests an OIDC token from the CI Provider (the Identity Provider).
- Issue Token: The Provider issues a JWT (JSON Web Token) containing claims about the job (e.g.,
repository: "org/repo",workflow: "deploy",ref: "refs/heads/main"). - Exchange: The runner sends this JWT to the Cloud Provider (e.g., AWS or GCP).
- Validate and Assume: The Cloud Provider validates the JWT signature against the CI Provider's public keys. If valid and the claims match an IAM policy, it issues a temporary, short-lived session token (usually valid for 15-60 minutes).
Implementation Example: GitHub Actions to AWS via OIDC
To move away from static IAM user keys, we first configure a Trust Relationship in AWS.
1. Define the Trust Policy (Terraform/HCL)
This policy ensures that only your specific GitHub repository can assume the role, and only when running on the main branch.
resource "aws_iam_role" "github_actions_role" {
name = "GitHubActionsDeployRole"
assume_role_policy = jsonencode({
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:my-org/my-repo:ref:refs/heads/main"
}
}
}
]
})
}
2. The Secretless Workflow (GitHub Actions YAML)
Notice there are no secrets used for AWS credentials here. The permissions block allows the runner to fetch the OIDC ID token.
name: Secretless Deployment
on:
push:
branches: [ main ]
permissions:
id-token: write # Required for requesting the JWT
contents: read # Required for checkout
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
aws-region: us-east-1
- name: Deploy to S3
run: |
aws s3 sync ./build s3://my-deployment-bucket
Deep Dive: Advanced Attestation with SPIRE for Hybrid Clouds
What if your CI/CD isn't on GitHub? What if you are running custom runners on-premise that need to access secret-managed resources in multiple clouds? This is where SPIRE shines.
SPIRE provides a concept called "Node Attestation." A SPIRE Agent running on a build server can prove its identity using the TPM (Trusted Platform Module) of the hardware or the instance identity document of a VM.
The "SVID Fetch" Pattern
When a build job starts on a SPIRE-controlled runner, the job doesn't look for an environment variable. Instead, it connects to a local Unix Domain Socket managed by the SPIRE Agent.
# Conceptual Python example of a CI script fetching identity from SPIRE
import os
from pyspire import SpiffeId, Svid
def get_cloud_client():
# Connect to the SPIRE Workload API socket
# No API keys required!
svid = Svid.fetch_from_agent(socket_path="/run/spire/sockets/agent.sock")
# Use the SVID (X.509 certificate) to authenticate via mTLS
# to a database, a vault, or a cloud provider's federated identity endpoint
return create_authenticated_client(svid)
client = get_cloud_client()
print("Successfully authenticated using SPIFFE ID!")
This pattern ensures that identity is never stored on disk. It exists only in memory for the duration of the process.
Real-World Use Case: The Zero-Trust Build Pipe
Imagine a FinTech company with strict compliance requirements. They use an internal Jenkins cluster. In a traditional setup, Jenkins has a "Global Credentials" store containing a master key to the Kubernetes cluster. If Jenkins is hacked, the whole cluster is lost.
By implementing SPIFFE/SPIRE:
- Each Jenkins Job is assigned a specific SPIFFE ID based on its name and the Git repository it pulls from.
- The Jenkins Agent attests that the job is indeed the one it claims to be.
- The Kubernetes API server is configured to accept SPIFFE SVIDs for authentication.
- The Job can only access the specific namespace it is assigned to, even if other jobs on the same agent are compromised.
Performance and Scalability Considerations
Transitioning to identity-based CI/CD does introduce a slight overhead. The OIDC exchange or SPIRE attestation adds a small amount of latency to the start of a job—typically under 500ms. However, the security gains far outweigh this cost.
Furthermore, because tokens are short-lived, the risk of a "token replay" attack is minimized. If a token is stolen, it will likely expire before it can be effectively used in a different context.
Summary
The era of long-lived secrets in CI/CD is coming to an end. As Senior Architects, we must move toward the Identity-as-Code paradigm. By utilizing OIDC for cloud-native workflows and SPIRE for complex or on-premise environments, we can build pipelines that are:
- Ephemeral: Identities last only as long as the job.
- Verifiable: Identities are backed by cryptographic proof and attestation.
- Maintainable: No more manual rotation or secret leaking in logs.
Investing in a secretless architecture today not only secures your supply chain but also simplifies the developer experience by removing the friction of credential management.