How to Implement Ephemeral Certificates and OIDC Authentication in CI/CD Pipelines
The proliferation of microservices, cloud-native architectures, and DevSecOps has triggered an explosion in machine identities. Every container, artifact, and service deployed through your CI/CD pipeline requires a cryptographic identity to communicate securely.
Historically, managing these identities meant generating long-lived certificates or static API tokens, storing them in CI/CD environment variables, and hoping they never leaked. Today, this approach is a massive security liability. With the industry moving toward 90-day maximum lifespans for public TLS certificates and strictly enforcing Zero Trust architectures, pipelines must be capable of automatically provisioning, utilizing, and discarding certificates without human intervention.
This tutorial covers how to eliminate static secrets in your deployment workflows by integrating OpenID Connect (OIDC) authentication, dynamically provisioning ephemeral certificates via HashiCorp Vault, and implementing keyless code signing using Sigstore.
The Danger of Hardcoded Pipeline Secrets
A common anti-pattern in deployment automation is the "Shadow PKI." To bypass slow IT ticketing systems, developers often spin up unauthorized self-signed certificates, or worse, commit private keys directly into Git repositories to make builds work.
Even when teams use dedicated CI/CD secret variables (like GitHub Secrets or GitLab CI/CD variables) to store infrastructure credentials or API tokens for their internal Certificate Authority (CA), they introduce significant risk:
1. Token Sprawl: Long-lived API tokens used to fetch certificates can be exfiltrated via compromised dependencies or malicious pull requests.
2. Lack of Auditing: When a single static token requests hundreds of certificates across different pipeline runs, security teams lose the ability to audit which specific job requested which certificate.
3. Expiration Outages: Hardcoded credentials inevitably expire. If the API token used by the pipeline to request TLS certificates expires silently, the pipeline breaks, preventing emergency hotfixes from reaching production.
The modern solution is Identity Federation via OIDC. Instead of giving the pipeline a password, the pipeline uses its own cryptographically verifiable identity to request short-lived, ephemeral certificates at runtime.
Step 1: Establishing OIDC Trust Between CI/CD and Your CA
OpenID Connect (OIDC) allows your CI/CD runner to request a JSON Web Token (JWT) from its provider (e.g., GitHub, GitLab, AWS). The runner presents this JWT to your Secrets Manager or PKI (e.g., HashiCorp Vault). Vault verifies the JWT's signature, checks the claims (such as the repository name and branch), and grants access to generate a certificate.
Here is how to configure HashiCorp Vault to trust GitHub Actions using OIDC.
Configuring Vault
First, enable the JWT authentication method in Vault and point it to GitHub's OIDC discovery URL:
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 policy that grants permission to issue certificates from your PKI secrets engine. In this example, we allow the pipeline to generate certificates for the internal.example.com domain:
vault policy write pipeline-cert-policy - <<EOF
path "pki_int/issue/internal-dot-com" {
capabilities = ["create", "update"]
}
EOF
Finally, create a Vault role that binds the GitHub repository and branch to the policy. This is the core of OIDC security: Vault will only issue a certificate if the request comes from the main branch of your specific repository.
vault write auth/jwt/role/github-actions-role \
role_type="jwt" \
policies="pipeline-cert-policy" \
token_ttl="15m" \
bound_audiences="https://github.com/your-org" \
bound_claims_type="glob" \
bound_claims='{
"repository": "your-org/your-repo",
"ref": "refs/heads/main"
}' \
user_claim="repository"
Step 2: Dynamically Provisioning Certificates in the Pipeline
With the trust established, you can configure your GitHub Actions workflow to request a certificate.
Crucially, you must grant the workflow the id-token: write permission. This allows the GitHub runner to generate the OIDC JWT. We will use the official hashicorp/vault-action to handle the authentication and certificate generation automatically.
name: Deploy Microservice with mTLS
on:
push:
branches: [ "main" ]
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Fetch Ephemeral Certificate from Vault
uses: hashicorp/vault-action@v3
id: vault
with:
url: https://vault.internal.example.com:8200
role: github-actions-role
method: jwt
secrets: |
pki_int/issue/internal-dot-com common_name="api.internal.example.com" ttl="1h" certificate | TLS_CERT ;
pki_int/issue/internal-dot-com common_name="api.internal.example.com" ttl="1h" private_key | TLS_KEY
- name: Deploy to Infrastructure
run: |
# The certificate and key are now available as environment variables
# They are injected directly into memory and never written to disk
echo "Deploying service with dynamically generated mTLS certificates..."
./deploy-script.sh
env:
CERTIFICATE: ${{ steps.vault.outputs.TLS_CERT }}
PRIVATE_KEY: ${{ steps.vault.outputs.TLS_KEY }}
Security Considerations for Injection
Notice that the certificate and private key are injected as environment variables. They are never written to the runner's disk. If your deployment tool strictly requires a file path (e.g., a legacy CLI tool), write the credentials to a temporary RAM disk (/dev/shm on Linux) rather than standard storage, ensuring they are wiped the moment the container terminates.
Because the certificate is generated with a Time-To-Live (TTL) of just one hour, explicit revocation (via CRLs or OCSP) is largely unnecessary. If the certificate is intercepted, its window of usefulness is incredibly narrow.
Step 3: Securing the Supply Chain with Ephemeral Code Signing
Certificates in CI/CD pipelines are not just for establishing mTLS or HTTPS; they are now a mandatory component of software supply chain security. Frameworks like the Secure Software Development Framework (SSDF) and US Executive Order 14028 mandate that software artifacts be cryptographically signed to prove provenance.
Managing long-lived GPG or RSA keys for code signing is notoriously difficult. If a developer's laptop is compromised or a CI/CD variable leaks, the signing key can be used to sign malware, masquerading as your official software.
The industry standard solution is keyless signing using Sigstore. Sigstore's Fulcio component acts as a Root CA that issues ephemeral, short-lived certificates based on an OIDC identity. The pipeline signs the container image, records the signature and certificate in an immutable transparency log (Rekor), and immediately discards the private key.
Here is how to implement ephemeral code signing for a Docker container in GitHub Actions using Sigstore's cosign CLI:
```yaml
name: Build and Sign Container
on:
release:
types: [published]
permissions:
id-token: write # Required for Sigstore OIDC
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
- name: Log into GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Docker Image
id: build-and-push
uses: docker/build-push-action@v5