SSL Certificate Automation in DevOps: Complete CI/CD Integration Guide
Modern DevOps practices demand seamless SSL certificate automation that integrates with continuous deployment pipelines, Infrastructure as Code (IaC), and container orchestration platforms. This comprehensive guide covers enterprise-grade certificate automation strategies that ensure security, scalability, and operational efficiency.
DevOps SSL Automation Fundamentals
Why Automate SSL Certificates in DevOps?
Operational Benefits:
- Zero-Touch Deployment: Eliminate manual certificate management tasks
- Consistent Security: Standardize certificate policies across environments
- Rapid Scaling: Automatically provision certificates for dynamic infrastructure
- Reduced Downtime: Prevent certificate-related outages through automation
Security Advantages:
- Standardized Configurations: Enforce security policies programmatically
- Audit Trails: Track certificate lifecycle through version control
- Compliance: Meet regulatory requirements with automated documentation
- Risk Reduction: Minimize human error in certificate management
Cost Efficiency:
- Resource Optimization: Reduce manual operational overhead
- Time Savings: Accelerate deployment cycles
- Error Prevention: Avoid costly certificate-related incidents
- Scalability: Handle certificate management at enterprise scale
Certificate Automation Principles
Infrastructure as Code (IaC):
# Terraform example for AWS Certificate Manager
resource "aws_acm_certificate" "main" {
domain_name = var.domain_name
subject_alternative_names = var.subject_alternative_names
validation_method = "DNS"
tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "terraform"
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_acm_certificate_validation" "main" {
certificate_arn = aws_acm_certificate.main.arn
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
timeouts {
create = "5m"
}
}
# Automatic DNS validation
resource "aws_route53_record" "cert_validation" {
for_each = {
for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = var.hosted_zone_id
}
Version Control Integration:
# .github/workflows/ssl-automation.yml
name: SSL Certificate Management
on:
push:
branches: [main]
paths: ['infrastructure/ssl/**']
schedule:
- cron: '0 6 * * *' # Daily certificate health check
jobs:
certificate-management:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_CERTIFICATE_ROLE }}
aws-region: us-east-1
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.0
- name: Terraform Init
run: terraform init
working-directory: infrastructure/ssl
- name: Terraform Plan
run: terraform plan -out=tfplan
working-directory: infrastructure/ssl
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply tfplan
working-directory: infrastructure/ssl
- name: Certificate Health Check
run: |
python scripts/certificate-health-check.py \
--environment production \
--alert-threshold 30
CI/CD Pipeline Integration
GitHub Actions SSL Automation
Complete Pipeline Example:
# .github/workflows/ssl-cicd.yml
name: SSL Certificate CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
TERRAFORM_VERSION: '1.6.0'
KUBECTL_VERSION: '1.28.0'
jobs:
validate-certificates:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Validate Certificate Configurations
run: |
# Validate Terraform configurations
terraform fmt -check infrastructure/ssl/
terraform validate infrastructure/ssl/
# Validate Kubernetes manifests
kubectl --dry-run=client apply -f k8s/certificates/
# Validate certificate templates
python scripts/validate-cert-templates.py
deploy-staging:
needs: validate-certificates
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy to Staging
run: |
# Deploy certificate infrastructure
terraform apply -auto-approve \
-var="environment=staging" \
infrastructure/ssl/
# Update Kubernetes certificates
kubectl apply -f k8s/certificates/staging/
# Verify certificate deployment
scripts/verify-ssl-deployment.sh staging
deploy-production:
needs: validate-certificates
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to Production
run: |
# Blue-green certificate deployment
scripts/blue-green-cert-deploy.sh production
# Verify certificate health
scripts/comprehensive-ssl-test.sh production
# Update monitoring and alerting
scripts/update-ssl-monitoring.sh production
certificate-monitoring:
needs: [deploy-staging, deploy-production]
if: always()
runs-on: ubuntu-latest
steps:
- name: Update Certificate Inventory
run: |
python scripts/update-cert-inventory.py
python scripts/generate-ssl-report.py
- name: Send Slack Notification
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'SSL Certificate deployment completed'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Advanced Pipeline Scripts:
#!/bin/bash
# scripts/blue-green-cert-deploy.sh
ENVIRONMENT="$1"
NAMESPACE="ssl-system"
echo "Starting blue-green certificate deployment for $ENVIRONMENT"
# Get current active environment
CURRENT_ACTIVE=$(kubectl get service ssl-router -o jsonpath='{.spec.selector.version}')
NEW_VERSION="green"
if [ "$CURRENT_ACTIVE" = "green" ]; then
NEW_VERSION="blue"
fi
echo "Current active: $CURRENT_ACTIVE, Deploying to: $NEW_VERSION"
# Deploy new certificates to inactive environment
kubectl apply -f k8s/certificates/$ENVIRONMENT-$NEW_VERSION.yaml
# Wait for certificate readiness
kubectl wait --for=condition=Ready certificate \
--selector="environment=$ENVIRONMENT,version=$NEW_VERSION" \
--timeout=300s \
--namespace=$NAMESPACE
# Test new certificates
echo "Testing new certificate deployment..."
if scripts/test-ssl-endpoints.sh $ENVIRONMENT-$NEW_VERSION; then
echo "Certificate tests passed. Switching traffic..."
# Switch traffic to new version
kubectl patch service ssl-router \
-p '{"spec":{"selector":{"version":"'$NEW_VERSION'"}}}'
# Verify traffic switch
sleep 30
if scripts/verify-live-traffic.sh $ENVIRONMENT; then
echo "Traffic switch successful. Cleaning up old certificates..."
kubectl delete -f k8s/certificates/$ENVIRONMENT-$CURRENT_ACTIVE.yaml
echo "Blue-green deployment completed successfully"
else
echo "Traffic verification failed. Rolling back..."
kubectl patch service ssl-router \
-p '{"spec":{"selector":{"version":"'$CURRENT_ACTIVE'"}}}'
exit 1
fi
else
echo "Certificate tests failed. Aborting deployment."
kubectl delete -f k8s/certificates/$ENVIRONMENT-$NEW_VERSION.yaml
exit 1
fi
GitLab CI/CD Integration
GitLab Pipeline Configuration:
# .gitlab-ci.yml
stages:
- validate
- build
- deploy-staging
- test-staging
- deploy-production
- monitor
variables:
DOCKER_DRIVER: overlay2
TERRAFORM_VERSION: '1.6.0'
.terraform-base: &terraform-base
image: hashicorp/terraform:$TERRAFORM_VERSION
before_script:
- terraform --version
- terraform init
validate-ssl-config:
<<: *terraform-base
stage: validate
script:
- terraform validate infrastructure/ssl/
- terraform fmt -check infrastructure/ssl/
- scripts/validate-certificate-policies.py
only:
changes:
- infrastructure/ssl/**/*
- k8s/certificates/**/*
build-certificate-images:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $CI_REGISTRY_IMAGE/cert-manager:$CI_COMMIT_SHA docker/cert-manager/
- docker push $CI_REGISTRY_IMAGE/cert-manager:$CI_COMMIT_SHA
only:
changes:
- docker/cert-manager/**/*
deploy-staging-ssl:
<<: *terraform-base
stage: deploy-staging
environment:
name: staging
script:
- terraform plan -var="environment=staging" -out=staging.tfplan infrastructure/ssl/
- terraform apply staging.tfplan
- kubectl apply -f k8s/certificates/staging/
artifacts:
reports:
terraform: infrastructure/ssl/staging.tfplan
only:
- develop
test-staging-certificates:
stage: test-staging
image: alpine/curl
script:
- apk add --no-cache openssl
- scripts/comprehensive-ssl-test.sh staging
- scripts/performance-test-ssl.sh staging
dependencies:
- deploy-staging-ssl
only:
- develop
deploy-production-ssl:
<<: *terraform-base
stage: deploy-production
environment:
name: production
script:
- terraform plan -var="environment=production" -out=production.tfplan infrastructure/ssl/
- terraform apply production.tfplan
- scripts/zero-downtime-cert-deploy.sh production
when: manual
only:
- main
monitor-ssl-health:
stage: monitor
image: python:3.11-alpine
script:
- pip install requests boto3 kubernetes
- python scripts/ssl-health-monitor.py --environment production
- python scripts/update-ssl-metrics.py
artifacts:
reports:
junit: ssl-test-results.xml
only:
- main
- schedules
Kubernetes Certificate Management
Cert-Manager Advanced Configuration
Complete Cert-Manager Setup:
# k8s/cert-manager/installation.yaml
apiVersion: v1
kind: Namespace
metadata:
name: cert-manager
---
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
name: cert-manager
namespace: kube-system
spec:
chart: cert-manager
repo: https://charts.jetstack.io
targetNamespace: cert-manager
set:
installCRDs: 'true'
prometheus.enabled: 'true'
webhook.timeoutSeconds: '30'
featureGates: 'ExperimentalCertificateSigningRequestControllers=true'
---
# Production Let's Encrypt ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ssl-admin@yourdomain.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- dns01:
route53:
region: us-east-1
hostedZoneID: Z1234567890
accessKeyID: AKIAIOSFODNN7EXAMPLE
secretAccessKeySecretRef:
name: route53-credentials
key: secret-access-key
- http01:
ingress:
class: nginx
podTemplate:
spec:
nodeSelector:
'kubernetes.io/os': linux
---
# Staging Let's Encrypt ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: ssl-admin@yourdomain.com
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- dns01:
route53:
region: us-east-1
hostedZoneID: Z1234567890
accessKeyID: AKIAIOSFODNN7EXAMPLE
secretAccessKeySecretRef:
name: route53-credentials
key: secret-access-key
---
# Commercial CA ClusterIssuer (DigiCert example)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: digicert-issuer
spec:
acme:
server: https://acme.digicert.com/v2/acme/directory
email: ssl-admin@yourdomain.com
privateKeySecretRef:
name: digicert-prod
solvers:
- dns01:
route53:
region: us-east-1
hostedZoneID: Z1234567890
Application Certificate Templates:
# k8s/certificates/web-app-certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: web-app-tls
namespace: production
labels:
app: web-app
environment: production
spec:
secretName: web-app-tls-secret
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
group: cert-manager.io
commonName: app.yourdomain.com
dnsNames:
- app.yourdomain.com
- www.app.yourdomain.com
- api.yourdomain.com
duration: 2160h # 90 days
renewBefore: 360h # 15 days before expiry
subject:
organizations:
- Your Organization
countries:
- US
organizationalUnits:
- IT Department
privateKey:
algorithm: RSA
encoding: PKCS1
size: 2048
rotationPolicy: Always
usages:
- digital signature
- key encipherment
- server auth
---
# Wildcard certificate for subdomains
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-yourdomain-com
namespace: kube-system
spec:
secretName: wildcard-yourdomain-com-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
commonName: '*.yourdomain.com'
dnsNames:
- '*.yourdomain.com'
- yourdomain.com
duration: 2160h
renewBefore: 720h # 30 days before expiry
privateKey:
algorithm: RSA
size: 2048
rotationPolicy: Always
Advanced Certificate Policies:
# k8s/certificates/certificate-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: ssl-certificate-policy
spec:
validationFailureAction: enforce
background: true
rules:
- name: require-ssl-certificates
match:
any:
- resources:
kinds:
- Ingress
validate:
message: 'Ingress must have TLS configuration'
pattern:
spec:
tls:
- hosts:
- '?*'
secretName: '?*'
- name: enforce-certificate-annotations
match:
any:
- resources:
kinds:
- Certificate
validate:
message: 'Certificate must have required annotations'
pattern:
metadata:
annotations:
cert-manager.io/issuer: '?*'
acme.cert-manager.io/http01-edit-in-place: 'true'
- name: minimum-certificate-duration
match:
any:
- resources:
kinds:
- Certificate
validate:
message: 'Certificate duration must be at least 720h (30 days)'
pattern:
spec:
duration: '>=720h'
- name: enforce-renewal-before
match:
any:
- resources:
kinds:
- Certificate
validate:
message: 'Certificate must specify renewBefore'
pattern:
spec:
renewBefore: '?*'
Container SSL Automation
Docker Multi-Stage Certificate Build:
# Dockerfile.ssl-automation
FROM golang:1.21-alpine AS cert-builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o ssl-automation ./cmd/ssl-automation
FROM alpine:latest AS cert-runtime
# Install certificate tools
RUN apk --no-cache add \
ca-certificates \
openssl \
curl \
jq
# Install certbot for Let's Encrypt
RUN apk add --no-cache \
certbot \
certbot-dns-route53 \
certbot-dns-cloudflare
# Copy automation binary
COPY --from=cert-builder /app/ssl-automation /usr/local/bin/
# Copy configuration templates
COPY configs/ /etc/ssl-automation/
COPY scripts/ /usr/local/bin/
# Create non-root user
RUN addgroup -g 1001 ssluser && \
adduser -D -s /bin/sh -u 1001 -G ssluser ssluser
# Set up directories
RUN mkdir -p /var/lib/ssl-automation /var/log/ssl-automation && \
chown -R ssluser:ssluser /var/lib/ssl-automation /var/log/ssl-automation
USER ssluser
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
EXPOSE 8080
CMD ["ssl-automation", "serve", "--config", "/etc/ssl-automation/config.yaml"]
Docker Compose for SSL Automation:
# docker-compose.ssl.yml
version: '3.8'
services:
ssl-automation:
build:
context: .
dockerfile: Dockerfile.ssl-automation
environment:
- SSL_AUTOMATION_MODE=production
- LOG_LEVEL=info
- METRICS_ENABLED=true
volumes:
- ssl_certificates:/var/lib/ssl-automation/certificates
- ssl_logs:/var/log/ssl-automation
- ./configs:/etc/ssl-automation:ro
networks:
- ssl_network
depends_on:
- redis
- postgres
deploy:
replicas: 2
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
resources:
limits:
cpus: '0.5'
memory: 512M
nginx-ssl-proxy:
image: nginx:alpine
ports:
- '80:80'
- '443:443'
volumes:
- ssl_certificates:/etc/ssl/certificates:ro
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl.conf:/etc/nginx/conf.d/ssl.conf:ro
networks:
- ssl_network
depends_on:
- ssl-automation
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
networks:
- ssl_network
command: redis-server --appendonly yes
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: ssl_automation
POSTGRES_USER: ssl_user
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- ssl_network
prometheus:
image: prom/prometheus:latest
ports:
- '9090:9090'
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
networks:
- ssl_network
grafana:
image: grafana/grafana:latest
ports:
- '3000:3000'
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
networks:
- ssl_network
volumes:
ssl_certificates:
ssl_logs:
redis_data:
postgres_data:
grafana_data:
networks:
ssl_network:
driver: bridge
Infrastructure as Code (IaC) SSL Management
Terraform SSL Automation
Comprehensive Terraform Module:
# modules/ssl-automation/main.tf
terraform {
required_version = ">= 1.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.0"
}
helm = {
source = "hashicorp/helm"
version = "~> 2.0"
}
}
}
# Certificate Authority configuration
resource "aws_acmpca_certificate_authority" "main" {
count = var.create_private_ca ? 1 : 0
certificate_authority_configuration {
key_algorithm = var.ca_key_algorithm
signing_algorithm = var.ca_signing_algorithm
subject {
common_name = var.ca_common_name
country = var.ca_country
locality = var.ca_locality
organization = var.ca_organization
organizational_unit = var.ca_organizational_unit
state = var.ca_state
}
}
permanent_deletion_time_in_days = var.ca_deletion_days
type = "ROOT"
tags = merge(var.common_tags, {
Name = "${var.project_name}-root-ca"
Component = "certificate-authority"
})
}
# Public certificates via ACM
resource "aws_acm_certificate" "public_certificates" {
for_each = var.public_certificates
domain_name = each.value.domain_name
subject_alternative_names = each.value.subject_alternative_names
validation_method = each.value.validation_method
options {
certificate_transparency_logging_preference = each.value.ct_logging_preference
}
tags = merge(var.common_tags, {
Name = "${var.project_name}-${each.key}"
Domain = each.value.domain_name
Environment = var.environment
})
lifecycle {
create_before_destroy = true
}
}
# DNS validation for public certificates
resource "aws_route53_record" "certificate_validation" {
for_each = {
for cert_key, cert in var.public_certificates : cert_key => {
for dvo in aws_acm_certificate.public_certificates[cert_key].domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
} if cert.validation_method == "DNS"
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = var.route53_zone_id
}
# Certificate validation
resource "aws_acm_certificate_validation" "public_certificates" {
for_each = {
for cert_key, cert in var.public_certificates : cert_key => cert
if cert.validation_method == "DNS"
}
certificate_arn = aws_acm_certificate.public_certificates[each.key].arn
validation_record_fqdns = [
for record in aws_route53_record.certificate_validation[each.key] : record.fqdn
]
timeouts {
create = "5m"
}
}
# Application Load Balancer with SSL
resource "aws_lb" "ssl_load_balancer" {
count = var.create_load_balancer ? 1 : 0
name = "${var.project_name}-ssl-alb"
internal = var.load_balancer_internal
load_balancer_type = "application"
security_groups = var.load_balancer_security_groups
subnets = var.load_balancer_subnets
enable_deletion_protection = var.load_balancer_deletion_protection
tags = merge(var.common_tags, {
Name = "${var.project_name}-ssl-alb"
})
}
# HTTPS listener with certificate
resource "aws_lb_listener" "https" {
count = var.create_load_balancer ? 1 : 0
load_balancer_arn = aws_lb.ssl_load_balancer[0].arn
port = "443"
protocol = "HTTPS"
ssl_policy = var.ssl_policy
certificate_arn = aws_acm_certificate_validation.public_certificates[var.primary_certificate_key].certificate_arn
default_action {
type = "forward"
target_group_arn = var.default_target_group_arn
}
}
# Additional certificate attachments
resource "aws_lb_listener_certificate" "additional_certificates" {
for_each = {
for cert_key, cert in var.public_certificates : cert_key => cert
if cert_key != var.primary_certificate_key && var.create_load_balancer
}
listener_arn = aws_lb_listener.https[0].arn
certificate_arn = aws_acm_certificate_validation.public_certificates[each.key].certificate_arn
}
# CloudWatch monitoring for certificates
resource "aws_cloudwatch_metric_alarm" "certificate_expiry" {
for_each = var.public_certificates
alarm_name = "${var.project_name}-${each.key}-cert-expiry"
comparison_operator = "LessThanThreshold"
evaluation_periods = "1"
metric_name = "DaysToExpiry"
namespace = "AWS/CertificateManager"
period = "86400"
statistic = "Average"
threshold = var.certificate_expiry_threshold
alarm_description = "Certificate ${each.value.domain_name} is expiring soon"
alarm_actions = var.sns_alarm_arns
dimensions = {
CertificateArn = aws_acm_certificate.public_certificates[each.key].arn
}
tags = var.common_tags
}
Terraform Variables:
# modules/ssl-automation/variables.tf
variable "project_name" {
description = "Name of the project"
type = string
}
variable "environment" {
description = "Environment (dev, staging, prod)"
type = string
}
variable "common_tags" {
description = "Common tags to apply to all resources"
type = map(string)
default = {}
}
variable "public_certificates" {
description = "Configuration for public certificates"
type = map(object({
domain_name = string
subject_alternative_names = list(string)
validation_method = string
ct_logging_preference = string
}))
default = {}
}
variable "create_private_ca" {
description = "Whether to create a private certificate authority"
type = bool
default = false
}
variable "ca_key_algorithm" {
description = "Key algorithm for private CA"
type = string
default = "RSA_2048"
}
variable "ca_signing_algorithm" {
description = "Signing algorithm for private CA"
type = string
default = "SHA256WITHRSA"
}
variable "certificate_expiry_threshold" {
description = "Days before expiry to trigger alarm"
type = number
default = 30
}
variable "route53_zone_id" {
description = "Route53 hosted zone ID for DNS validation"
type = string
}
variable "sns_alarm_arns" {
description = "SNS topic ARNs for certificate expiry alarms"
type = list(string)
default = []
}
# Load balancer variables
variable "create_load_balancer" {
description = "Whether to create an application load balancer"
type = bool
default = false
}
variable "ssl_policy" {
description = "SSL policy for the load balancer"
type = string
default = "ELBSecurityPolicy-TLS-1-2-2017-01"
}
variable "primary_certificate_key" {
description = "Key of the primary certificate to use"
type = string
}
Usage Example:
# environments/production/main.tf
module "ssl_automation" {
source = "../../modules/ssl-automation"
project_name = "myapp"
environment = "production"
common_tags = {
Project = "MyApp"
Environment = "Production"
ManagedBy = "Terraform"
Team = "DevOps"
}
public_certificates = {
primary = {
domain_name = "myapp.com"
subject_alternative_names = ["www.myapp.com", "api.myapp.com"]
validation_method = "DNS"
ct_logging_preference = "ENABLED"
}
wildcard = {
domain_name = "*.myapp.com"
subject_alternative_names = ["myapp.com"]
validation_method = "DNS"
ct_logging_preference = "ENABLED"
}
}
create_load_balancer = true
primary_certificate_key = "primary"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-Ext-2018-06"
route53_zone_id = "Z1234567890123"
certificate_expiry_threshold = 30
sns_alarm_arns = [aws_sns_topic.ssl_alerts.arn]
}
# SNS topic for SSL alerts
resource "aws_sns_topic" "ssl_alerts" {
name = "ssl-certificate-alerts"
tags = {
Environment = "production"
Purpose = "ssl-monitoring"
}
}
# SNS subscription for email alerts
resource "aws_sns_topic_subscription" "ssl_email_alerts" {
topic_arn = aws_sns_topic.ssl_alerts.arn
protocol = "email"
endpoint = "ssl-alerts@myapp.com"
}
Enterprise Security Automation
Certificate Lifecycle Management
Comprehensive Lifecycle Script:
#!/usr/bin/env python3
# scripts/enterprise-cert-lifecycle.py
import boto3
import kubernetes
import logging
import yaml
import json
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List, Dict, Optional
@dataclass
class CertificateInfo:
name: str
domain: str
issuer: str
expiry_date: datetime
days_to_expiry: int
environment: str
certificate_arn: Optional[str] = None
class EnterpriseCertificateManager:
def __init__(self, config_file: str):
with open(config_file, 'r') as f:
self.config = yaml.safe_load(f)
self.aws_client = boto3.client('acm')
self.k8s_client = kubernetes.client.ApiClient()
self.setup_logging()
def setup_logging(self):
logging.basicConfig(
level=getattr(logging, self.config['logging']['level']),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(self.config['logging']['file']),
logging.StreamHandler()
]
)
self.logger = logging.getLogger(__name__)
def discover_certificates(self) -> List[CertificateInfo]:
"""Discover all certificates across AWS and Kubernetes"""
certificates = []
# Discover AWS ACM certificates
certificates.extend(self._discover_aws_certificates())
# Discover Kubernetes certificates
certificates.extend(self._discover_k8s_certificates())
return certificates
def _discover_aws_certificates(self) -> List[CertificateInfo]:
"""Discover AWS ACM certificates"""
certificates = []
try:
paginator = self.aws_client.get_paginator('list_certificates')
for page in paginator.paginate():
for cert in page['CertificateSummaryList']:
cert_details = self.aws_client.describe_certificate(
CertificateArn=cert['CertificateArn']
)
cert_info = cert_details['Certificate']
expiry_date = cert_info['NotAfter']
days_to_expiry = (expiry_date - datetime.now(expiry_date.tzinfo)).days
certificates.append(CertificateInfo(
name=cert_info['DomainName'],
domain=cert_info['DomainName'],
issuer=cert_info['Issuer'],
expiry_date=expiry_date,
days_to_expiry=days_to_expiry,
environment=self._extract_environment_from_tags(cert_info.get('Tags', [])),
certificate_arn=cert['CertificateArn']
))
except Exception as e:
self.logger.error(f"Error discovering AWS certificates: {e}")
return certificates
def _discover_k8s_certificates(self) -> List[CertificateInfo]:
"""Discover Kubernetes cert-manager certificates"""
certificates = []
try:
# Use kubectl to get certificate resources
import subprocess
result = subprocess.run([
'kubectl', 'get', 'certificates', '--all-namespaces', '-o', 'json'
], capture_output=True, text=True)
if result.returncode == 0:
cert_data = json.loads(result.stdout)
for cert in cert_data['items']:
# Parse certificate information
metadata = cert['metadata']
spec = cert['spec']
status = cert.get('status', {})
if 'notAfter' in status:
expiry_date = datetime.fromisoformat(
status['notAfter'].replace('Z', '+00:00')
)
days_to_expiry = (expiry_date - datetime.now(expiry_date.tzinfo)).days
certificates.append(CertificateInfo(
name=metadata['name'],
domain=spec['commonName'],
issuer=spec['issuerRef']['name'],
expiry_date=expiry_date,
days_to_expiry=days_to_expiry,
environment=metadata.get('namespace', 'default')
))
except Exception as e:
self.logger.error(f"Error discovering Kubernetes certificates: {e}")
return certificates
def check_certificate_health(self, certificates: List[CertificateInfo]) -> Dict:
"""Perform comprehensive certificate health checks"""
health_report = {
'total_certificates': len(certificates),
'expiring_soon': [],
'expired': [],
'healthy': [],
'validation_errors': []
}
for cert in certificates:
if cert.days_to_expiry < 0:
health_report['expired'].append(cert)
elif cert.days_to_expiry <= self.config['alerting']['critical_threshold']:
health_report['expiring_soon'].append(cert)
else:
health_report['healthy'].append(cert)
# Perform additional validation
validation_result = self._validate_certificate(cert)
if not validation_result['valid']:
health_report['validation_errors'].append({
'certificate': cert,
'errors': validation_result['errors']
})
return health_report
def _validate_certificate(self, cert: CertificateInfo) -> Dict:
"""Validate certificate configuration and health"""
validation_result = {'valid': True, 'errors': []}
try:
# Check if certificate is accessible
import ssl
import socket
context = ssl.create_default_context()
with socket.create_connection((cert.domain, 443), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=cert.domain) as ssock:
cert_der = ssock.getpeercert_chain()[0]
# Additional certificate validation logic here
except Exception as e:
validation_result['valid'] = False
validation_result['errors'].append(f"Connection error: {e}")
return validation_result
def auto_renew_certificates(self, certificates: List[CertificateInfo]) -> Dict:
"""Automatically renew certificates that are expiring"""
renewal_results = {
'successful': [],
'failed': [],
'skipped': []
}
for cert in certificates:
if cert.days_to_expiry <= self.config['renewal']['auto_renew_threshold']:
try:
if cert.certificate_arn: # AWS certificate
renewal_result = self._renew_aws_certificate(cert)
else: # Kubernetes certificate
renewal_result = self._renew_k8s_certificate(cert)
if renewal_result['success']:
renewal_results['successful'].append(cert)
self.logger.info(f"Successfully renewed certificate: {cert.name}")
else:
renewal_results['failed'].append({
'certificate': cert,
'error': renewal_result['error']
})
self.logger.error(f"Failed to renew certificate {cert.name}: {renewal_result['error']}")
except Exception as e:
renewal_results['failed'].append({
'certificate': cert,
'error': str(e)
})
self.logger.error(f"Exception during renewal of {cert.name}: {e}")
else:
renewal_results['skipped'].append(cert)
return renewal_results
def _renew_k8s_certificate(self, cert: CertificateInfo) -> Dict:
"""Renew Kubernetes certificate via cert-manager"""
try:
# Force renewal by annotating the certificate
import subprocess
result = subprocess.run([
'kubectl', 'annotate', 'certificate', cert.name,
'cert-manager.io/issue-temporary-certificate=true',
'--overwrite',
'-n', cert.environment
], capture_output=True, text=True)
if result.returncode == 0:
return {'success': True}
else:
return {'success': False, 'error': result.stderr}
except Exception as e:
return {'success': False, 'error': str(e)}
def generate_compliance_report(self, certificates: List[CertificateInfo]) -> Dict:
"""Generate compliance report for audit purposes"""
report = {
'generated_at': datetime.now().isoformat(),
'total_certificates': len(certificates),
'by_environment': {},
'by_issuer': {},
'compliance_status': {},
'recommendations': []
}
# Group by environment
for cert in certificates:
env = cert.environment
if env not in report['by_environment']:
report['by_environment'][env] = []
report['by_environment'][env].append({
'domain': cert.domain,
'expiry_date': cert.expiry_date.isoformat(),
'days_to_expiry': cert.days_to_expiry
})
# Group by issuer
for cert in certificates:
issuer = cert.issuer
if issuer not in report['by_issuer']:
report['by_issuer'][issuer] = 0
report['by_issuer'][issuer] += 1
# Compliance checks
report['compliance_status'] = self._check_compliance_rules(certificates)
# Generate recommendations
report['recommendations'] = self._generate_recommendations(certificates)
return report
def _check_compliance_rules(self, certificates: List[CertificateInfo]) -> Dict:
"""Check certificates against compliance rules"""
compliance = {
'total_checked': len(certificates),
'compliant': 0,
'non_compliant': 0,
'violations': []
}
for cert in certificates:
violations = []
# Check minimum validity period
if cert.days_to_expiry < self.config['compliance']['minimum_validity_days']:
violations.append(f"Certificate expires in {cert.days_to_expiry} days (minimum: {self.config['compliance']['minimum_validity_days']})")
# Check approved issuers
if cert.issuer not in self.config['compliance']['approved_issuers']:
violations.append(f"Certificate issued by non-approved CA: {cert.issuer}")
if violations:
compliance['non_compliant'] += 1
compliance['violations'].append({
'certificate': cert.domain,
'violations': violations
})
else:
compliance['compliant'] += 1
return compliance
def main():
"""Main execution function"""
cert_manager = EnterpriseCertificateManager('config/cert-lifecycle.yaml')
# Discover all certificates
certificates = cert_manager.discover_certificates()
cert_manager.logger.info(f"Discovered {len(certificates)} certificates")
# Perform health check
health_report = cert_manager.check_certificate_health(certificates)
cert_manager.logger.info(f"Health check complete: {len(health_report['healthy'])} healthy, {len(health_report['expiring_soon'])} expiring soon")
# Auto-renew certificates
renewal_results = cert_manager.auto_renew_certificates(health_report['expiring_soon'])
cert_manager.logger.info(f"Renewal complete: {len(renewal_results['successful'])} successful, {len(renewal_results['failed'])} failed")
# Generate compliance report
compliance_report = cert_manager.generate_compliance_report(certificates)
# Save reports
with open('reports/certificate-health.json', 'w') as f:
json.dump(health_report, f, indent=2, default=str)
with open('reports/compliance-report.json', 'w') as f:
json.dump(compliance_report, f, indent=2, default=str)
cert_manager.logger.info("Certificate lifecycle management completed")
if __name__ == "__main__":
main()
Monitoring and Alerting Integration
Prometheus SSL Monitoring
Custom SSL Exporter:
# monitoring/ssl-exporter.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ssl-exporter
namespace: monitoring
spec:
replicas: 2
selector:
matchLabels:
app: ssl-exporter
template:
metadata:
labels:
app: ssl-exporter
spec:
containers:
- name: ssl-exporter
image: ribbybibby/ssl-exporter:latest
ports:
- containerPort: 9219
args:
- --web.listen-address=:9219
- --config.file=/etc/ssl-exporter/config.yaml
volumeMounts:
- name: config
mountPath: /etc/ssl-exporter
resources:
requests:
memory: '64Mi'
cpu: '250m'
limits:
memory: '128Mi'
cpu: '500m'
volumes:
- name: config
configMap:
name: ssl-exporter-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ssl-exporter-config
namespace: monitoring
data:
config.yaml: |
modules:
tcp_connect:
prober: tcp
timeout: 10s
tcp:
tls: true
tls_config:
insecure_skip_verify: false
https_2xx:
prober: http
timeout: 10s
http:
preferred_ip_protocol: "ip4"
method: GET
follow_redirects: true
fail_if_ssl: false
fail_if_not_ssl: true
tls_config:
insecure_skip_verify: false
---
apiVersion: v1
kind: Service
metadata:
name: ssl-exporter
namespace: monitoring
labels:
app: ssl-exporter
spec:
ports:
- port: 9219
targetPort: 9219
name: metrics
selector:
app: ssl-exporter
Prometheus AlertManager Rules:
# monitoring/ssl-alerts.yaml
groups:
- name: ssl-certificate-alerts
rules:
- alert: SSLCertificateExpiringSoon
expr: ssl_cert_not_after - time() < 7 * 24 * 3600
for: 1h
labels:
severity: warning
service: ssl-monitoring
annotations:
summary: 'SSL certificate expiring soon'
description: 'SSL certificate for {{ $labels.instance }} expires in less than 7 days'
- alert: SSLCertificateExpired
expr: ssl_cert_not_after - time() <= 0
for: 0m
labels:
severity: critical
service: ssl-monitoring
annotations:
summary: 'SSL certificate expired'
description: 'SSL certificate for {{ $labels.instance }} has expired'
- alert: SSLCertificateInvalid
expr: ssl_cert_verify_error == 1
for: 5m
labels:
severity: critical
service: ssl-monitoring
annotations:
summary: 'SSL certificate validation failed'
description: 'SSL certificate for {{ $labels.instance }} failed validation'
- alert: SSLHandshakeFailed
expr: ssl_tls_connect_success == 0
for: 5m
labels:
severity: critical
service: ssl-monitoring
annotations:
summary: 'SSL/TLS handshake failed'
description: 'SSL/TLS handshake failed for {{ $labels.instance }}'
- alert: SSLCertificateRenewalFailed
expr: increase(certmanager_certificate_renewal_failures_total[1h]) > 0
for: 0m
labels:
severity: critical
service: cert-manager
annotations:
summary: 'SSL certificate renewal failed'
description: 'Certificate renewal failed for {{ $labels.name }} in namespace {{ $labels.namespace }}'
Conclusion
SSL certificate automation in DevOps environments requires comprehensive integration across CI/CD pipelines, Infrastructure as Code, container orchestration, and monitoring systems. By implementing these advanced automation strategies, organizations can achieve zero-touch certificate management that scales with their infrastructure while maintaining the highest security standards.
Key Implementation Strategies:
- Pipeline Integration: Embed certificate management into CI/CD workflows
- Infrastructure as Code: Version control and automate certificate infrastructure
- Container Orchestration: Leverage Kubernetes cert-manager for dynamic environments
- Enterprise Policies: Implement governance and compliance through automation
- Comprehensive Monitoring: Track certificate health across all environments
Action Steps:
- Assess current certificate management processes and identify automation opportunities
- Implement Infrastructure as Code for certificate provisioning and management
- Integrate certificate automation into CI/CD pipelines with proper testing
- Deploy comprehensive monitoring and alerting for certificate health
- Establish enterprise policies and compliance tracking through automation
Related Articles
- SSL Certificate Installation Guide
- SSL Certificate Monitoring Strategies
- SSL Certificate Renewal Guide
- SSL/TLS Security Best Practices
Ready to Automate Your SSL Certificate Management? Our enterprise SSL monitoring platform provides comprehensive DevOps integration, automated renewal tracking, and compliance reporting to ensure your certificates are always secure and up-to-date across all environments.