#!/bin/bash # OpenSLX SSL Certificate management mkdir -p "/run/openslx" if ! mkdir "/run/openslx/cert-manager" 2> /dev/null; then echo "Already in progress." exit 1 fi trap 'rm -rf -- /run/openslx/cert-manager' EXIT declare -rg BASE="/etc/ssl/openslx" declare -rg PRIVDIR="$BASE/private" declare -rg CERTDIR="$BASE/cert" declare -rg LIGHTDIR="$BASE/lighttpd" mkdir -p "$BASE" "$PRIVDIR" "$CERTDIR" chown -R root:root "$BASE" || exit 1 chmod u+rwx,go+rx-w "$BASE" "$CERTDIR" || exit 1 chmod u+rwx,go-rwx "$PRIVDIR" || exit 1 # Before doing anything, make sure we have a CA with enough validity left # File name format for ca is: # ${PRIVDIR}/ca-TTTTTTTTTT.key # ${CERTDIR}/ca-TTTTTTTTTT.crt # Where TT is the unix timestamp of "validTo" of that cert. # # A new CA will be generated when the previous has less than two years # of validity left. # We deliver a new CA to clients immediately when it was generated, but # keep signing server certs with the old one, until the old one only # has a year of validity left, then we switch to the next-oldest CA-cert. # CA certs are valid for 10 years, server certs for 1 year. # This should allow for clients to have two years of uptime without any # problems due to cert rollover. declare -rg NOW="$( date +%s )" # PROD declare -rg srv_days=365 # 1y declare -rg srv_s="$(( srv_days * 86400 ))" declare -rg srv_min_remain_s="$(( srv_days / 2 * 86400 ))" # half a year declare -rg srv_new_ts="$(( srv_days * 86400 + NOW ))" declare -rg ca_days="$(( 10 * 365 ))" # 10y declare -rg ca_min_remain_s="$(( 2 * srv_days * 86400 ))" # 2y, twice that of a server cert declare -rg ca_new_expire_ts="$(( ca_days * 86400 + NOW ))" # TEST #declare -rg ca_days=1825 # 5y #declare -rg ca_min_remain_s="$(( 1260 ))" # bit more than 1y #declare -rg ca_new_expire_ts="$(( 1320 + NOW ))" #declare -rg srv_days=365 # 1y #declare -rg srv_min_remain_s="$(( 1200 ))" # half a year #declare -rg srv_new_ts="$(( 1230 + NOW ))" # Extract timestamp from given filename. # Filename format has to be ca-UUUUUUUUU[.-]* or srv-UUUUUUUU[.-]* # Where UUUUU is a unix timestamp get_ts () { ts="${1%.*}" # Remove everything from last dot on ts="${ts##*/ca-}" # remove ca- prefix ts="${ts##*/srv-}" # remove srv- prefix from="${ts%-*}" # Remove everything from last dash on if [ "$from" = "$ts" ]; then # There was no dash, we have a normal timestamp from= else # There was a dash - $from is now the start of the range, # $ts is the end of the range ts="${ts#*-}" fi # Set the return value (( ts > 1234567890 )) } # Create an openssl config for signing CSRs create_conf () { ca_dir="$( mktemp -d /tmp/bwlp-XXXXXXXX )" [ -z "$ca_dir" ] && exit 1 mkdir "$ca_dir"/{certs,crl,newcerts,private} touch "$ca_dir"/index.txt ca_config="$ca_dir/openssl.cnf" cp -f "/etc/ssl/openssl.cnf" "$ca_config" cat >> "$ca_config" <<-MYCA [ CA_openslx ] dir = $ca_dir certs = \$dir/certs crl_dir = \$dir/crl database = \$dir/index.txt new_certs_dir = \$dir/newcerts serial = \$dir/serial crl = \$dir/crl.pem x509_extensions = usr_cert name_opt = ca_default cert_opt = ca_default default_md = default preserve = no policy = policy_match MYCA } declare -ag cmdargs=( "$@" ) maybe_restart () { for i in "${cmdargs[@]}"; do [ "$i" = "--restart" ] && exit 13 done echo "Restating script, wiping everything except CAs..." rm -f -- /etc/ssl/openslx/*/{srv,int}* exec "$0" "${cmdargs[@]}" --restart exit 14 } # Start # Check if existing CA is still valid for long enough # Globbing is sorted so should make sure we check the newest one last declare -a ca_list=() ca_last= latest_ca_cert= oldest_ca_ts= for i in "${PRIVDIR}"/ca-??????????.key; do [ -s "$i" ] || continue if ! get_ts "$i"; then echo "Invalid ca-key: $i" rm -f -- "$i" continue fi cert="${CERTDIR}/ca-${ts}.crt" if ! [ -s "$cert" ] \ || (( ts < NOW )); then # Missing cert, or expired -> delete rm -f -- "$cert" "$i" continue fi # Check if key and cert match if [ "$( openssl x509 -in "$cert" -noout -pubkey )" != "$( openssl pkey -in "$i" -pubout )" ]; then echo "Publickey in cert doesn't match publickey in keypair: $cert + $i" rm -f -- "$cert" "$i" continue fi # All CAs still valid ca_list+=( "$cert" ) # Latest CA ca_last="$ts" latest_ca_cert="$cert" # Oldest one that is still valid for as long as we want to sign a new cert if [ -z "$oldest_ca_ts" ] && (( ts > NOW + srv_s )); then oldest_ca_ts="$ts" fi done mknew= if [ -z "$ca_last" ] || (( ca_last < NOW + ca_min_remain_s )); then # Make new CA since the newest one we have is about to expire echo "Creating new CA..." cert="${CERTDIR}/ca-${ca_new_expire_ts}.crt" openssl req -new -newkey rsa:3072 -x509 -days "$ca_days" -extensions v3_ca \ -nodes -subj "/C=DE/ST=PewPew/L=HeyHey/O=bwLehrpool/CN=ca-${NOW}.bwlehrpool" \ -keyout "${PRIVDIR}/ca-${ca_new_expire_ts}.key" -out "$cert" || exit 2 ca_list+=( "$cert" ) ca_last="${ca_new_expire_ts}" latest_ca_cert="$cert" if [ -z "$oldest_ca_ts" ]; then oldest_ca_ts="$ca_new_expire_ts" fi mknew=1 fi # Repackage config.tgz module? if [ -n "$mknew" ] || ! [ -s "/opt/openslx/configs/modules/self-signed-ca.tar" ] \ || ! [ -e "/run/openslx/cert-conf-done" ] \ || [ "/opt/openslx/configs/modules/self-signed-ca.tar" -ot "$latest_ca_cert" ]; then # Rebuild config module for clients echo "Updating client config module..." ( tmpdir="$( mktemp -d '/tmp/bwlp-XXXXXXX' )" # Copy all current ca-certs to tmpdir cp -a "${CERTDIR}/"ca-*.crt "$tmpdir/" cd "$tmpdir/" || exit 6 # Build hashed symlinks for openssl openssl rehash . # Put everything in config module and rebuild tar -c -k -f "/opt/openslx/configs/modules/self-signed-ca.tar" \ --transform 's#^[./][./]*#/opt/openslx/ssl/#' . cd /tmp || exit 7 rm -rf -- "$tmpdir" sudo -u www-data -n php /srv/openslx/www/slx-admin/api.php sysconfig --action rebuild echo "." touch "/run/openslx/cert-conf-done" ) fi # Now check the server certificate # First, check if we could still use old-style certs with intermediate declare -a unt_list=() for i in "${CERTDIR}"/intermediate-??????????.crt; do [ -s "$i" ] || continue if ! get_ts "$i"; then echo "Invalid intermediate cert: $i" rm -f -- "$i" continue fi if (( ts < NOW )); then echo "Expired intermediate: $i" rm -f -- "$i" continue fi unt_list+=( "-untrusted" "$i" ) done # Now check existing server certs have_srv= for i in "${PRIVDIR}"/srv-??????????.key; do [ -s "$i" ] || continue if ! get_ts "$i"; then echo "Invalid srv-key: $i" rm -f -- "$i" continue fi cert="${CERTDIR}/srv-${ts}.crt" if (( ts < NOW + srv_min_remain_s )) || ! [ -s "$cert" ]; then echo "Expired srv cert or key with no cert: $i" rm -f -- "$i" "$cert" continue fi # Keys match? if [ "$( openssl x509 -in "$cert" -noout -pubkey )" != "$( openssl pkey -in "$i" -pubout )" ]; then echo "Publickey in cert doesn't match publickey in keypair: $cert + $i" rm -f -- "$cert" "$i" continue fi # Validate chain valid= for ca in "${ca_list[@]}"; do if openssl verify -CAfile "$ca" "${unt_list[@]}" \ "$cert" &> /dev/null; then valid=1 break fi done if [ -n "$valid" ]; then have_srv=1 break fi echo "No valid CA/chain for $i, removing" rm -f -- "$i" "$cert" done # Now still check the current lighttpd config, in case it is out of sync # with our generated stuff for whatever reason. if [ -n "$have_srv" ] || [ -z "$makenew" ]; then if [ -s "${LIGHTDIR}/ca-chain.pem" ]; then unt_list=( "-untrusted" "${LIGHTDIR}/ca-chain.pem" ) else unt_list=() fi valid= for ca in "${ca_list[@]}"; do openssl verify -CAfile "$ca" "${unt_list[@]}" \ "${LIGHTDIR}/server.pem" &> /dev/null || continue valid=1 break done if [ -z "$valid" ]; then echo "Current lighttpd SSL setup seems invalid, making new one" makenew=1 fi fi # Make new one? if [ -z "$have_srv" ] || [ -n "$makenew" ]; then # Request ServerCert csr="$( mktemp /tmp/bwlp-XXXXXXX.csr )" echo "Generating new Server Certificate. Key+CSR..." rm -f -- "${CERTDIR}"/srv-*.crt "${PRIVDIR}/srv.key.tmp" "${PRIVDIR}"/srv-*.key openssl req -newkey rsa:3072 -nodes -keyout "${PRIVDIR}/srv.key.tmp" -out "$csr" \ -subj "/C=DE/ST=PewPew/L=HeyHey/O=bwLehrpool/CN=satellite.bwlehrpool" || exit 4 echo "Signing Server Certificate with CA..." sign_cert="${CERTDIR}/ca-${oldest_ca_ts}.crt" sign_key="${PRIVDIR}/ca-${oldest_ca_ts}.key" if ! [ -s "$sign_cert" ] || ! [ -s "$sign_key" ]; then echo "CA sign/key to sign does not exist!?" maybe_restart fi echo "Signing with $sign_cert" create_conf # Need extfile for SAN, chromium doesn't honor CN anymore cat > "${csr}.cnf" <<-END basicConstraints = CA:FALSE nsCertType = server nsComment = "bwLehrpool Generated Server Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer:always keyUsage = critical, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth subjectAltName = @alt_names [alt_names] DNS.1 = satellite.bwlehrpool END if ! openssl ca -config "$ca_config" -create_serial -policy policy_anything -days "$srv_days" \ -cert "$sign_cert" -keyfile "$sign_key" -extfile "${csr}.cnf" \ -notext -name CA_openslx -batch -out "${CERTDIR}/srv-${srv_new_ts}.crt" -in "$csr"; then echo "Failed to sign CSR" rm -f -- "$sign_key" "$sign_cert" maybe_restart fi rm -rf -- "$ca_dir" rm -f -- "$csr" "${csr}.cnf" mv "${PRIVDIR}/srv.key.tmp" "${PRIVDIR}/srv-${srv_new_ts}.key" || exit 5 # Combine and prepare for lighttpd mkdir -p "$LIGHTDIR" || exit 10 # Combine cert and key, as required by (older) lighttpd echo "Writing out lighttpd PEMs..." cat "${CERTDIR}/srv-${srv_new_ts}.crt" "${PRIVDIR}/srv-${srv_new_ts}.key" > "${LIGHTDIR}/server.pem" || exit 10 chmod 0600 "${LIGHTDIR}/server.pem" # Don't need this anymore rm -f -- "${LIGHTDIR}/ca-chain.pem" if [ "$1" = "--restart" ] || [ -t 0 ]; then echo "Restarting lighttpd..." systemctl restart lighttpd.service fi fi echo "Done." exit 0