Certificates: only include SANs permitted by the Root CA name constraints.

Global IPs from `hostname -I` (e.g. routable IPv6 assigned via SLAAC)
violated the Root CA's critical nameConstraints, failing leaf verification
with "permitted subtree violation" (error 47) and aborting
install/repair/upgrade. Filter SAN candidates, including EXTRA_SANS, to the
permitted private ranges and log skipped entries; normalize the IP:/DNS:
prefix case in EXTRA_SANS since openssl rejects lowercase prefixes.

Also guard the service stops in upgrade.sh so upgrading a box whose
install died before the units were installed can still recover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Roland 2026-07-05 12:37:59 -06:00
parent 7d6a77a54e
commit e8c6d8415b
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
2 changed files with 37 additions and 4 deletions

View file

@ -39,6 +39,23 @@ fi
# --- Leaf certificate (always regenerated) ---
echo "Generating WROLPi leaf certificate..."
# Only SANs inside the Root CA's name constraints may be included; any other
# (e.g. a global IPv6 address from the router) fails verification with
# "permitted subtree violation". Takes one SAN like "IP:192.168.0.1" or "DNS:wrolpi.local".
san_permitted() {
local san
san="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
case "${san}" in
dns:localhost|dns:*.local) return 0 ;;
ip:127.*|ip:10.*|ip:192.168.*) return 0 ;;
ip:172.1[6-9].*|ip:172.2[0-9].*|ip:172.3[01].*) return 0 ;;
ip:::1) return 0 ;;
ip:f[cd]*) return 0 ;; # fc00::/7 unique-local
ip:fe[89ab]*) return 0 ;; # fe80::/10 link-local
*) return 1 ;;
esac
}
# Build SAN list dynamically.
SAN="DNS:localhost,DNS:wrolpi.local"
@ -51,12 +68,27 @@ SAN="${SAN},IP:127.0.0.1"
# Add all local IPs.
for ip in $(hostname -I 2>/dev/null || true); do
SAN="${SAN},IP:${ip}"
if san_permitted "IP:${ip}"; then
SAN="${SAN},IP:${ip}"
else
echo "Skipping IP:${ip}: not permitted by Root CA name constraints"
fi
done
# Append any extra SANs (e.g. host IP when running in Docker).
if [[ -n "${EXTRA_SANS:-}" ]]; then
SAN="${SAN},${EXTRA_SANS}"
for extra in ${EXTRA_SANS//,/ }; do
# openssl requires the canonical prefix case (IP:/DNS:).
case "$(printf '%s' "${extra%%:*}" | tr '[:lower:]' '[:upper:]')" in
IP) extra="IP:${extra#*:}" ;;
DNS) extra="DNS:${extra#*:}" ;;
esac
if san_permitted "${extra}"; then
SAN="${SAN},${extra}"
else
echo "Skipping ${extra}: not permitted by Root CA name constraints"
fi
done
fi
echo "SANs: ${SAN}"

View file

@ -98,8 +98,9 @@ set -x
set -e
set -o pipefail
systemctl stop wrolpi-api
systemctl stop wrolpi-app
# Units may not be installed yet if a previous install/repair failed; repair.sh installs them later.
systemctl stop wrolpi-api || :
systemctl stop wrolpi-app || :
# Fetch the latest commits without modifying the working tree.
git -C /opt/wrolpi fetch || exit 4