Letsencrypt app - wildcards supported?

Since, I really love wildcard certificates, I managed to find a way to implement them for UCS.
The script is based on an ACME-DNS challenge and consequently requires your UCS server to act as a public DNS. If this is not the case, this solution will not work for you.

Furthermore, make sure you replace example with your domain. :wink:

Please feel free to share this post or make a cool solution out of this. Credits appreciated. :slight_smile:

#-----------------------------------------------
#-----------------------------------------------
#-----------------------------------------------
#---- Install and configure dehydrated

univention-install -yy dehydrated
echo -e 'example.com > example_com
*.lyscope.com > wildcard_example_com' > /etc/dehydrated/domains.txt

echo 'CONFIG_D=/etc/dehydrated/conf.d
BASEDIR=/var/lib/dehydrated
WELLKNOWN="${BASEDIR}/acme-challenges"
DOMAINS_TXT="/etc/dehydrated/domains.txt"
CONTACT_EMAIL="root@example.com"' > /etc/dehydrated/config
echo -e '
#!/bin/bash

# letsencrypt.sh dns-01 challenge RFC2136 hook.

# TTL - DNS Time-To-Live of ACME TXT record
[ -z "${TTL}" ] && TTL=10

# DESTINATION - Copy files to subdirectory of DESTINATION upon successful certificate request
[ -z "${DESTINATION}" ] && DESTINATION=

# CERT_OWNER - If DESTINATION and CERT_OWNER are set, chown files to CERT_OWNER after copy
[ -z "${CERT_OWNER}" ] && CERT_OWNER=

# CERT_GROUP - If DESTINATION, CERT_OWNER and CERT_GROUP are set, chown files to CERT_GROUP after copy
[ -z "${CERT_GROUP}" ] && CERT_GROUP=

# CERT_MODE - If DESTINATION and CERT_MODE are set, chmod files to CERT_MODE after copy
[ -z "${CERT_MODE}" ] && CERT_MODE=

# CERTDIR_OWNER - If DESTINATION and CERTDIR_OWNER are set, chown files to CERTDIR_OWNER after copy
[ -z "${CERTDIR_OWNER}" ] && CERTDIR_OWNER=

# CERTDIR_GROUP - If DESTINATION, CERTDIR_OWNER and CERTDIR_GROUP are set, chown files to CERTDIR_GROUP after copy
[ -z "${CERTDIR_GROUP}" ] && CERTDIR_GROUP=

# CERTDIR_MODE - If DESTINATION and CERT_MODE are set, chmod files to CERT_MODE after copy
[ -z "${CERTDIR_MODE}" ] && CERTDIR_MODE=

# ATTEMPTS - Wait $ATTEMPTS times $SLEEP seconds for propagation to succeed, then bail out.
[ -z "${ATTEMPTS}" ] && ATTEMPTS=30

# SLEEP - Amount of seconds to sleep before retrying propagation check.
[ -z "${SLEEP}" ] && SLEEP=60

# DOMAINS_TXT - Path to the domains.txt file containing all requested certificates.
[ -z "${DOMAINS_TXT}" ] && DOMAINS_TXT="${BASEDIR}/domains.txt"

_log() {
echo >&2 " + ${@}"
}

_checkdns() {
local ATTEMPT="${1}" DOMAIN="${2}" TOKEN_VALUE="${3}"
if [ $ATTEMPT -gt $ATTEMPTS ];
then
_log "Propagation check failed after ${ATTEMPTS} attempts. Giving up!"
exit 2
fi

_log "Checking for dns propagation via Google's recursor... (${ATTEMPT}/${ATTEMPTS})"

# host -t txt _acme-challenge.${DOMAIN} 8.8.8.8 | grep ${TOKEN_VALUE} >/dev/null 2>&1
host -t txt _acme-challenge.${DOMAIN} 8.8.8.8 | grep -- ${TOKEN_VALUE} >/dev/null 2>&1 
if [ "$?" -eq 0 ];
then
host -t txt _acme-challenge.${DOMAIN} 192.174.68.104 | grep -- ${TOKEN_VALUE} >/dev/null 2>&1 
if [ "$?" -eq 0 ];
then
host -t txt _acme-challenge.${DOMAIN} 176.97.158.104 | grep -- ${TOKEN_VALUE} >/dev/null 2>&1 
if [ "$?" -eq 0 ];
then
_log "Propagation success!"
return
fi
fi 
else

_log "Waiting ${SLEEP}s..."
sleep ${SLEEP}
_checkdns $((ATTEMPT+1)) ${DOMAIN} ${TOKEN_VALUE}
fi
}

