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


« <- Kubernetes Cert-Manager | -> Operator Scenarios »


Wolfgang van der Stille @ EMSR DATA d.o.o. - Post-Quantum Cryptography Professional

Zuletzt geändert: on 2026/01/30 at 01:22 AM