mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 02:48:38 -04:00
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>
126 lines
4.3 KiB
Bash
Executable file
126 lines
4.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Generate a root CA and signed leaf certificate for WROLPi.
|
|
#
|
|
# Root CA: stored in ${MEDIA_DIRECTORY}/config/ssl/ — persists forever.
|
|
# Leaf cert: stored in ${CERT_DIR}/ — regenerated each run to pick up hostname/IP changes.
|
|
#
|
|
# Users trust the root CA once; leaf certs can be regenerated without re-trusting.
|
|
|
|
set -e
|
|
|
|
MEDIA_DIRECTORY="${MEDIA_DIRECTORY:-/media/wrolpi}"
|
|
CERT_DIR="${CERT_DIR:-/etc/ssl/wrolpi}"
|
|
|
|
CA_DIR="${MEDIA_DIRECTORY}/config/ssl"
|
|
CA_KEY="${CA_DIR}/ca.key"
|
|
CA_CERT="${CA_DIR}/ca.crt"
|
|
|
|
LEAF_KEY="${CERT_DIR}/cert.key"
|
|
LEAF_CERT="${CERT_DIR}/cert.crt"
|
|
|
|
mkdir -p "${CA_DIR}" "${CERT_DIR}"
|
|
|
|
# --- Root CA (only created once) ---
|
|
if [[ -f "${CA_CERT}" && -f "${CA_KEY}" ]]; then
|
|
echo "Root CA already exists at ${CA_CERT}"
|
|
else
|
|
echo "Generating WROLPi Root CA..."
|
|
openssl genrsa -out "${CA_KEY}" 4096
|
|
openssl req -new -x509 -nodes -key "${CA_KEY}" -out "${CA_CERT}" -days 36500 \
|
|
-subj "/C=US/ST=State/L=City/O=WROLPi/CN=WROLPi Root CA" \
|
|
-addext "basicConstraints = critical, CA:TRUE" \
|
|
-addext "keyUsage = critical, keyCertSign, cRLSign" \
|
|
-addext "nameConstraints = critical, permitted;DNS:.local, permitted;DNS:localhost, permitted;IP:127.0.0.0/255.0.0.0, permitted;IP:10.0.0.0/255.0.0.0, permitted;IP:172.16.0.0/255.240.0.0, permitted;IP:192.168.0.0/255.255.0.0, permitted;IP:::1/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff, permitted;IP:fc00::/fe00::, permitted;IP:fe80::/ffc0::"
|
|
chmod 600 "${CA_KEY}"
|
|
chmod 644 "${CA_CERT}"
|
|
echo "Root CA generated."
|
|
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"
|
|
|
|
HOSTNAME_SHORT="$(hostname)"
|
|
if [[ -n "${HOSTNAME_SHORT}" ]]; then
|
|
SAN="${SAN},DNS:${HOSTNAME_SHORT}.local"
|
|
fi
|
|
|
|
SAN="${SAN},IP:127.0.0.1"
|
|
|
|
# Add all local IPs.
|
|
for ip in $(hostname -I 2>/dev/null || true); do
|
|
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
|
|
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}"
|
|
|
|
# Create CSR and sign with root CA.
|
|
openssl genrsa -out "${LEAF_KEY}" 2048
|
|
|
|
openssl req -new -key "${LEAF_KEY}" \
|
|
-subj "/C=US/ST=State/L=City/O=WROLPi/CN=${HOSTNAME_SHORT:-wrolpi}.local" \
|
|
-out "${CERT_DIR}/cert.csr"
|
|
|
|
openssl x509 -req -in "${CERT_DIR}/cert.csr" \
|
|
-CA "${CA_CERT}" -CAkey "${CA_KEY}" -CAcreateserial \
|
|
-days 825 \
|
|
-extfile <(printf "subjectAltName=%s\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth" "${SAN}") \
|
|
-out "${LEAF_CERT}"
|
|
|
|
rm -f "${CERT_DIR}/cert.csr"
|
|
chmod 640 "${LEAF_KEY}" "${LEAF_CERT}"
|
|
|
|
# Allow the caddy user to read the leaf cert/key (native installs).
|
|
if id caddy >/dev/null 2>&1; then
|
|
chown root:caddy "${LEAF_KEY}" "${LEAF_CERT}"
|
|
fi
|
|
|
|
# Validate the chain.
|
|
openssl verify -CAfile "${CA_CERT}" "${LEAF_CERT}"
|
|
echo "Leaf certificate generated and verified."
|
|
|
|
# Install root CA into system trust store so the device trusts its own certs.
|
|
if command -v update-ca-certificates >/dev/null 2>&1; then
|
|
cp "${CA_CERT}" /usr/local/share/ca-certificates/wrolpi-ca.crt
|
|
update-ca-certificates
|
|
echo "Root CA installed in system trust store."
|
|
fi
|