Letsencrypt app - wildcards supported?

letsencrypt has started to support wildcard certificates recently, see https://community.letsencrypt.org/t/acme-v2-production-environment-wildcards/55578
Is UCS’s letsencrypt plugin going to support wildcards too?
It’s a nice thing to have and makes things easier, if all subdomains are covered by one certificate.
Boris

Hello @bheithecker ,

wildcard certificate support is on the way. I guess within the next days.

Best regards

Hello @gulden,
any news on wildcard support for the let’s encrypt univention app?

BR
FK

Can you please confirm, that wildcard support is not implemented yet?
I have tried it on several test systems and always received the message “IndexError: list index out of range”.

If it is already implemented, I would be glad to find some tutorial for it.

Hi everybody. Was in that same situation today, and Letsencrypt choked on the wildcard. Is there some pointer to getting wildcards supported? If not, Is 10 still the maximum number of domains available?

Thanks!

Hi!

That’s my first post, so far I really enjoy UCS, thank you for the great work! :slight_smile:
Just entered *.mydomain.tld and got the following errror:
IndexError: list index out of range

Am I doing anything wrong or are wildcards not yet supported?

Best regards
pate

Wildcard certificates are not yet supported in the Let’s Encrypt app

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

I suggest to use acme.sh which supports many dns api to get wildcard certs from LE