Automating Short-Lived Certificate Issuance in CI/CD Pipelines
The integration of Public Key Infrastructure (PKI) into CI/CD pipelines is undergoing a fundamental architectural shift. For years, the standard operating procedure for securing pipeline communications and signing artifacts involved generating long-lived certificates or API keys and storing them as static secrets in platforms like Jenkins, GitLab CI, or GitHub Actions.
Today, that approach is a massive liability. The volume of machine identities—containers, microservices, and automated runners—has vastly outpaced human identities. Furthermore, aggressive industry shifts, such as Google’s proposal to reduce maximum public TLS certificate lifespans to 90 days, mean that manual certificate provisioning is no longer mathematically viable. Compounding this is the urgency around software supply chain security; the aftermath of the SolarWinds breach highlighted exactly what happens when build environments rely on static, easily exfiltrated cryptographic keys.
To secure modern software delivery, DevOps teams must eliminate long-lived pipeline secrets. The modern standard relies on crypto-agility, OpenID Connect (OIDC), and ephemeral certificates that exist only for the duration of a single pipeline run.
The Danger of Static Secrets and Shadow PKI
When developers need a certificate to authenticate a CI/CD runner to a deployment environment or to sign a Docker image, the traditional IT ticketing process is often too slow. To bypass this friction, engineering teams frequently resort to "Shadow PKI." They generate self-signed certificates with ten-year expirations or export .p12 files and hardcode them directly into Git repositories or CI/CD secret stores.
This creates two critical vulnerabilities:
- Secret Sprawl and Exfiltration: A long-lived certificate sitting in a CI/CD variable store is a high-value target. If an attacker compromises a runner or gains access to the repository, they can exfiltrate that certificate and use it indefinitely to authenticate to production systems or sign malicious code under the company's name.
- Pipeline Outages: Because these certificates are generated outside of centralized IT visibility, their expiration dates are rarely tracked. A recent Keyfactor report noted that over 75% of organizations experienced a system outage due to an expired certificate. In automated DevOps environments, a single expired internal certificate can halt all production deployments instantly.
Transitioning to Ephemeral Machine Identities
The solution to secret sprawl and untracked expirations is to stop storing certificates altogether. Instead, pipelines should utilize ephemeral machine identities.
An ephemeral certificate is dynamically requested at the exact moment it is needed, used for its specific purpose (such as mutual TLS authentication or code signing), and then naturally expires minutes or hours later. Even if an attacker manages to scrape the certificate from the runner's memory, it will be useless by the time they attempt to weaponize it.
A modern, secure CI/CD certificate integration follows this exact lifecycle:
1. Authentication: The CI/CD runner starts a job and requests a JSON Web Token (JWT) from its native platform via OIDC.
2. Authorization: The runner presents this JWT to a Secrets Manager (like HashiCorp Vault) or a Machine Identity platform.
3. Issuance: The platform verifies the OIDC token's cryptographic claims (repository name, branch, environment) and issues a short-lived X.509 certificate.
4. Execution: The runner uses the certificate to authenticate to a target environment or sign an artifact.
5. Expiration: The certificate expires naturally, requiring no manual revocation.
Implementing OIDC with HashiCorp Vault
To see this in practice, consider a GitHub Actions workflow that needs to deploy code to a Kubernetes cluster requiring mutual TLS (mTLS). Instead of storing the client certificate in GitHub Secrets, we can configure a trust relationship between GitHub and HashiCorp Vault.
First, configure Vault to trust GitHub's OIDC provider. Using the Vault CLI, you enable the JWT auth method and point it to GitHub:
vault auth enable jwt
vault write auth/jwt/config \
oidc_discovery_url="https://token.actions.githubusercontent.com" \
bound_issuer="https://token.actions.githubusercontent.com"
Next, create a Vault role that explicitly defines which GitHub repositories and branches are allowed to request certificates from your Vault PKI engine:
vault write auth/jwt/role/github-actions-deploy \
role_type="jwt" \
policies="pki-issue-policy" \
token_ttl="1h" \
bound_audiences="https://github.com/your-org" \
bound_claims_type="glob" \
bound_claims='{"sub": "repo:your-org/your-repo:ref:refs/heads/main"}' \
user_claim="actor"
In your GitHub Actions YAML file, you can now use the OIDC token to authenticate to Vault and generate a short-lived certificate (e.g., valid for 5 minutes) directly into the runner's workspace:
name: Deploy to Production
on:
push:
branches:
- main
# Required to allow GitHub to request an OIDC JWT
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Import Secrets and Generate Certificate
uses: hashicorp/vault-action@v3
with:
url: https://vault.your-org.internal:8200
role: github-actions-deploy
method: jwt
secrets: |
pki/issue/k8s-deploy-role certificate | TLS_CERT ;
pki/issue/k8s-deploy-role private_key | TLS_KEY
- name: Deploy with mTLS
run: |
echo "$TLS_CERT" > client.crt
echo "$TLS_KEY" > client.key
# Execute deployment tool using the ephemeral certificates
kubectl --client-certificate=client.crt --client-key=client.key apply -f deployment.yaml
In this architecture, there are zero static secrets. The cryptographic keys exist only in the runner's memory during execution and expire before they can be misused.
Securing the Supply Chain with Keyless Code Signing
While mTLS secures the transport layer, CI/CD pipelines are also responsible for securing the software supply chain. Under frameworks like SLSA (Supply-chain Levels for Software Artifacts), organizations are required to cryptographically sign their build artifacts and Software Bill of Materials (SBOMs).
Managing long-lived code-signing certificates is notoriously difficult. If a code-signing key is compromised, attackers can sign malware that operating systems and clusters will trust as legitimate software.
To solve this, the industry is rapidly adopting "keyless" signing through Sigstore. Tools like Cosign leverage OIDC to bind a machine identity to a short-lived certificate generated on the fly by Sigstore's certificate authority (Fulcio). The artifact is signed, and the signature is recorded in an immutable transparency log (Rekor).
Here is how you implement keyless signing in a pipeline:
name: Build and Sign Container
on: [push]
permissions:
id-token: write # Required for OIDC and Sigstore
packages: write
contents: read
jobs:
build-and-sign:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Cosign
uses: sigstore/cosign-installer@v3.5.0
- name: Build and Push Docker Image
id: build-image
run: |
docker build -t ghcr.io/your-org/app:${{ github.sha }} .
docker push ghcr.io/your-org/app:${{ github.sha }}
- name: Sign the published Docker image
env:
TAGS: ghcr.io/your-org/app:${{ github.sha }}
run: |
# Cosign automatically requests an ephemeral certificate via OIDC,
# signs the image, and logs it to the transparency log.
cosign sign --yes ${TAGS}
Because the certificate used to sign the code expires within 10 minutes, there is no need for complex key rotation policies or revocation lists. The transparency log proves that the signature was valid at the exact time the code was built by the authorized CI/CD runner.
Establishing Zero-Trust with SPIFFE and SPIRE
For complex, multi-cloud CI/CD environments where pipelines span across on-premises Jenkins servers, AWS CodeBuild, and Kubernetes clusters, OIDC alone may not provide enough granularity. This is where SPIFFE (Secure Production Identity Framework for Everyone) becomes critical.
SPIFFE provides a standard for securely identifying software systems. Its implementation, SPIRE, dynamically provisions short-lived SVIDs (SPIFFE Verifiable Identity Documents)—which are effectively X.509 certificates—based on deep cryptographic attestation of the workload.
Instead of a Jenkins runner simply asking for a certificate, a SPIRE agent running on the build node will verify the runner's process ID, its container cgroup, and the cryptographic hash of its binary. Only if all attestations match the security policy will SPIRE issue an ephemeral certificate (e.g., spiffe://example.org/ci/jenkins-runner-prod) to the pipeline. This ensures that even if an attacker injects malicious code into a pipeline script, the underlying runner cannot request a production deployment certificate if its process signature has been tampered with.
Infrastructure Visibility and Expiration Tracking
Transitioning to ephemeral certificates drastically reduces the attack surface of your CI/CD pipelines, but it does not eliminate the need for certificate lifecycle management. In fact, it shifts the monitoring requirement from the leaf certificates to