====== Scheduled Renewal ======
**Complexity:** Low \\
**Duration:** 30-60 minutes setup \\
**Use Case:** Servers without ACME/Kubernetes
Automatic certificate renewal via scheduled jobs for classic server environments.
----
===== Architecture =====
flowchart LR
subgraph TRIGGER["TRIGGER"]
C[Cron Job]
S[Systemd Timer]
T[Task Scheduler]
end
subgraph CHECK["CHECK"]
E{Expiry < 30 days?}
end
subgraph RENEW["RENEWAL"]
R1[Create CSR]
R2[Get signed]
R3[Deploy]
R4[Reload service]
end
subgraph NOTIFY["NOTIFICATION"]
N1[Email]
N2[Slack/Teams]
N3[Ticket]
end
C --> E
S --> E
T --> E
E -->|Yes| R1 --> R2 --> R3 --> R4
E -->|No| N1
R4 --> N1
R4 --> N2
R4 --> N3
style E fill:#fff3e0
style R3 fill:#e8f5e9
----
===== Linux: Cron + Bash =====
==== Renewal Script ====
#!/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
==== Cron Job ====
# /etc/cron.d/cert-renewal
# Daily at 03:00
0 3 * * * root /usr/local/bin/cert-renewal.sh
----
===== Linux: Systemd Timer =====
# /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
----
===== Windows: Task Scheduler + PowerShell =====
# %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
----
===== Ansible Automation =====
# 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
----
===== Monitoring Integration =====
# 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
----
===== Checklist =====
| # | 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 | |
----
===== Related Documentation =====
* [[..:tagesgeschaeft:zertifikat-erneuern|Manual Renewal]] - For special cases
* [[.:acme-integration|ACME Integration]] - For public certificates
* [[..:monitoring:ablauf-monitoring|Expiry Monitoring]] - Monitoring
----
<< [[.:cert-manager-k8s|<- Kubernetes Cert-Manager]] | [[..:start|-> Operator Scenarios]] >>
----
//Wolfgang van der Stille @ EMSR DATA d.o.o. - Post-Quantum Cryptography Professional//
{{tag>scheduled renewal cron systemd powershell ansible automation operator}}