File: //usr/local/directadmin/scripts/letsencrypt.sh
#!/bin/bash
export EXEC_PROPAGATION_TIMEOUT=300
export EXEC_POLLING_INTERVAL=30
if [ "$(id -u)" != "0" ]; then
echo "this script can only run as root";
exit 1
fi
if [ ! -x /usr/local/bin/lego ]; then
echo "missing 'lego' command, it can be installed using CustomBuild with command:"
echo " da build lego"
exit 1
fi
update_conf_file() {
local file=$1 # .../data/users/jonas/domains/example.com.conf
local key=$2 # acme_enabled
local val=$3 # yes
if [ ! -f "${file}" ]; then
echo "no such file: ${file}" 1>&2
exit 1
fi
local key_escaped val_escaped
key_escaped=$(sed 's/[.[\*^\/&$]/\\&/g' <<< "${key}")
val_escaped=$(sed 's/[.[\*^\/&$]/\\&/g' <<< "${val}")
if grep -q "^${key_escaped}=" "${file}"; then
sed -i -e "s/^${key_escaped}=.*$/${key_escaped}=${val_escaped}/" "${file}"
else
echo "${key}=${val}" >> "${file}"
fi
}
lego_key_type() {
local key_type=$1
case "${key_type}" in
# Identity mappings
ec256|ec384|rsa2048|rsa4096|rsa8192) echo "${key_type}";;
# Old DA key-types
prime256v1) echo "ec256";;
secp384r1) echo "ec384";;
2048) echo "rsa2048";;
4096) echo "rsa4096";;
8192) echo "rsa8192";;
# Default
*) echo "ec256" ;;
esac
}
command_server_cert() {
local domain_csv=$1
local key_type=$2
da config-set acme_server_cert_enabled "1"
if [ -n "${domain_csv}" ]; then
ADDITIONAL_DOMAINS="$(tr , '\n' <<< "${domain_csv}" | grep -Fvx "$(da config-get servername)" | paste -sd,)"
da config-set acme_server_cert_additional_domains "${ADDITIONAL_DOMAINS}"
fi
if [ -n "${key_type}" ]; then
da config-set acme_server_cert_key_type "$(lego_key_type "${key_type}")"
fi
if [ -s "/usr/local/directadmin/conf/ca.dnsprovider" ]; then
da config-set acme_server_cert_dns_provider_env_file "/usr/local/directadmin/conf/ca.dnsprovider"
fi
if ! da taskq --run 'action=ssl&value=server_acme&force=true'; then
echo "Failed to issue new certificate"
exit 1
fi
echo "Server certificate with domains ${domain_csv} has been created successfully"
da config-set ssl 1
if systemctl --quiet is-active directadmin.service; then
systemctl restart directadmin.service
fi
}
command_domain_cert() {
local dns_name=$1
local key_size=$2
local domain
if ! domain=$(find_domain "${dns_name}"); then
echo "unable to find '${dns_name}' domain owner" 1>&2
echo "make sure this DNS name is owned by some user" 1>&2
exit 1
fi
local username
username=$(sed -n -e 's/^'"${domain//./\\.}"':\s*\(.*\)/\1/p' /etc/virtual/domainowners)
if [ -z "${username}" ]; then
echo "unable to find '${domain}' owner" 1>&2
exit 1
fi
# Enable ACME for the domain
local domain_conf="/usr/local/directadmin/data/users/${username}/domains/${domain}.conf"
# convert alias/pointer domain to main domain
if [ ! -f "${domain_conf}" ]; then
local f
for f in "/usr/local/directadmin/data/users/${username}/domains/"*.pointers; do
if [ -f "${f}" ] && grep -q "^${domain//./\\.}=" "${f}"; then
domain_conf="${f%%.pointers}.conf"
f="${f//*\/}"
domain="${f%%.pointers}"
break
fi
done
fi
update_conf_file "${domain_conf}" acme_enabled yes || exit 1
if [ -n "${key_size}" ]; then
update_conf_file "${domain_conf}" acme_key_type "$(lego_key_type "${key_size}")" || exit 1
fi
if ! da taskq --debug=1 --run "action=rewrite&value=letsencrypt&domain=${domain}"; then
echo "failed issuing certificates" 1>&2
exit 1
fi
}
find_domain() {
local record=$1 # long.name.example.com
if [ -z "${record}" ]; then
return 1
fi
local domains
readarray -t domains < <(cut -d : -f 1 /etc/virtual/domainowners)
while true; do
local d
for d in "${domains[@]}"; do
if [ "${d}" = "${record}" ]; then
echo "${record}"
return
fi
done
# remove single component and try again
if [ "${record#*.}" = "${record}" ]; then
return 1
fi
record=${record#*.}
done
}
command_add_dns_challenge_record() {
local key=${1%%.} # Strip '.' suffix
local value=$2
local domain
if ! domain=$(find_domain "${key}"); then
echo "refusing to create DNS challenge record '${key}', unable to find matching domain" 1>&2
exit 1
fi
if [ "${domain}" = "${key}" ]; then
# top level entries uses full domain with leading .
key="${domain}."
else
# remove domain suffix
key="${key%%".${domain}"}"
fi
da taskq --run "action=dns&do=add&domain=${domain}&type=TXT&name=${key}&value=\"${value}\"&ttl=120&named_reload=yes"
exit 0
}
command_remove_dns_challenge_record() {
local key=${1%%.} # Strip '.' suffix
local value=$2
local domain
if ! domain=$(find_domain "${key}"); then
echo "refusing to create DNS challenge record '${key}', unable to find matching domain" 1>&2
exit 1
fi
if [ "${domain}" = "${key}" ]; then
# top level entries uses full domain with leading .
key="${domain}."
else
# remove domain suffix
key="${key%%".${domain}"}"
fi
da taskq --run "action=dns&do=delete&domain=${domain}&type=TXT&name=${key}&value=\"${value}\"&named_reload=yes"
exit 0
}
command_help() {
echo "Usage:"
echo " $0 request|renew <domain> [<key-type>]"
echo " $0 server_cert [<domain>] [<key-type>]"
echo ""
echo "Got $# args:"
echo " $0 $1 $2 $3"
echo ""
echo "Multiple comma separated domains, owned by the same user, can be used for a certificate request"
exit 0
}
case "$1" in
server_cert)
command_server_cert "${2}" "${3}"
;;
request|request_single|request_full|renew)
IFS=',' read -r -a dns_names <<< "${2}"
for name in "${dns_names[@]}"; do
command_domain_cert "${name}" "${3}"
done
;;
present)
# lego callback for DNS challenge
# ./letsencrypt.sh "present" "_acme-challenge.foo.example.com." "MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI"
command_add_dns_challenge_record "${2}" "${3}"
;;
cleanup)
# lego callback for DNS challenge
command_remove_dns_challenge_record "${2}" "${3}"
;;
*)
command_help "$@" ;;
esac