Certificate Automation in CI/CD
Key Takeaways
- CI/CD pipelines can request, validate, and deploy certificates as part of the release process — no manual certificate management
- Short-lived certificates issued per-deployment eliminate the need for renewal infrastructure — each deploy gets a fresh cert
- Pipeline-managed certificates must be verified after deployment: confirm the new cert is actually being served, not just written to disk
- Certificate automation in CI/CD requires the pipeline to have CA access — securing this access is critical (OIDC, short-lived tokens, not long-lived keys)
Certificate automation in CI/CD integrates certificate lifecycle operations directly into your deployment pipeline. Instead of managing certificates separately from application deployments, the pipeline handles everything: requesting certificates from a CA, validating domain ownership, deploying the certificate to the target infrastructure, reloading the TLS-terminating service, and verifying the new certificate is live. This ensures every deployment has a valid certificate and eliminates the gap between “application deployed” and “certificate configured.”
Why it matters
- Deployment-time provisioning — new services get certificates automatically as part of their first deployment. No separate ticket, no waiting for a CA team, no manual steps.
- Environment parity — staging, QA, and production all get certificates through the same automated process. No more “works in prod but staging has an expired self-signed cert.”
- Short-lived certificates per deploy — each deployment can request a fresh certificate, making certificate lifetime match deployment frequency. Deploy daily? Certificate is always fresh.
- Drift prevention — certificates are declared in code (IaC) and provisioned by the pipeline. Manual changes are overwritten on next deploy. Configuration drift is impossible.
- Faster incident response — need to rotate a certificate immediately? Trigger the pipeline. The same automation that deploys normally handles emergency rotation.
How it works
- Certificate declared in code — the required certificate (domain, SANs, issuer, key type) is defined in infrastructure code (Terraform, Helm, Kubernetes manifests).
- Pipeline triggers — on merge to main or tag creation, the CI/CD pipeline starts.
- Certificate requested — pipeline calls the CA (ACME, Vault PKI, cloud KMS) to issue a certificate. Authentication via OIDC workload identity or short-lived token.
- Challenge completed — for ACME: pipeline provisions DNS record or HTTP challenge response. For private CA: pipeline authenticates with service credentials.
- Certificate deployed — pipeline writes the certificate to the target (Kubernetes Secret, cloud load balancer, secrets manager, server filesystem).
- Service reloaded — pipeline triggers the TLS-terminating service to load the new certificate (rolling restart, config reload, API call).
- Verification — pipeline connects to the endpoint and confirms the new certificate is being served (checks serial number, expiry, SANs).
- Rollback on failure — if verification fails, pipeline reverts to the previous certificate and alerts.
In real systems
GitHub Actions with cert-manager (Kubernetes):
jobs:
deploy:
steps:
- name: Deploy application + certificate
run: |
helm upgrade --install myapp ./chart \
--set ingress.hosts[0]=app.example.com \
--set ingress.tls[0].secretName=app-tls \
--set ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt-prod
# cert-manager handles certificate issuance automatically
# Wait for certificate to be ready
kubectl wait --for=condition=Ready certificate/app-tls -n production --timeout=120s
- name: Verify certificate is served
run: |
sleep 10 # Allow ingress controller to reload
SERVED_CN=$(echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null | openssl x509 -noout -subject)
echo "Served certificate: $SERVED_CN"
echo "$SERVED_CN" | grep -q "app.example.com" || exit 1
Terraform with AWS ACM:
resource "aws_acm_certificate" "app" {
domain_name = "app.example.com"
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
resource "aws_route53_record" "cert_validation" {
for_each = {
for dvo in aws_acm_certificate.app.domain_validation_options : dvo.domain_name => dvo
}
name = each.value.resource_record_name
type = each.value.resource_record_type
zone_id = data.aws_route53_zone.main.zone_id
records = [each.value.resource_record_value]
ttl = 60
}
resource "aws_acm_certificate_validation" "app" {
certificate_arn = aws_acm_certificate.app.arn
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}
# ALB listener references the validated certificate
resource "aws_lb_listener" "https" {
certificate_arn = aws_acm_certificate_validation.app.certificate_arn
# ...
}
Vault PKI in GitLab CI:
deploy:
script:
- |
# Authenticate to Vault with CI JWT
export VAULT_TOKEN=$(vault write -field=token auth/jwt/login \
role=deploy jwt=$CI_JOB_JWT)
# Request certificate from Vault PKI
vault write -format=json pki/issue/web-server \
common_name="api.example.com" \
alt_names="api-v2.example.com" \
ttl="720h" > cert.json
# Extract and deploy
jq -r '.data.certificate' cert.json > /etc/ssl/api.pem
jq -r '.data.private_key' cert.json > /etc/ssl/api-key.pem
jq -r '.data.ca_chain[]' cert.json >> /etc/ssl/api.pem
# Reload
nginx -t && nginx -s reload
Where it breaks
Pipeline has long-lived CA credentials — the pipeline authenticates to the CA with a static API key or long-lived service account. If the pipeline is compromised (malicious PR, dependency attack), the attacker can request certificates for any domain the CA allows. Use OIDC workload identity (GitHub Actions OIDC, GitLab CI JWT) for short-lived, scoped authentication to the CA — no stored secrets.
Certificate issued but deployment fails — the pipeline successfully requests a certificate from the CA, but the deployment step fails (Kubernetes API timeout, permission error, network issue). The certificate exists but isn’t deployed. On retry, the pipeline requests another certificate (hitting rate limits) instead of using the already-issued one. Implement idempotent certificate handling: check if a valid certificate already exists before requesting a new one.
No verification step — the pipeline deploys the certificate and reports success. But the ingress controller didn’t reload, or the certificate was written to the wrong path, or the chain is incomplete. Without a verification step that actually connects to the endpoint and checks the served certificate, the pipeline gives false confidence. Always verify by connecting to the live endpoint after deployment.
Operational insight
The most mature certificate automation pattern treats certificates as ephemeral deployment artifacts — not long-lived infrastructure. Each deployment requests a fresh certificate (or validates the existing one), deploys it, and verifies it. If the certificate needs rotation, you simply re-run the deployment pipeline. This eliminates the entire category of “certificate renewal” as a separate operational concern — renewal is just another deployment. The prerequisite: your pipeline must be reliable enough to run on-demand for certificate rotation, not just for code changes. If your pipeline is flaky or slow, certificate automation becomes a liability rather than an asset.
Related topics
Ready to Secure Your Enterprise?
Experience how our cryptographic solutions simplify, centralize, and automate identity management for your entire organization.