2017-06-12 16:45:12 +08:00
#!/bin/bash
ACME_BASE = /var/lib/acme
2017-06-21 02:06:54 +08:00
SSL_EXAMPLE = /var/lib/ssl-example
2017-06-29 16:25:32 +08:00
2017-06-12 16:45:12 +08:00
mkdir -p ${ ACME_BASE } /acme/private
restart_containers( ) {
for container in $* ; do
2017-07-01 02:29:55 +08:00
echo " Restarting ${ container } ... "
2017-06-12 16:45:12 +08:00
curl -X POST \
--unix-socket /var/run/docker.sock \
" http/containers/ ${ container } /restart "
done
}
2017-06-29 05:22:51 +08:00
verify_hash_match( ) {
CERT_HASH = $( openssl x509 -noout -modulus -in " ${ 1 } " | openssl md5)
KEY_HASH = $( openssl rsa -noout -modulus -in " ${ 2 } " | openssl md5)
if [ [ ${ CERT_HASH } != ${ KEY_HASH } ] ] ; then
echo "Certificate and key hashes do not match!"
return 1
else
echo "Verified hashes."
return 0
fi
}
2017-09-12 03:51:17 +08:00
get_ipv4( ) {
local IPV4 =
local IPV4_SRCS =
local TRY =
IPV4_SRCS[ 0] = "api.ipify.org"
IPV4_SRCS[ 1] = "ifconfig.co"
IPV4_SRCS[ 2] = "icanhazip.com"
IPV4_SRCS[ 3] = "v4.ident.me"
IPV4_SRCS[ 4] = "ipecho.net/plain"
IPV4_SRCS[ 5] = "mailcow.email/ip.php"
until [ [ ! -z ${ IPV4 } ] ] || [ [ ${ TRY } -ge 100 ] ] ; do
IPV4 = $( curl --connect-timeout 3 -m 10 -L4s ${ IPV4_SRCS [ $RANDOM % ${# IPV4_SRCS [@] } ] } | grep -E " ^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) $" )
[ [ ! -z ${ TRY } ] ] && sleep 1
TRY = $(( TRY+1))
done
echo ${ IPV4 }
}
2017-06-29 06:56:51 +08:00
[ [ ! -f ${ ACME_BASE } /dhparams.pem ] ] && cp ${ SSL_EXAMPLE } /dhparams.pem ${ ACME_BASE } /dhparams.pem
if [ [ -f ${ ACME_BASE } /cert.pem ] ] && [ [ -f ${ ACME_BASE } /key.pem ] ] ; then
2017-06-23 14:33:07 +08:00
ISSUER = $( openssl x509 -in ${ ACME_BASE } /cert.pem -noout -issuer)
if [ [ ${ ISSUER } != *"Let's Encrypt" * && ${ ISSUER } != *"mailcow" * ] ] ; then
echo "Found certificate with issuer other than mailcow snake-oil CA and Let's Encrypt, skipping ACME client..."
2017-09-16 19:17:48 +08:00
sleep 3650d
exec $( readlink -f " $0 " )
2017-06-21 02:06:54 +08:00
else
declare -a SAN_ARRAY_NOW
SAN_NAMES = $( openssl x509 -noout -text -in ${ ACME_BASE } /cert.pem | awk '/X509v3 Subject Alternative Name/ {getline;gsub(/ /, "", $0); print}' | tr -d "DNS:" )
if [ [ ! -z ${ SAN_NAMES } ] ] ; then
IFS = ',' read -a SAN_ARRAY_NOW <<< ${ SAN_NAMES }
2017-06-23 14:33:07 +08:00
echo " Found Let's Encrypt or mailcow snake-oil CA issued certificate with SANs: ${ SAN_ARRAY_NOW [*] } "
2017-06-21 02:06:54 +08:00
fi
fi
else
if [ [ -f ${ ACME_BASE } /acme/fullchain.pem ] ] && [ [ -f ${ ACME_BASE } /acme/private/privkey.pem ] ] ; then
2017-06-29 05:22:51 +08:00
if verify_hash_match ${ ACME_BASE } /acme/fullchain.pem ${ ACME_BASE } /acme/private/privkey.pem; then
2017-06-29 06:56:51 +08:00
echo "Restoring previous acme certificate and restarting script..."
2017-06-29 05:22:51 +08:00
cp ${ ACME_BASE } /acme/fullchain.pem ${ ACME_BASE } /cert.pem
cp ${ ACME_BASE } /acme/private/privkey.pem ${ ACME_BASE } /key.pem
2017-06-29 16:25:32 +08:00
exec env TRIGGER_RESTART = 1 $( readlink -f " $0 " )
2017-06-29 05:22:51 +08:00
fi
2017-06-29 06:56:51 +08:00
ISSUER = "mailcow"
2017-06-21 02:06:54 +08:00
else
2017-06-29 06:56:51 +08:00
echo "Restoring mailcow snake-oil certificates and restarting script..."
2017-06-21 02:06:54 +08:00
cp ${ SSL_EXAMPLE } /cert.pem ${ ACME_BASE } /cert.pem
cp ${ SSL_EXAMPLE } /key.pem ${ ACME_BASE } /key.pem
2017-06-29 16:25:32 +08:00
exec env TRIGGER_RESTART = 1 $( readlink -f " $0 " )
2017-06-21 02:06:54 +08:00
fi
2017-06-17 16:08:12 +08:00
fi
2017-06-12 16:45:12 +08:00
while true; do
2017-06-23 14:33:07 +08:00
if [ [ " ${ SKIP_LETS_ENCRYPT } " = ~ ^( [ yY] [ eE] [ sS] | [ yY] ) +$ ] ] ; then
echo "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
2017-09-16 19:17:48 +08:00
sleep 3650d
exec $( readlink -f " $0 " )
2017-06-23 14:33:07 +08:00
fi
2017-07-03 02:18:22 +08:00
if [ [ " ${ SKIP_IP_CHECK } " = ~ ^( [ yY] [ eE] [ sS] | [ yY] ) +$ ] ] ; then
SKIP_IP_CHECK = y
fi
2017-07-03 16:20:09 +08:00
unset SQL_DOMAIN_ARR
unset VALIDATED_CONFIG_DOMAINS
unset ADDITIONAL_VALIDATED_SAN
2017-06-14 05:37:48 +08:00
declare -a SQL_DOMAIN_ARR
2017-06-29 05:22:51 +08:00
declare -a VALIDATED_CONFIG_DOMAINS
2017-06-14 13:24:32 +08:00
declare -a ADDITIONAL_VALIDATED_SAN
2017-06-28 16:50:51 +08:00
IFS = ',' read -r -a ADDITIONAL_SAN_ARR <<< " ${ ADDITIONAL_SAN } "
2017-09-12 03:51:17 +08:00
IPV4 = $( get_ipv4)
2017-06-29 16:25:32 +08:00
# Container ids may have changed
2017-07-01 02:29:55 +08:00
CONTAINERS_RESTART = ( $( curl --silent --unix-socket /var/run/docker.sock http/containers/json | jq -rc 'map(select(.Names[] | contains ("nginx-mailcow") or contains ("postfix-mailcow") or contains ("dovecot-mailcow"))) | .[] .Id' | tr "\n" " " ) )
2017-06-14 05:37:48 +08:00
2017-07-13 18:51:52 +08:00
while read domain; do
SQL_DOMAIN_ARR += ( " ${ domain } " )
2017-08-18 15:57:25 +08:00
done < <( mysql -h mysql-mailcow -u ${ DBUSER } -p${ DBPASS } ${ DBNAME } -e "SELECT domain FROM domain WHERE backupmx=0" -Bs)
2017-09-09 00:41:02 +08:00
while read alias_domain; do
SQL_DOMAIN_ARR += ( " ${ alias_domain } " )
done < <( mysql -h mysql-mailcow -u ${ DBUSER } -p${ DBPASS } ${ DBNAME } -e "SELECT alias_domain FROM alias_domain" -Bs)
2017-06-14 05:37:48 +08:00
for SQL_DOMAIN in " ${ SQL_DOMAIN_ARR [@] } " ; do
2017-06-23 02:34:54 +08:00
A_CONFIG = $( dig A autoconfig.${ SQL_DOMAIN } +short | tail -n 1)
2017-06-14 05:37:48 +08:00
if [ [ ! -z ${ A_CONFIG } ] ] ; then
echo " Found A record for autoconfig. ${ SQL_DOMAIN } : ${ A_CONFIG } "
2017-07-03 02:18:22 +08:00
if [ [ ${ IPV4 :- ERR } = = ${ A_CONFIG } ] ] || [ [ ${ SKIP_IP_CHECK } = = "y" ] ] ; then
2017-06-14 05:37:48 +08:00
echo " Confirmed A record autoconfig. ${ SQL_DOMAIN } "
2017-06-21 02:06:54 +08:00
VALIDATED_CONFIG_DOMAINS += ( " autoconfig. ${ SQL_DOMAIN } " )
2017-06-14 05:37:48 +08:00
else
2017-06-30 03:22:01 +08:00
echo " Cannot match your IP ${ IPV4 } against hostname autoconfig. ${ SQL_DOMAIN } ( ${ A_CONFIG } ) "
2017-06-14 05:37:48 +08:00
fi
else
echo " No A record for autoconfig. ${ SQL_DOMAIN } found "
2017-06-12 16:45:12 +08:00
fi
2017-06-23 02:34:54 +08:00
A_DISCOVER = $( dig A autodiscover.${ SQL_DOMAIN } +short | tail -n 1)
2017-06-14 05:37:48 +08:00
if [ [ ! -z ${ A_DISCOVER } ] ] ; then
2017-06-29 05:22:51 +08:00
echo " Found A record for autodiscover. ${ SQL_DOMAIN } : ${ A_DISCOVER } "
2017-07-03 02:18:22 +08:00
if [ [ ${ IPV4 :- ERR } = = ${ A_DISCOVER } ] ] || [ [ ${ SKIP_IP_CHECK } = = "y" ] ] ; then
2017-06-14 05:37:48 +08:00
echo " Confirmed A record autodiscover. ${ SQL_DOMAIN } "
2017-06-21 02:06:54 +08:00
VALIDATED_CONFIG_DOMAINS += ( " autodiscover. ${ SQL_DOMAIN } " )
2017-06-14 05:37:48 +08:00
else
2017-06-30 03:22:01 +08:00
echo " Cannot match your IP ${ IPV4 } against hostname autodiscover. ${ SQL_DOMAIN } ( ${ A_DISCOVER } ) "
2017-06-14 05:37:48 +08:00
fi
else
echo " No A record for autodiscover. ${ SQL_DOMAIN } found "
2017-06-12 16:45:12 +08:00
fi
2017-06-14 05:37:48 +08:00
done
2017-06-12 16:45:12 +08:00
2017-06-29 05:22:51 +08:00
A_MAILCOW_HOSTNAME = $( dig A ${ MAILCOW_HOSTNAME } +short | tail -n 1)
if [ [ ! -z ${ A_MAILCOW_HOSTNAME } ] ] ; then
echo " Found A record for ${ MAILCOW_HOSTNAME } : ${ A_MAILCOW_HOSTNAME } "
2017-07-03 02:18:22 +08:00
if [ [ ${ IPV4 :- ERR } = = ${ A_MAILCOW_HOSTNAME } ] ] || [ [ ${ SKIP_IP_CHECK } = = "y" ] ] ; then
2017-06-29 05:22:51 +08:00
echo " Confirmed A record ${ MAILCOW_HOSTNAME } "
VALIDATED_MAILCOW_HOSTNAME = ${ MAILCOW_HOSTNAME }
else
2017-06-30 03:22:01 +08:00
echo " Cannot match your IP ${ IPV4 } against hostname ${ MAILCOW_HOSTNAME } ( ${ A_MAILCOW_HOSTNAME } ) "
2017-06-29 05:22:51 +08:00
fi
else
echo " No A record for ${ MAILCOW_HOSTNAME } found "
fi
2017-06-14 13:24:32 +08:00
for SAN in " ${ ADDITIONAL_SAN_ARR [@] } " ; do
2017-09-04 01:41:47 +08:00
if [ [ ${ SAN } = = ${ MAILCOW_HOSTNAME } ] ] ; then
continue
fi
2017-06-23 02:34:54 +08:00
A_SAN = $( dig A ${ SAN } +short | tail -n 1)
2017-06-14 13:24:32 +08:00
if [ [ ! -z ${ A_SAN } ] ] ; then
echo " Found A record for ${ SAN } : ${ A_SAN } "
2017-07-03 02:18:22 +08:00
if [ [ ${ IPV4 :- ERR } = = ${ A_SAN } ] ] || [ [ ${ SKIP_IP_CHECK } = = "y" ] ] ; then
2017-06-14 13:24:32 +08:00
echo " Confirmed A record ${ SAN } "
ADDITIONAL_VALIDATED_SAN += ( " ${ SAN } " )
else
2017-06-29 05:22:51 +08:00
echo " Cannot match your IP against hostname ${ SAN } "
2017-06-14 13:24:32 +08:00
fi
else
echo " No A record for ${ SAN } found "
fi
done
2017-07-05 03:32:58 +08:00
# Unique elements
2017-09-04 01:41:47 +08:00
ALL_VALIDATED = ( ${ VALIDATED_MAILCOW_HOSTNAME } $( echo ${ VALIDATED_CONFIG_DOMAINS [*] } ${ ADDITIONAL_VALIDATED_SAN [*] } | xargs -n1 | sort -u | xargs) )
2017-06-29 05:22:51 +08:00
if [ [ -z ${ ALL_VALIDATED [*] } ] ] ; then
2017-09-16 19:17:48 +08:00
echo "Cannot validate hostnames, skipping Let's Encrypt for 1 hour."
echo "Use SKIP_LETS_ENCRYPT=y in mailcow.conf to skip it permanently."
sleep 1h
exec $( readlink -f " $0 " )
2017-06-29 05:22:51 +08:00
fi
2017-07-21 17:03:35 +08:00
ORPHANED_SAN = ( $( echo ${ SAN_ARRAY_NOW [*] } ${ ALL_VALIDATED [*] } | tr ' ' '\n' | sort | uniq -u ) )
2017-06-29 06:56:51 +08:00
if [ [ ! -z ${ ORPHANED_SAN [*] } ] ] && [ [ ${ ISSUER } != *"mailcow" * ] ] ; then
2017-06-21 02:06:54 +08:00
DATE = $( date +%Y-%m-%d_%H_%M_%S)
2017-06-29 16:25:32 +08:00
echo " Found orphaned SAN ${ ORPHANED_SAN [*] } in certificate, moving old files to ${ ACME_BASE } /acme/private/ ${ DATE } .bak/, keeping key file... "
2017-06-23 14:33:07 +08:00
mkdir -p ${ ACME_BASE } /acme/private/${ DATE } .bak/
[ [ -f ${ ACME_BASE } /acme/private/account.key ] ] && mv ${ ACME_BASE } /acme/private/account.key ${ ACME_BASE } /acme/private/${ DATE } .bak/
2017-07-21 17:03:35 +08:00
[ [ -f ${ ACME_BASE } /acme/fullchain.pem ] ] && mv ${ ACME_BASE } /acme/fullchain.pem ${ ACME_BASE } /acme/private/${ DATE } .bak/
[ [ -f ${ ACME_BASE } /acme/cert.pem ] ] && mv ${ ACME_BASE } /acme/cert.pem ${ ACME_BASE } /acme/private/${ DATE } .bak/
2017-06-23 14:33:07 +08:00
cp ${ ACME_BASE } /acme/private/privkey.pem ${ ACME_BASE } /acme/private/${ DATE } .bak/ # Keep key for TLSA 3 1 1 records
2017-06-21 02:06:54 +08:00
fi
2017-06-12 16:45:12 +08:00
acme-client \
2017-06-14 05:37:48 +08:00
-v -e -b -N -n \
2017-06-12 16:45:12 +08:00
-f ${ ACME_BASE } /acme/private/account.key \
-k ${ ACME_BASE } /acme/private/privkey.pem \
-c ${ ACME_BASE } /acme \
2017-07-05 03:32:58 +08:00
${ ALL_VALIDATED [*] }
2017-06-12 16:45:12 +08:00
case " $? " in
0) # new certs
# cp the new certificates and keys
cp ${ ACME_BASE } /acme/fullchain.pem ${ ACME_BASE } /cert.pem
cp ${ ACME_BASE } /acme/private/privkey.pem ${ ACME_BASE } /key.pem
# restart docker containers
2017-06-29 05:22:51 +08:00
if ! verify_hash_match ${ ACME_BASE } /cert.pem ${ ACME_BASE } /key.pem; then
2017-06-29 16:25:32 +08:00
echo "Certificate was successfully requested, but key and certificate have non-matching hashes, restoring mailcow snake-oil and restarting containers..."
2017-06-29 05:22:51 +08:00
cp ${ SSL_EXAMPLE } /cert.pem ${ ACME_BASE } /cert.pem
cp ${ SSL_EXAMPLE } /key.pem ${ ACME_BASE } /key.pem
fi
2017-06-29 16:25:32 +08:00
restart_containers ${ CONTAINERS_RESTART [*] }
2017-06-12 16:45:12 +08:00
; ;
1) # failure
2017-06-29 06:56:51 +08:00
if [ [ -f ${ ACME_BASE } /acme/private/${ DATE } .bak/fullchain.pem ] ] && [ [ -f ${ ACME_BASE } /acme/private/${ DATE } .bak/privkey.pem ] ] ; then
echo "Error requesting certificate, restoring previous certificate from backup and restarting containers...."
cp ${ ACME_BASE } /acme/private/${ DATE } .bak/fullchain.pem ${ ACME_BASE } /cert.pem
cp ${ ACME_BASE } /acme/private/${ DATE } .bak/privkey.pem ${ ACME_BASE } /key.pem
TRIGGER_RESTART = 1
2017-06-29 16:25:32 +08:00
elif [ [ -f ${ ACME_BASE } /acme/fullchain.pem ] ] && [ [ -f ${ ACME_BASE } /acme/private/privkey.pem ] ] ; then
2017-06-29 06:56:51 +08:00
echo "Error requesting certificate, restoring from previous acme request and restarting containers..."
cp ${ ACME_BASE } /acme/fullchain.pem ${ ACME_BASE } /cert.pem
cp ${ ACME_BASE } /acme/private/privkey.pem ${ ACME_BASE } /key.pem
TRIGGER_RESTART = 1
fi
2017-06-29 05:22:51 +08:00
if ! verify_hash_match ${ ACME_BASE } /cert.pem ${ ACME_BASE } /key.pem; then
2017-06-29 06:56:51 +08:00
echo "Error verifying certificates, restoring mailcow snake-oil and restarting containers..."
2017-06-29 05:22:51 +08:00
cp ${ SSL_EXAMPLE } /cert.pem ${ ACME_BASE } /cert.pem
cp ${ SSL_EXAMPLE } /key.pem ${ ACME_BASE } /key.pem
2017-06-29 06:56:51 +08:00
TRIGGER_RESTART = 1
2017-06-29 05:22:51 +08:00
fi
2017-06-29 16:25:32 +08:00
[ [ ${ TRIGGER_RESTART } = = 1 ] ] && restart_containers ${ CONTAINERS_RESTART [*] }
2017-09-16 19:17:48 +08:00
echo "Retrying in 30 minutes..."
sleep 30m
exec $( readlink -f " $0 " )
; ;
2017-06-12 16:45:12 +08:00
2) # no change
2017-06-29 06:56:51 +08:00
if ! diff ${ ACME_BASE } /acme/fullchain.pem ${ ACME_BASE } /cert.pem; then
echo "Certificate was not changed, but active certificate does not match the verified certificate, fixing and restarting containers..."
cp ${ ACME_BASE } /acme/fullchain.pem ${ ACME_BASE } /cert.pem
cp ${ ACME_BASE } /acme/private/privkey.pem ${ ACME_BASE } /key.pem
2017-06-29 16:25:32 +08:00
restart_containers ${ CONTAINERS_RESTART [*] }
2017-06-29 06:56:51 +08:00
fi
2017-06-29 05:22:51 +08:00
if ! verify_hash_match ${ ACME_BASE } /cert.pem ${ ACME_BASE } /key.pem; then
2017-06-29 06:56:51 +08:00
echo "Certificate was not changed, but hashes do not match, restoring from previous acme request and restarting containers..."
cp ${ ACME_BASE } /acme/fullchain.pem ${ ACME_BASE } /cert.pem
cp ${ ACME_BASE } /acme/private/privkey.pem ${ ACME_BASE } /key.pem
2017-06-29 16:25:32 +08:00
restart_containers ${ CONTAINERS_RESTART [*] }
2017-06-29 05:22:51 +08:00
fi
2017-06-12 16:45:12 +08:00
; ;
*) # unspecified
2017-06-29 06:56:51 +08:00
if [ [ -f ${ ACME_BASE } /acme/private/${ DATE } .bak/fullchain.pem ] ] && [ [ -f ${ ACME_BASE } /acme/private/${ DATE } .bak/privkey.pem ] ] ; then
echo "Error requesting certificate, restoring previous certificate from backup and restarting containers...."
cp ${ ACME_BASE } /acme/private/${ DATE } .bak/fullchain.pem ${ ACME_BASE } /cert.pem
cp ${ ACME_BASE } /acme/private/${ DATE } .bak/privkey.pem ${ ACME_BASE } /key.pem
TRIGGER_RESTART = 1
2017-06-29 16:25:32 +08:00
elif [ [ -f ${ ACME_BASE } /acme/fullchain.pem ] ] && [ [ -f ${ ACME_BASE } /acme/private/privkey.pem ] ] ; then
2017-06-29 06:56:51 +08:00
echo "Error requesting certificate, restoring from previous acme request and restarting containers..."
cp ${ ACME_BASE } /acme/fullchain.pem ${ ACME_BASE } /cert.pem
cp ${ ACME_BASE } /acme/private/privkey.pem ${ ACME_BASE } /key.pem
TRIGGER_RESTART = 1
fi
if ! verify_hash_match ${ ACME_BASE } /cert.pem ${ ACME_BASE } /key.pem; then
echo "Error verifying certificates, restoring mailcow snake-oil..."
2017-06-29 05:22:51 +08:00
cp ${ SSL_EXAMPLE } /cert.pem ${ ACME_BASE } /cert.pem
cp ${ SSL_EXAMPLE } /key.pem ${ ACME_BASE } /key.pem
2017-06-29 06:56:51 +08:00
TRIGGER_RESTART = 1
2017-06-29 05:22:51 +08:00
fi
2017-06-29 16:25:32 +08:00
[ [ ${ TRIGGER_RESTART } = = 1 ] ] && restart_containers ${ CONTAINERS_RESTART [*] }
2017-09-16 19:17:48 +08:00
sleep 3650d
; ;
2017-06-12 16:45:12 +08:00
esac
echo "ACME certificate validation done. Sleeping for another day."
2017-09-16 19:17:48 +08:00
sleep 1d
2017-06-12 16:45:12 +08:00
done