--- # SSL Certificate Management and Renewal Playbook # Manage Let's Encrypt certificates and other SSL certificates # Usage: ansible-playbook playbooks/certificate_renewal.yml # Usage: ansible-playbook playbooks/certificate_renewal.yml -e "force_renewal=true" # Usage: ansible-playbook playbooks/certificate_renewal.yml -e "check_only=true" - name: SSL Certificate Management and Renewal hosts: "{{ host_target | default('all') }}" gather_facts: yes vars: force_renewal: "{{ force_renewal | default(false) }}" check_only: "{{ check_only | default(false) }}" renewal_threshold_days: "{{ renewal_threshold_days | default(30) }}" backup_certificates: "{{ backup_certificates | default(true) }}" restart_services: "{{ restart_services | default(true) }}" # Certificate locations and services certificate_configs: atlantis: - name: "nginx-proxy-manager" cert_path: "/volume1/docker/nginx-proxy-manager/data/letsencrypt" domains: ["*.vish.gg", "vish.gg"] service: "nginx-proxy-manager" renewal_method: "npm" # Nginx Proxy Manager handles this - name: "synology-dsm" cert_path: "/usr/syno/etc/certificate" domains: ["atlantis.vish.local"] service: "nginx" renewal_method: "synology" calypso: - name: "nginx-proxy-manager" cert_path: "/volume1/docker/nginx-proxy-manager/data/letsencrypt" domains: ["*.calypso.local"] service: "nginx-proxy-manager" renewal_method: "npm" homelab_vm: - name: "nginx" cert_path: "/etc/letsencrypt" domains: ["homelab.vish.gg"] service: "nginx" renewal_method: "certbot" - name: "traefik" cert_path: "/opt/docker/traefik/certs" domains: ["*.homelab.vish.gg"] service: "traefik" renewal_method: "traefik" tasks: - name: Create certificate report directory file: path: "/tmp/certificate_reports/{{ ansible_date_time.date }}" state: directory mode: '0755' delegate_to: localhost - name: Get current certificate configurations for this host set_fact: current_certificates: "{{ certificate_configs.get(inventory_hostname, []) }}" - name: Display certificate management plan debug: msg: | 🔒 CERTIFICATE MANAGEMENT PLAN ============================== 🖥️ Host: {{ inventory_hostname }} 📅 Date: {{ ansible_date_time.date }} 🔍 Check Only: {{ check_only }} 🔄 Force Renewal: {{ force_renewal }} 📅 Renewal Threshold: {{ renewal_threshold_days }} days 💾 Backup Certificates: {{ backup_certificates }} 📋 Certificates to manage: {{ current_certificates | length }} {% for cert in current_certificates %} - {{ cert.name }}: {{ cert.domains | join(', ') }} {% endfor %} - name: Check certificate expiration dates shell: | cert_info_file="/tmp/certificate_reports/{{ ansible_date_time.date }}/{{ inventory_hostname }}_cert_info.txt" echo "🔒 CERTIFICATE STATUS REPORT - {{ inventory_hostname }}" > "$cert_info_file" echo "=================================================" >> "$cert_info_file" echo "Date: {{ ansible_date_time.iso8601 }}" >> "$cert_info_file" echo "Renewal Threshold: {{ renewal_threshold_days }} days" >> "$cert_info_file" echo "" >> "$cert_info_file" {% for cert in current_certificates %} echo "=== {{ cert.name }} ===" >> "$cert_info_file" echo "Domains: {{ cert.domains | join(', ') }}" >> "$cert_info_file" echo "Method: {{ cert.renewal_method }}" >> "$cert_info_file" # Check certificate expiration for each domain {% for domain in cert.domains %} echo "Checking {{ domain }}..." >> "$cert_info_file" # Try different methods to check certificate if command -v openssl &> /dev/null; then # Method 1: Check via SSL connection (if accessible) cert_info=$(echo | timeout 10 openssl s_client -servername {{ domain }} -connect {{ domain }}:443 2>/dev/null | openssl x509 -noout -dates 2>/dev/null) if [ $? -eq 0 ]; then echo " SSL Connection: ✅" >> "$cert_info_file" echo " $cert_info" >> "$cert_info_file" # Calculate days until expiration not_after=$(echo "$cert_info" | grep notAfter | cut -d= -f2) if [ -n "$not_after" ]; then exp_date=$(date -d "$not_after" +%s 2>/dev/null || echo "0") current_date=$(date +%s) days_left=$(( (exp_date - current_date) / 86400 )) echo " Days until expiration: $days_left" >> "$cert_info_file" if [ $days_left -lt {{ renewal_threshold_days }} ]; then echo " Status: ⚠️ RENEWAL NEEDED" >> "$cert_info_file" else echo " Status: ✅ Valid" >> "$cert_info_file" fi fi else echo " SSL Connection: ❌ Failed" >> "$cert_info_file" fi # Method 2: Check local certificate files {% if cert.cert_path %} if [ -d "{{ cert.cert_path }}" ]; then echo " Local cert path: {{ cert.cert_path }}" >> "$cert_info_file" # Find certificate files cert_files=$(find {{ cert.cert_path }} -name "*.crt" -o -name "*.pem" -o -name "fullchain.pem" 2>/dev/null | head -5) if [ -n "$cert_files" ]; then echo " Certificate files found:" >> "$cert_info_file" for cert_file in $cert_files; do echo " $cert_file" >> "$cert_info_file" if openssl x509 -in "$cert_file" -noout -dates 2>/dev/null; then local_cert_info=$(openssl x509 -in "$cert_file" -noout -dates 2>/dev/null) echo " $local_cert_info" >> "$cert_info_file" fi done else echo " No certificate files found in {{ cert.cert_path }}" >> "$cert_info_file" fi else echo " Certificate path {{ cert.cert_path }} not found" >> "$cert_info_file" fi {% endif %} else echo " OpenSSL not available" >> "$cert_info_file" fi echo "" >> "$cert_info_file" {% endfor %} echo "" >> "$cert_info_file" {% endfor %} cat "$cert_info_file" register: certificate_status changed_when: false - name: Backup existing certificates shell: | backup_dir="/tmp/certificate_backups/{{ ansible_date_time.epoch }}" mkdir -p "$backup_dir" echo "Creating certificate backup..." {% for cert in current_certificates %} {% if cert.cert_path %} if [ -d "{{ cert.cert_path }}" ]; then echo "Backing up {{ cert.name }}..." tar -czf "$backup_dir/{{ cert.name }}_backup.tar.gz" -C "$(dirname {{ cert.cert_path }})" "$(basename {{ cert.cert_path }})" 2>/dev/null || echo "Backup failed for {{ cert.name }}" fi {% endif %} {% endfor %} echo "✅ Certificate backup created at $backup_dir" ls -la "$backup_dir" register: certificate_backup when: - backup_certificates | bool - not check_only | bool - name: Renew certificates via Certbot shell: | echo "🔄 Renewing certificates via Certbot..." {% if force_renewal %} certbot renew --force-renewal --quiet {% else %} certbot renew --quiet {% endif %} if [ $? -eq 0 ]; then echo "✅ Certbot renewal successful" else echo "❌ Certbot renewal failed" exit 1 fi register: certbot_renewal when: - not check_only | bool - current_certificates | selectattr('renewal_method', 'equalto', 'certbot') | list | length > 0 ignore_errors: yes - name: Check Nginx Proxy Manager certificates shell: | echo "🔍 Checking Nginx Proxy Manager certificates..." {% for cert in current_certificates %} {% if cert.renewal_method == 'npm' %} if [ -d "{{ cert.cert_path }}" ]; then echo "NPM certificate path exists: {{ cert.cert_path }}" # NPM manages certificates automatically, just check status find {{ cert.cert_path }} -name "*.pem" -mtime -1 | head -5 | while read cert_file; do echo "Recent certificate: $cert_file" done else echo "NPM certificate path not found: {{ cert.cert_path }}" fi {% endif %} {% endfor %} register: npm_certificate_check when: current_certificates | selectattr('renewal_method', 'equalto', 'npm') | list | length > 0 changed_when: false - name: Restart services after certificate renewal ansible.builtin.command: "docker restart {{ item.service }}" loop: "{{ current_certificates | selectattr('service', 'defined') | list }}" when: - restart_services | bool - item.service is defined register: service_restart_result failed_when: false changed_when: service_restart_result.rc == 0 - not check_only | bool - (certbot_renewal.changed | default(false)) or (force_renewal | bool) - name: Verify certificate renewal shell: | echo "🔍 Verifying certificate renewal..." verification_results=() {% for cert in current_certificates %} {% for domain in cert.domains %} echo "Verifying {{ domain }}..." if command -v openssl &> /dev/null; then # Check certificate via SSL connection cert_info=$(echo | timeout 10 openssl s_client -servername {{ domain }} -connect {{ domain }}:443 2>/dev/null | openssl x509 -noout -dates 2>/dev/null) if [ $? -eq 0 ]; then not_after=$(echo "$cert_info" | grep notAfter | cut -d= -f2) if [ -n "$not_after" ]; then exp_date=$(date -d "$not_after" +%s 2>/dev/null || echo "0") current_date=$(date +%s) days_left=$(( (exp_date - current_date) / 86400 )) if [ $days_left -gt {{ renewal_threshold_days }} ]; then echo "✅ {{ domain }}: $days_left days remaining" verification_results+=("{{ domain }}:OK:$days_left") else echo "⚠️ {{ domain }}: Only $days_left days remaining" verification_results+=("{{ domain }}:WARNING:$days_left") fi else echo "❌ {{ domain }}: Cannot parse expiration date" verification_results+=("{{ domain }}:ERROR:unknown") fi else echo "❌ {{ domain }}: SSL connection failed" verification_results+=("{{ domain }}:ERROR:connection_failed") fi else echo "⚠️ Cannot verify {{ domain }}: OpenSSL not available" verification_results+=("{{ domain }}:SKIP:no_openssl") fi {% endfor %} {% endfor %} echo "" echo "📊 VERIFICATION SUMMARY:" for result in "${verification_results[@]}"; do echo "$result" done register: certificate_verification changed_when: false - name: Generate certificate management report copy: content: | 🔒 CERTIFICATE MANAGEMENT REPORT - {{ inventory_hostname }} ====================================================== 📅 Management Date: {{ ansible_date_time.iso8601 }} 🖥️ Host: {{ inventory_hostname }} 🔍 Check Only: {{ check_only }} 🔄 Force Renewal: {{ force_renewal }} 📅 Renewal Threshold: {{ renewal_threshold_days }} days 💾 Backup Created: {{ backup_certificates }} 📋 CERTIFICATES MANAGED: {{ current_certificates | length }} {% for cert in current_certificates %} - {{ cert.name }}: {{ cert.domains | join(', ') }} ({{ cert.renewal_method }}) {% endfor %} 📊 CERTIFICATE STATUS: {{ certificate_status.stdout }} {% if not check_only %} 🔄 RENEWAL ACTIONS: {% if certbot_renewal is defined %} Certbot Renewal: {{ 'Success' if certbot_renewal.rc == 0 else 'Failed' }} {% endif %} {% if service_restart_result is defined %} Service Restarts: {{ service_restart_result.stdout }} {% endif %} {% if backup_certificates %} 💾 BACKUP INFO: {{ certificate_backup.stdout }} {% endif %} {% endif %} 🔍 VERIFICATION RESULTS: {{ certificate_verification.stdout }} 💡 RECOMMENDATIONS: - Schedule regular certificate checks via cron - Monitor certificate expiration alerts - Test certificate renewal in staging environment - Keep certificate backups in secure location {% if current_certificates | selectattr('renewal_method', 'equalto', 'npm') | list | length > 0 %} - Nginx Proxy Manager handles automatic renewal {% endif %} ✅ CERTIFICATE MANAGEMENT COMPLETE dest: "/tmp/certificate_reports/{{ ansible_date_time.date }}/{{ inventory_hostname }}_cert_report.txt" delegate_to: localhost - name: Display certificate management summary debug: msg: | ✅ CERTIFICATE MANAGEMENT COMPLETE - {{ inventory_hostname }} ==================================================== 📅 Date: {{ ansible_date_time.date }} 🔍 Mode: {{ 'Check Only' if check_only else 'Full Management' }} 📋 Certificates: {{ current_certificates | length }} {{ certificate_verification.stdout }} 📄 Full report: /tmp/certificate_reports/{{ ansible_date_time.date }}/{{ inventory_hostname }}_cert_report.txt 🔍 Next Steps: {% if check_only %} - Run without check_only to perform renewals {% endif %} - Schedule regular certificate monitoring - Set up expiration alerts - Test certificate functionality ==================================================== - name: Send certificate alerts (if configured) debug: msg: | 📧 CERTIFICATE ALERT Host: {{ inventory_hostname }} Certificates expiring soon detected! Check the full report for details. when: - send_alerts | default(false) | bool - "'WARNING' in certificate_verification.stdout"