#!/bin/bash # Certbot post-renewal hook for HAProxy # Managed by Ansible - DO NOT EDIT MANUALLY # # This script: # 1. Combines fullchain.pem + privkey.pem into HAProxy format # 2. Sets correct permissions # 3. Reloads HAProxy via systemd # 4. Updates certificate metrics for Prometheus set -uo pipefail # RENEWED_LINEAGE is set by certbot --deploy-hook or passed explicitly by deploy.yml CERT_DIR="${RENEWED_LINEAGE:?RENEWED_LINEAGE must be set}" CERT_NAME=$(basename "${CERT_DIR}") HAPROXY_CERT="{{ haproxy_cert_path }}" HAPROXY_DIR="{{ haproxy_directory }}" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } fail() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2; } # Always refresh Prometheus cert metrics on exit, even if installation below # fails. The metrics drive the SSLCertificateExpired/ExpiringSoon alerts, so # they must reflect reality precisely when the hook is broken — otherwise a # failed renewal rots silently (which is exactly how the cert expired before). # A non-zero exit is reported by certbot as a WARNING, surfacing the failure. hook_status=0 finish() { {{ certbot_directory }}/hooks/cert-metrics.sh || fail "cert-metrics.sh failed" if [[ ${hook_status} -ne 0 ]]; then fail "Renewal hook FAILED for ${CERT_NAME} — HAProxy is serving a STALE certificate" fi exit "${hook_status}" } trap finish EXIT log "Starting renewal hook for ${CERT_NAME}" # Check if certificate files exist if [[ ! -f "${CERT_DIR}/fullchain.pem" ]] || [[ ! -f "${CERT_DIR}/privkey.pem" ]]; then fail "Certificate files not found in ${CERT_DIR}" hook_status=1 exit 1 fi # Combine certificate and private key for HAProxy (single PEM), writing to a # temp file in the same directory and moving atomically so HAProxy never reads # a partial file. A permission failure here is the documented failure mode. if ! cat "${CERT_DIR}/fullchain.pem" "${CERT_DIR}/privkey.pem" > "${HAPROXY_CERT}.tmp"; then fail "Could not write ${HAPROXY_CERT}.tmp — check ownership/permissions of $(dirname "${HAPROXY_CERT}")" rm -f "${HAPROXY_CERT}.tmp" hook_status=1 exit 1 fi if ! mv "${HAPROXY_CERT}.tmp" "${HAPROXY_CERT}"; then fail "Could not move combined PEM into place at ${HAPROXY_CERT}" rm -f "${HAPROXY_CERT}.tmp" hook_status=1 exit 1 fi # Set permissions chown {{ certbot_user }}:{{ haproxy_group }} "${HAPROXY_CERT}" chmod 640 "${HAPROXY_CERT}" log "Certificate combined and written to ${HAPROXY_CERT}" # Reload HAProxy if running. The hook runs as the unprivileged certbot user, # so the reload goes through sudo (a scoped sudoers rule grants exactly this # command). sudo -n fails fast rather than blocking on a password prompt. if systemctl is-active --quiet haproxy; then log "Reloading HAProxy..." if sudo -n systemctl reload haproxy; then log "HAProxy reloaded" else fail "HAProxy reload failed" hook_status=1 exit 1 fi else log "HAProxy not running, skipping reload" fi log "Renewal hook completed successfully"