SSL Certificate Automation in DevOps: Complete CI/CD Integration Guide

Master SSL certificate automation in DevOps environments with CI/CD integration, Infrastructure as Code, container orchestration, and enterprise security practices.

By DevOps Security Team Updated May 23, 2025 18 min read
Advanced

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:

  1. Assess current certificate management processes and identify automation opportunities
  2. Implement Infrastructure as Code for certificate provisioning and management
  3. Integrate certificate automation into CI/CD pipelines with proper testing
  4. Deploy comprehensive monitoring and alerting for certificate health
  5. Establish enterprise policies and compliance tracking through automation

Related Articles


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.