Docs / Automation & IaC / Automate SSL Certificate Deployment Across Servers

Automate SSL Certificate Deployment Across Servers

By Admin · Mar 15, 2026 · Updated Apr 24, 2026 · 255 views · 4 min read

Managing SSL certificates across multiple servers is tedious and error-prone. A missed renewal can cause outages. This guide covers automating the entire SSL lifecycle — from issuance with Let's Encrypt to distribution across your fleet — using Ansible, acme.sh, or centralized certificate management.

Architecture Options

  • Centralized issuance: One server obtains certs, distributes to all servers
  • Per-server issuance: Each server runs its own certbot/acme.sh
  • DNS-based validation: Works for servers behind firewalls or load balancers

Centralized SSL with Ansible

# playbooks/ssl-deploy.yml
---
- name: Issue and Deploy SSL Certificates
  hosts: cert_manager
  become: yes
  vars:
    domains:
      - name: example.com
        san: ["www.example.com", "api.example.com"]
        targets: ["web1", "web2"]
      - name: app.example.com
        san: []
        targets: ["app1", "app2", "app3"]

  tasks:
    - name: Install acme.sh
      shell: curl https://get.acme.sh | sh
      args:
        creates: /root/.acme.sh/acme.sh

    - name: Issue certificates via DNS
      shell: |
        /root/.acme.sh/acme.sh --issue \
          -d {{ item.name }} \
          {% for san in item.san %}-d {{ san }} {% endfor %}\
          --dns dns_cf \
          --keylength ec-256
      environment:
        CF_Token: "{{ vault_cloudflare_token }}"
        CF_Zone_ID: "{{ vault_cloudflare_zone_id }}"
      loop: "{{ domains }}"
      register: cert_results
      changed_when: "'Cert success' in cert_results.stdout"
      failed_when: false

- name: Distribute Certificates
  hosts: webservers
  become: yes
  vars:
    cert_source: "/root/.acme.sh"

  tasks:
    - name: Create SSL directory
      file:
        path: /etc/ssl/custom
        state: directory
        mode: '0700'

    - name: Copy certificate files
      copy:
        src: "{{ cert_source }}/{{ domain }}_ecc/{{ item.src }}"
        dest: "/etc/ssl/custom/{{ item.dest }}"
        mode: "{{ item.mode }}"
      loop:
        - { src: "fullchain.cer", dest: "{{ domain }}.crt", mode: "0644" }
        - { src: "{{ domain }}.key", dest: "{{ domain }}.key", mode: "0600" }
      notify: reload nginx
      delegate_to: cert_manager

  handlers:
    - name: reload nginx
      systemd:
        name: nginx
        state: reloaded

Automated Renewal Script

#!/bin/bash
# /opt/scripts/ssl-renew-and-deploy.sh
set -euo pipefail

LOG="/var/log/ssl-renewal.log"
CERT_DIR="/root/.acme.sh"
SERVERS=("web1.example.com" "web2.example.com" "app1.example.com")
DOMAINS=("example.com" "app.example.com")

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG"; }

for domain in "${DOMAINS[@]}"; do
    log "Renewing certificate for $domain"

    if $CERT_DIR/acme.sh --renew -d "$domain" --ecc 2>&1 | tee -a "$LOG"; then
        log "Certificate renewed for $domain — deploying"

        for server in "${SERVERS[@]}"; do
            log "  Deploying to $server"
            scp -q "$CERT_DIR/${domain}_ecc/fullchain.cer" "$server:/etc/ssl/custom/${domain}.crt"
            scp -q "$CERT_DIR/${domain}_ecc/${domain}.key" "$server:/etc/ssl/custom/${domain}.key"
            ssh "$server" "nginx -t && systemctl reload nginx" 2>&1 | tee -a "$LOG"

            if [ $? -eq 0 ]; then
                log "  Deployed to $server successfully"
            else
                log "  ERROR: Failed to deploy to $server"
            fi
        done
    else
        log "Certificate for $domain not due for renewal (or error occurred)"
    fi
done

log "SSL renewal run complete"

# Crontab: daily at 2 AM
# 0 2 * * * /opt/scripts/ssl-renew-and-deploy.sh

Wildcard Certificates with DNS Validation

# Issue wildcard certificate
/root/.acme.sh/acme.sh --issue \
  -d "example.com" \
  -d "*.example.com" \
  --dns dns_cf \
  --keylength ec-256

# This single cert covers:
# example.com, www.example.com, api.example.com, *.example.com
# Deploy to all servers — one cert covers everything

Certificate Monitoring

#!/bin/bash
# /opt/scripts/check-ssl-expiry.sh
# Alert if any certificate expires within 14 days

DOMAINS=("example.com" "api.example.com" "app.example.com")
WARN_DAYS=14

for domain in "${DOMAINS[@]}"; do
    expiry=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null | \
        openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)

    if [ -z "$expiry" ]; then
        echo "WARNING: Cannot check $domain"
        continue
    fi

    expiry_epoch=$(date -d "$expiry" +%s)
    now_epoch=$(date +%s)
    days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

    if [ "$days_left" -lt "$WARN_DAYS" ]; then
        echo "ALERT: $domain expires in $days_left days ($expiry)"
        curl -X POST "https://hooks.slack.com/services/YOUR/WEBHOOK" \
          -H 'Content-type: application/json' \
          -d "{\"text\": \"SSL ALERT: $domain expires in $days_left days\"}"
    else
        echo "OK: $domain expires in $days_left days"
    fi
done

Best Practices

  • Use DNS validation for wildcard certs and servers behind load balancers
  • Automate everything: Never rely on manual certificate renewal
  • Monitor expiry dates independently of the renewal process
  • Use ECC certificates (ec-256) for faster TLS handshakes and smaller certificates
  • Test Nginx config before reloading to prevent outages from bad certificates
  • Keep backup copies of certificates and keys in a secure, encrypted location
  • Use separate keys per environment — don't share production keys with staging

Was this article helpful?