deploy_challenge() {
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"

# This hook is called once for every domain that needs to be
# validated, including any alternative names you may have listed.
#
# Parameters:
# - DOMAIN
# The domain name (CN or subject alternative name) being
# validated.
# - TOKEN_FILENAME
# The name of the file containing the token to be served for HTTP
# validation. Should be served by your web server as
# /.well-known/acme-challenge/${TOKEN_FILENAME}.
# - TOKEN_VALUE
# The token value that needs to be served for validation. For DNS
# validation, this is what you want to put in the _acme-challenge
# TXT record. For HTTP validation it is the value that is expected
# be found in the $TOKEN_FILENAME file.

_log "Adding ACME challenge record via RFC2136 update to DNS/LDAP"

if [[ ${DOMAIN} == '\*'* ]];
then
exit 66
fi

DOMAIN_ZONE="$(cut -d'.' -f1 <<<$DOMAIN)"
DOMAIN_TLD="$(cut -d'.' -f2 <<<$DOMAIN)"

udm dns/txt_record create \
--superordinate zoneName=${DOMAIN_ZONE}.${DOMAIN_TLD},cn=dns,dc=${DOMAIN_ZONE},dc=${DOMAIN_TLD} \
--set name=_acme-challenge \
--set txt=$TOKEN_VALUE \
--set zonettl=$TTL

if [ "$?" -ne 0 ];
then
_log "Failure reported by udm. Giving up!"
exit 2
fi

# Allow at least a little time to propagate to slaves before asking Google
sleep 30

_checkdns 1 ${DOMAIN} ${TOKEN_VALUE}
}

clean_challenge() {
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"

# This hook is called after attempting to validate each domain,
# whether or not validation was successful. Here you can delete
# files or DNS records that are no longer needed.
#
# The parameters are the same as for deploy_challenge.

_log "Removing ACME challenge record via RFC2136 update to DNS/LDAP"

if [[ ${DOMAIN} == '\*'* ]];
then
exit 66
fi

DOMAIN_ZONE="$(cut -d'.' -f1 <<<$DOMAIN)"
DOMAIN_TLD="$(cut -d'.' -f2 <<<$DOMAIN)"

udm dns/txt_record remove \
--dn relativeDomainName=_acme-challenge,zoneName=${DOMAIN_ZONE}.${DOMAIN_TLD},cn=dns,dc=${DOMAIN_ZONE},dc=${DOMAIN_TLD}

if [ "$?" -ne 0 ];
then
_log "Failure reported by udm. Giving up!"
exit 2
fi
}

deploy_cert() {
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"

# This hook is called once for each certificate that has been
# produced. Here you might, for instance, copy your new certificates
# to service-specific locations and reload the service.
#
# Parameters:
# - DOMAIN
# The primary domain name, i.e. the certificate common
# name (CN).
# - KEYFILE
# The path of the file containing the private key.
# - CERTFILE
# The path of the file containing the signed certificate.
# - FULLCHAINFILE
# The path of the file containing the full certificate chain.
# - CHAINFILE
# The path of the file containing the intermediate certificate(s).
# - TIMESTAMP
# Timestamp when the specified certificate was created.

# Simple example: Copy file to nginx config
# cp "${KEYFILE}" "${FULLCHAINFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
# systemctl reload nginx

# If destination is set, copy/chown/chmod certificate files
if [ "$DESTINATION" != "" ];
then
_log "Copying certificate files to destination repository"

mkdir -p ${DESTINATION}/${DOMAIN}
if [ "$CERTDIR_MODE" != "" ];
then
chmod ${CERTDIR_MODE} ${DESTINATION}/${DOMAIN}
fi

if [ "$CERTDIR_OWNER" != "" ];
then
chown ${CERTDIR_OWNER}:${CERTDIR_GROUP} ${DESTINATION}/${DOMAIN}
fi

if [ "$CERTDIR_MODE" != "" ];
then
chmod ${CERTDIR_MODE} ${DESTINATION}/${DOMAIN}
fi

for FILE in ${KEYFILE} ${CERTFILE} ${CHAINFILE}
do
FILENAME=$(basename $FILE)
cp ${FILE} ${DESTINATION}/${DOMAIN}

if [ "$CERT_OWNER" != "" ];
then
chown ${CERT_OWNER}:${CERT_GROUP} ${DESTINATION}/${DOMAIN}/${FILENAME}
fi

if [ "$CERT_MODE" != "" ];
then
chmod ${CERT_MODE} ${DESTINATION}/${DOMAIN}/${FILENAME}
fi
done
fi

# Add DOMAIN to domains.txt if not already there
grep ^$HOST\$ ${DOMAINS_TXT} > /dev/null 2>&1
if [ "$?" -ne 0 ];
then
echo ${DOMAIN} >> ${DOMAINS_TXT}
fi
}

unchanged_cert() {
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}"

# This hook is called once for each certificate that is still
# valid and therefore wasn't reissued.
#
# Parameters:
# - DOMAIN
# The primary domain name, i.e. the certificate common
# name (CN).
# - KEYFILE
# The path of the file containing the private key.
# - CERTFILE
# The path of the file containing the signed certificate.
# - FULLCHAINFILE
# The path of the file containing the full certificate chain.
# - CHAINFILE
# The path of the file containing the intermediate certificate(s).


