From d6b019c24e7983b94448ec3d2c5489a8e0156de3 Mon Sep 17 00:00:00 2001 From: Roland Date: Thu, 9 Jul 2026 15:28:54 -0600 Subject: [PATCH] Certificates: help.sh validates the CA and leaf certs; fix renew exec bit. help.sh now detects invalid Caddy certificates: expired or unparseable CA/leaf certs, a leaf expiring within 30 days, a leaf that does not match its private key or the current hostname, and Caddy serving a stale cert after regeneration. It also checks that the renewal script is executable and its timer is enabled. scripts/renew_certificates.sh was committed without the executable bit, so wrolpi-cert-renew.service failed with "Permission denied". Set the bit and make repair.sh restore executable bits on all scripts. Co-Authored-By: Claude Fable 5 --- help.sh | 64 +++++++++++++++++++++++++++++++++++ repair.sh | 4 +++ scripts/renew_certificates.sh | 0 3 files changed, 68 insertions(+) mode change 100644 => 100755 scripts/renew_certificates.sh diff --git a/help.sh b/help.sh index 4b275cc8..37c270a0 100755 --- a/help.sh +++ b/help.sh @@ -303,6 +303,70 @@ if [ -f ${MEDIA_DIRECTORY}/config/ssl/ca.crt ] && [ -f /etc/ssl/wrolpi/cert.crt fi fi +if [ -f ${MEDIA_DIRECTORY}/config/ssl/ca.crt ]; then + if ! openssl x509 -noout -in ${MEDIA_DIRECTORY}/config/ssl/ca.crt >/dev/null 2>&1; then + echo "FAILED: Root CA certificate cannot be parsed (regenerate with /opt/wrolpi/scripts/generate_certificates.sh)" + elif ! openssl x509 -checkend 0 -noout -in ${MEDIA_DIRECTORY}/config/ssl/ca.crt >/dev/null 2>&1; then + echo "FAILED: Root CA certificate is expired (delete ${MEDIA_DIRECTORY}/config/ssl and regenerate with /opt/wrolpi/scripts/generate_certificates.sh)" + else + echo "OK: Root CA certificate is valid (expires $(openssl x509 -enddate -noout -in ${MEDIA_DIRECTORY}/config/ssl/ca.crt | cut -d= -f2))" + fi +fi + +if [ -f /etc/ssl/wrolpi/cert.crt ]; then + if ! openssl x509 -noout -in /etc/ssl/wrolpi/cert.crt >/dev/null 2>&1; then + echo "FAILED: TLS certificate cannot be parsed (regenerate with /opt/wrolpi/scripts/generate_certificates.sh)" + elif ! openssl x509 -checkend 0 -noout -in /etc/ssl/wrolpi/cert.crt >/dev/null 2>&1; then + echo "FAILED: TLS certificate is expired (regenerate with /opt/wrolpi/scripts/generate_certificates.sh)" + elif ! openssl x509 -checkend 2592000 -noout -in /etc/ssl/wrolpi/cert.crt >/dev/null 2>&1; then + echo "Note: TLS certificate expires within 30 days ($(openssl x509 -enddate -noout -in /etc/ssl/wrolpi/cert.crt | cut -d= -f2))" + else + echo "OK: TLS certificate is valid (expires $(openssl x509 -enddate -noout -in /etc/ssl/wrolpi/cert.crt | cut -d= -f2))" + fi + + if openssl x509 -in /etc/ssl/wrolpi/cert.crt -noout -text 2>/dev/null | grep -A1 'Subject Alternative Name' | grep -q "$(hostname).local"; then + echo "OK: TLS certificate covers $(hostname).local" + else + echo "FAILED: TLS certificate does not cover $(hostname).local (was the hostname changed? regenerate with /opt/wrolpi/scripts/generate_certificates.sh)" + fi +fi + +if [ -f /etc/ssl/wrolpi/cert.crt ] && [ -f /etc/ssl/wrolpi/cert.key ]; then + cert_pubkey=$(openssl x509 -in /etc/ssl/wrolpi/cert.crt -noout -pubkey 2>/dev/null) + key_pubkey=$(openssl pkey -in /etc/ssl/wrolpi/cert.key -pubout 2>/dev/null) + if [ -n "${cert_pubkey}" ] && [ "${cert_pubkey}" = "${key_pubkey}" ]; then + echo "OK: TLS certificate matches its private key" + else + echo "FAILED: TLS certificate does not match its private key (regenerate with /opt/wrolpi/scripts/generate_certificates.sh)" + fi +fi + +# Compare the certificate Caddy is actually serving to the one on disk. A mismatch means +# Caddy was not reloaded after the certificate was regenerated. +if [ -f /etc/ssl/wrolpi/cert.crt ]; then + served_fingerprint=$(echo | timeout 5 openssl s_client -connect 127.0.0.1:443 2>/dev/null | openssl x509 -noout -fingerprint -sha256 2>/dev/null) + disk_fingerprint=$(openssl x509 -in /etc/ssl/wrolpi/cert.crt -noout -fingerprint -sha256 2>/dev/null) + if [ -z "${served_fingerprint}" ]; then + echo "FAILED: Could not fetch the TLS certificate being served on port 443" + elif [ "${served_fingerprint}" = "${disk_fingerprint}" ]; then + echo "OK: Caddy is serving the TLS certificate on disk" + else + echo "FAILED: Caddy is serving a different TLS certificate than the one on disk (reload with: systemctl reload caddy)" + fi +fi + +if [ -x /opt/wrolpi/scripts/renew_certificates.sh ]; then + echo "OK: Certificate renewal script is executable" +else + echo "FAILED: Certificate renewal script is not executable (fix with: chmod +x /opt/wrolpi/scripts/renew_certificates.sh)" +fi + +if systemctl is-enabled wrolpi-cert-renew.timer >/dev/null 2>&1; then + echo "OK: Certificate renewal timer is enabled" +else + echo "FAILED: Certificate renewal timer is not enabled (fix with: systemctl enable --now wrolpi-cert-renew.timer)" +fi + for port in 80 443 8084 8085 8086; do if netstat -ant | grep LISTEN | grep -E "(0\.0\.0\.0|:::)${port}\b" >/dev/null; then echo "OK: Caddy port ${port} is occupied" diff --git a/repair.sh b/repair.sh index 35edfaf9..2b802f61 100755 --- a/repair.sh +++ b/repair.sh @@ -45,6 +45,10 @@ git config --global --add safe.directory /opt/wrolpi git reset HEAD --hard chown -R wrolpi:wrolpi /opt/wrolpi +# Scripts are run by systemd services (e.g. wrolpi-cert-renew.service) which fail with +# "Permission denied" if the executable bit was lost. +chmod +x /opt/wrolpi/*.sh /opt/wrolpi/scripts/*.sh + # Copy configs to system. mkdir -p /etc/caddy cp /opt/wrolpi/etc/raspberrypios/Caddyfile /etc/caddy/Caddyfile diff --git a/scripts/renew_certificates.sh b/scripts/renew_certificates.sh old mode 100644 new mode 100755