Complexity: Low
Duration: 30-60 minutes setup
Use Case: Servers without ACME/Kubernetes
Automatic certificate renewal via scheduled jobs for classic server environments.
#!/bin/bash # /usr/local/bin/cert-renewal.sh # Automatic certificate renewal set -euo pipefail # Configuration CERT_DIR="/etc/ssl/certs" KEY_DIR="/etc/ssl/private" CA_SERVER="https://ca.internal.example.com" RENEWAL_DAYS=30 LOG_FILE="/var/log/cert-renewal.log" MAIL_TO="pki-team@example.com" log() { echo "$(date -Iseconds) $1" >> "$LOG_FILE" } check_and_renew() { local cert_file="$1" local key_file="$2" local service="$3" # Check expiry if openssl x509 -checkend $((RENEWAL_DAYS * 86400)) -noout -in "$cert_file" 2>/dev/null; then log "INFO: $cert_file still valid" return 0 fi log "WARN: $cert_file expiring, renewing..." # Extract subject from old certificate local subject=$(openssl x509 -in "$cert_file" -subject -noout | sed 's/subject=//') local san=$(openssl x509 -in "$cert_file" -text -noout | grep -A1 "Subject Alternative" | tail -1 | tr -d ' ') # Create new CSR local csr_file=$(mktemp) openssl req -new -key "$key_file" -out "$csr_file" -subj "$subject" # Send CSR to CA (example: REST API) local new_cert=$(curl -s -X POST "$CA_SERVER/api/sign" \ -H "Content-Type: application/x-pem-file" \ -H "Authorization: Bearer $(cat /etc/ssl/ca-token)" \ --data-binary @"$csr_file") if [ -z "$new_cert" ]; then log "ERROR: Renewal failed for $cert_file" echo "Certificate renewal failed: $cert_file" | mail -s "CERT RENEWAL FAILED" "$MAIL_TO" rm "$csr_file" return 1 fi # Backup and replace cp "$cert_file" "${cert_file}.bak.$(date +%Y%m%d)" echo "$new_cert" > "$cert_file" # Reload service if [ -n "$service" ]; then systemctl reload "$service" || systemctl restart "$service" log "INFO: Service $service reloaded" fi rm "$csr_file" log "INFO: $cert_file successfully renewed" # Notification echo "Certificate renewed: $cert_file" | mail -s "CERT RENEWED" "$MAIL_TO" } # Main logic log "=== Renewal check started ===" # Define certificates: cert_file:key_file:service CERTS=( "/etc/ssl/certs/webserver.pem:/etc/ssl/private/webserver.key:nginx" "/etc/ssl/certs/api.pem:/etc/ssl/private/api.key:api-service" "/etc/ssl/certs/mail.pem:/etc/ssl/private/mail.key:postfix" ) for cert_entry in "${CERTS[@]}"; do IFS=':' read -r cert_file key_file service <<< "$cert_entry" check_and_renew "$cert_file" "$key_file" "$service" || true done log "=== Renewal check completed ==="
chmod +x /usr/local/bin/cert-renewal.sh
# /etc/cron.d/cert-renewal # Daily at 03:00 0 3 * * * root /usr/local/bin/cert-renewal.sh
# /etc/systemd/system/cert-renewal.service [Unit] Description=Certificate Renewal Service After=network.target [Service] Type=oneshot ExecStart=/usr/local/bin/cert-renewal.sh StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target
# /etc/systemd/system/cert-renewal.timer [Unit] Description=Daily Certificate Renewal Check [Timer] OnCalendar=*-*-* 03:00:00 RandomizedDelaySec=1h Persistent=true [Install] WantedBy=timers.target
# Enable timer systemctl daemon-reload systemctl enable --now cert-renewal.timer # Check status systemctl list-timers cert-renewal.timer journalctl -u cert-renewal.service
# %SCRIPTS_PATH%\Cert-Renewal.ps1 param( [int]$RenewalDays = 30, [string]$LogPath = "C:\Logs\cert-renewal.log", [string]$SmtpServer = "smtp.example.com", [string]$MailTo = "pki-team@example.com" ) function Write-Log { param([string]$Message) $timestamp = Get-Date -Format "yyyy-MM-ddTHH:mm:ss" "$timestamp $Message" | Out-File -Append -FilePath $LogPath } function Test-CertificateExpiry { param( [string]$CertPath, [int]$DaysThreshold ) $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertPath) $expiryDate = $cert.NotAfter $daysUntilExpiry = ($expiryDate - (Get-Date)).Days return @{ DaysRemaining = $daysUntilExpiry ExpiryDate = $expiryDate NeedsRenewal = $daysUntilExpiry -lt $DaysThreshold Subject = $cert.Subject } } function Invoke-CertificateRenewal { param( [string]$CertPath, [string]$KeyPath, [string]$ServiceName ) Write-Log "INFO: Starting renewal for $CertPath" try { # Create CSR from existing certificate $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertPath) # Here the CA API would be called # Example: REST call to internal CA $body = @{ subject = $cert.Subject san = $cert.Extensions | Where-Object { $_.Oid.Value -eq "2.5.29.17" } } | ConvertTo-Json $newCert = Invoke-RestMethod -Uri "https://ca.internal.example.com/api/renew" ` -Method POST ` -Body $body ` -Headers @{ Authorization = "Bearer $(Get-Content %SCRIPTS_PATH%\ca-token.txt)" } # Create backup $backupPath = "$CertPath.bak.$(Get-Date -Format 'yyyyMMdd')" Copy-Item $CertPath $backupPath # Save new certificate $newCert | Out-File -FilePath $CertPath -Encoding ascii # Restart service if ($ServiceName) { Restart-Service $ServiceName -Force Write-Log "INFO: Service $ServiceName restarted" } Write-Log "INFO: Renewal successful for $CertPath" # Email notification Send-MailMessage -To $MailTo -From "pki@example.com" ` -Subject "Certificate renewed: $($cert.Subject)" ` -Body "The certificate was successfully renewed." ` -SmtpServer $SmtpServer return $true } catch { Write-Log "ERROR: Renewal failed - $_" Send-MailMessage -To $MailTo -From "pki@example.com" ` -Subject "ERROR: Certificate renewal" ` -Body "Error renewing $CertPath : $_" ` -SmtpServer $SmtpServer return $false } } # Main logic Write-Log "=== Renewal check started ===" $certificates = @( @{ Cert = "C:\certs\webserver.pem"; Key = "C:\certs\webserver.key"; Service = "W3SVC" }, @{ Cert = "C:\certs\api.pem"; Key = "C:\certs\api.key"; Service = "APIService" } ) foreach ($certConfig in $certificates) { $status = Test-CertificateExpiry -CertPath $certConfig.Cert -DaysThreshold $RenewalDays Write-Log "INFO: $($certConfig.Cert) - $($status.DaysRemaining) days remaining" if ($status.NeedsRenewal) { Invoke-CertificateRenewal ` -CertPath $certConfig.Cert ` -KeyPath $certConfig.Key ` -ServiceName $certConfig.Service } } Write-Log "=== Renewal check completed ==="
Set up Task Scheduler:
# Create task $action = New-ScheduledTaskAction -Execute "PowerShell.exe" ` -Argument "-ExecutionPolicy Bypass -File %SCRIPTS_PATH%\Cert-Renewal.ps1" $trigger = New-ScheduledTaskTrigger -Daily -At "03:00" $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -DontStopOnIdleEnd Register-ScheduledTask -TaskName "Certificate Renewal" ` -Action $action ` -Trigger $trigger ` -Principal $principal ` -Settings $settings
# cert-renewal.yml --- - name: Certificate Renewal hosts: all become: yes vars: renewal_days: 30 ca_server: "https://ca.internal.example.com" tasks: - name: Check certificate expiry community.crypto.x509_certificate_info: path: "{{ item.cert }}" register: cert_info loop: "{{ certificates }}" - name: Create CSR for expiring certificates community.crypto.openssl_csr: path: "/tmp/{{ item.item.name }}.csr" privatekey_path: "{{ item.item.key }}" common_name: "{{ item.subject.commonName }}" subject_alt_name: "{{ item.subject_alt_name | default(omit) }}" when: (item.not_after | to_datetime('%Y%m%d%H%M%SZ') - ansible_date_time.iso8601 | to_datetime('%Y-%m-%dT%H:%M:%SZ')).days < renewal_days loop: "{{ cert_info.results }}" - name: Request new certificate from CA uri: url: "{{ ca_server }}/api/sign" method: POST body_format: raw body: "{{ lookup('file', '/tmp/' + item.item.name + '.csr') }}" headers: Authorization: "Bearer {{ ca_token }}" return_content: yes register: new_cert when: (item.not_after | to_datetime('%Y%m%d%H%M%SZ') - ansible_date_time.iso8601 | to_datetime('%Y-%m-%dT%H:%M:%SZ')).days < renewal_days loop: "{{ cert_info.results }}" - name: Deploy new certificate copy: content: "{{ item.content }}" dest: "{{ item.item.item.cert }}" backup: yes notify: Reload services when: item.content is defined loop: "{{ new_cert.results }}" handlers: - name: Reload services service: name: "{{ item.item.item.service }}" state: reloaded loop: "{{ new_cert.results }}" when: item.changed
# Inventory (group_vars/all.yml) certificates: - name: webserver cert: /etc/ssl/certs/webserver.pem key: /etc/ssl/private/webserver.key service: nginx - name: api cert: /etc/ssl/certs/api.pem key: /etc/ssl/private/api.key service: api-service
# Prometheus Node Exporter Textfile # /var/lib/node_exporter/textfile_collector/cert_expiry.prom for cert in /etc/ssl/certs/*.pem; do expiry=$(openssl x509 -enddate -noout -in "$cert" 2>/dev/null | cut -d= -f2) if [ -n "$expiry" ]; then expiry_epoch=$(date -d "$expiry" +%s) name=$(basename "$cert" .pem) echo "cert_expiry_seconds{cert=\"$name\"} $expiry_epoch" fi done
| # | Checkpoint | Done |
| — | ———— | —— |
| 1 | Renewal script created and tested | |
| 2 | Cron/Timer/Task configured | |
| 3 | CA authentication set up | |
| 4 | Backup before renewal | |
| 5 | Service reload works | |
| 6 | Notification on error | |
| 7 | Logging enabled |
« <- Kubernetes Cert-Manager | -> Operator Scenarios »
Wolfgang van der Stille @ EMSR DATA d.o.o. - Post-Quantum Cryptography Professional