# NOOP
}

invalid_challenge() {
# This hook is called at the beginning of a dehydrated command 
local DOMAIN="${1}" RESPONSE="${2}"

# This hook is called if the challenge response has failed, so domain
# owners can be aware and act accordingly.
#
# Parameters:
# - DOMAIN
# The primary domain name, i.e. the certificate common
# name (CN).
# - RESPONSE
# The response that the verification server returned

# Simple example: Send mail to root
printf "Subject: Validation of ${DOMAIN} failed!\n\nletsencrypt certificate could not be retrieved!" \
| sendmail root
:
}

request_failure() {
local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}" HEADERS="${4}"

# This hook is called when an HTTP request fails (e.g., when the ACME
# server is busy, returns an error, etc). It will be called upon any
# response code that does not start with '2'. Useful to alert admins
# about problems with requests.
#
# Parameters:
# - STATUSCODE
# The HTML status code that originated the error.
# - REASON
# The specified reason for the error.
# - REQTYPE
# The kind of request that was made (GET, POST...)

# Simple example: Send mail to root
printf "Subject: HTTP request failed failed!\n\nA http request failed with status ${STATUSCODE}!" \
| sendmail root
}

generate_csr() {
local DOMAIN="${1}" CERTDIR="${2}" ALTNAMES="${3}"

# This hook is called before any certificate signing operation takes place.
# It can be used to generate or fetch a certificate signing request with external
# tools.
# The output should be just the cerificate signing request formatted as PEM.
#
# Parameters:
# - DOMAIN
# The primary domain as specified in domains.txt. This does not need to
# match with the domains in the CSR, it's basically just the directory name.
# - CERTDIR
# Certificate output directory for this particular certificate. Can be used
# for storing additional files.
# - ALTNAMES
# All domain names for the current certificate as specified in domains.txt.
# Again, this doesn't need to match with the CSR, it's just there for convenience.

# Simple example: Look for pre-generated CSRs
# if [ -e "${CERTDIR}/pre-generated.csr" ]; then
# cat "${CERTDIR}/pre-generated.csr"
# fi
}


startup_hook() {
# This hook is called at the beginning of a dehydrated command

:
}

exit_hook() {
# This hook is called at the end of a dehydrated command and can be used
# to do some final (cleanup or other) tasks.

service apache2 reload
if [ "$?" -ne 0 ];
then
_log "Failed to reload apache2."
echo "Failed to reload apache2." | sendmail root
exit 2
fi
service postfix reload
if [ "$?" -ne 0 ];
then
_log "Failed to reload postfix."
echo "Failed to reload postfix." | sendmail root
exit 2
fi
service dovecot reload
if [ "$?" -ne 0 ];
then
_log "Failed to reload dovecot."
echo "Failed to reload dovecot." | sendmail root
exit 2
fi

:
}


HANDLER="$1"; shift
if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|unchanged_cert|invalid_challenge|request_failure|generate_csr|startup_hook|exit_hook)$ ]]; then
"$HANDLER" "$@"
fi
' > /etc/dehydrated/hook.sh

chmod 700 /etc/dehydrated/hook.sh
dehydrated --register --accept-terms
dehydrated -c -k /etc/dehydrated/hook.sh -t dns-01

echo -e '#!/bin/bash
dehydrated -c -k /etc/dehydrated/hook.sh -t dns-01' > /etc/cron.daily/dehydrated

#-----------------------------------------------
#-----------------------------------------------
#-----------------------------------------------
#---- Use letsencrypt

#https://help.univention.com/t/using-your-own-ssl-certificates/38

#apache
ucr set apache2/ssl/certificate="/var/lib/dehydrated/certs/wildcard_lyscope_com/cert.pem"
ucr set apache2/ssl/key="/var/lib/dehydrated/certs/wildcard_lyscope_com/privkey.pem"
ucr set saml/apache2/ssl/certificate="/var/lib/dehydrated/certs/wildcard_lyscope_com/cert.pem"
ucr set saml/apache2/ssl/key="/var/lib/dehydrated/certs/wildcard_lyscope_com/privkey.pem"
service apache2 restart

#dovecot
ucr set mail/dovecot/ssl/certificate="/var/lib/dehydrated/certs/wildcard_lyscope_com/cert.pem"
ucr set mail/dovecot/ssl/key="/var/lib/dehydrated/certs/wildcard_lyscope_com/privkey.pem"
service dovecot restart

#postfix
ucr set mail/postfix/ssl/certificate="/var/lib/dehydrated/certs/wildcard_lyscope_com/cert.pem"
ucr set mail/postfix/ssl/key="/var/lib/dehydrated/certs/wildcard_lyscope_com/privkey.pem"
service postfix restart