Support letsencrypt proxy validation via CNAMEs

Allows to point other domains' `_acme-challenge.example.com` entries at
`example.com.letsencrypt.kosmos.chat` so we can validate from our side
without access to the other domain's DNS records.

Used for 5apps.com XMPP for now. Can be used for others later.

Co-authored-by: Greg Karékinian <greg@karekinian.com>
This commit is contained in:
Râu Cao 2024-03-11 16:15:12 +01:00
parent 21de964e1b
commit 4a8ab3abe3
Signed by: raucao
GPG Key ID: 37036C356E56CC51
3 changed files with 48 additions and 24 deletions

View File

@ -1,9 +1,17 @@
{ {
"id": "gandi_api_5apps", "id": "gandi_api_5apps",
"key": { "key": {
"encrypted_data": "+tcD9x5MkNpf2Za5iLM7oTGrmAXxuWFEbyg4xrcWypSkSTjdIncOfD1UoIoS\nGzy1\n", "encrypted_data": "AGYIkLdbnU3+O6OxGsFyLpZtTw531s2dbRC4Lik+8NYp3l4P0UMM2Pqf0g==\n",
"iv": "ymls2idI/PdiRZCgsulwrA==\n", "iv": "kPRHGpLwNIC3MpES\n",
"version": 1, "auth_tag": "wKth2tA+JxILFIKppHLDJg==\n",
"cipher": "aes-256-cbc" "version": 3,
"cipher": "aes-256-gcm"
},
"access_token": {
"encrypted_data": "+tKKFcWV0CZ5wEB/No5hou5+p1llsUkq7AXBvfnA7xsgbpa2q8AX/2UFf9Cf\nGtd9om1CeJJtz+o4ceA=\n",
"iv": "hLJSV77DQtqXZDbV\n",
"auth_tag": "8xgyudyDk4hq16LRkykGhQ==\n",
"version": 3,
"cipher": "aes-256-gcm"
} }
} }

View File

@ -33,11 +33,11 @@ file "/etc/letsencrypt/renewal-hooks/post/ejabberd" do
group "root" group "root"
end end
gandi_api_data_bag_item = data_bag_item('credentials', 'gandi_api_5apps') gandi_api_credentials = data_bag_item('credentials', 'gandi_api_5apps')
template "/root/gandi_dns_certbot_hook.sh" do template "/root/gandi_dns_certbot_hook.sh" do
variables gandi_api_key: gandi_api_data_bag_item["key"] variables access_token: gandi_api_credentials["access_token"]
mode 0770 mode 0700
end end
# Generate a Let's Encrypt cert (only if no cert has been generated before). # Generate a Let's Encrypt cert (only if no cert has been generated before).
@ -52,7 +52,7 @@ end
# Generate a Let's Encrypt cert (only if no cert has been generated before). # Generate a Let's Encrypt cert (only if no cert has been generated before).
# The systemd timer will take care of renewing # The systemd timer will take care of renewing
execute "letsencrypt cert for 5apps xmpp" do execute "letsencrypt cert for 5apps xmpp" do
command "certbot certonly --manual --preferred-challenges dns --manual-public-ip-logging-ok --agree-tos --manual-auth-hook \"/root/gandi_dns_certbot_hook.sh auth\" --manual-cleanup-hook \"/root/gandi_dns_certbot_hook.sh cleanup\" --deploy-hook \"/etc/letsencrypt/renewal-hooks/post/ejabberd\" --email ops@5apps.com -d 5apps.com -d muc.5apps.com -d xmpp.5apps.com -d uploads.xmpp.5apps.com -n" command "certbot certonly --manual --preferred-challenges dns --manual-public-ip-logging-ok --agree-tos --manual-auth-hook \"/root/gandi_dns_certbot_hook.sh auth letsencrypt.kosmos.chat\" --manual-cleanup-hook \"/root/gandi_dns_certbot_hook.sh cleanup letsencrypt.kosmos.chat\" --deploy-hook \"/etc/letsencrypt/renewal-hooks/post/ejabberd\" --email ops@5apps.com -d 5apps.com -d muc.5apps.com -d xmpp.5apps.com -d uploads.xmpp.5apps.com -n"
not_if do not_if do
File.exist?("/etc/letsencrypt/live/5apps.com/fullchain.pem") File.exist?("/etc/letsencrypt/live/5apps.com/fullchain.pem")
end end

View File

@ -1,6 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#
set -euf -o pipefail set -euf -o pipefail
# ************** USAGE ************** # ************** USAGE **************
@ -25,10 +23,12 @@ set -euf -o pipefail
# #
# Defaults to 30 seconds. # Defaults to 30 seconds.
# #
GANDI_API_KEY="<%= @gandi_api_key %>" ACCESS_TOKEN="<%= @access_token %>"
PROVIDER_UPDATE_DELAY=30 PROVIDER_UPDATE_DELAY=10
VALIDATION_DOMAIN="${2:-}"
regex='.*\.(.*\..*)' regex='.*\.(.*\..*)'
if [[ $CERTBOT_DOMAIN =~ $regex ]] if [[ $CERTBOT_DOMAIN =~ $regex ]]
then then
DOMAIN="${BASH_REMATCH[1]}" DOMAIN="${BASH_REMATCH[1]}"
@ -36,25 +36,41 @@ else
DOMAIN="${CERTBOT_DOMAIN}" DOMAIN="${CERTBOT_DOMAIN}"
fi fi
if [[ -n "$VALIDATION_DOMAIN" ]]
then
if [[ $VALIDATION_DOMAIN =~ $regex ]]
then
ACME_BASE_DOMAIN="${BASH_REMATCH[1]}"
else
echo "Validation domain has to be a subdomain, but it is not: \"${VALIDATION_DOMAIN}\""
exit 1
fi
ACME_DOMAIN="${CERTBOT_DOMAIN}.${VALIDATION_DOMAIN}"
else
ACME_BASE_DOMAIN="${DOMAIN}"
ACME_DOMAIN="_acme-challenge.${CERTBOT_DOMAIN}"
fi
# To be invoked via Certbot's --manual-auth-hook # To be invoked via Certbot's --manual-auth-hook
function auth { function auth {
curl -s -D- -H "Content-Type: application/json" \ curl -s -D- \
-H "X-Api-Key: ${GANDI_API_KEY}" \ -H "Content-Type: application/json" \
-d "{\"rrset_name\": \"_acme-challenge.${CERTBOT_DOMAIN}.\", -H "Authorization: Bearer ${ACCESS_TOKEN}" \
\"rrset_type\": \"TXT\", -d "{\"rrset_name\": \"${ACME_DOMAIN}.\",
\"rrset_ttl\": 3600, \"rrset_type\": \"TXT\",
\"rrset_values\": [\"${CERTBOT_VALIDATION}\"]}" \ \"rrset_ttl\": 300,
"https://dns.api.gandi.net/api/v5/domains/${DOMAIN}/records" \"rrset_values\": [\"${CERTBOT_VALIDATION}\"]}" \
"https://api.gandi.net/v5/livedns/domains/${ACME_BASE_DOMAIN}/records"
sleep ${PROVIDER_UPDATE_DELAY}
sleep ${PROVIDER_UPDATE_DELAY}
} }
# To be invoked via Certbot's --manual-cleanup-hook # To be invoked via Certbot's --manual-cleanup-hook
function cleanup { function cleanup {
curl -s -X DELETE -H "Content-Type: application/json" \ curl -s -X DELETE \
-H "X-Api-Key: ${GANDI_API_KEY}" \ -H "Content-Type: application/json" \
https://dns.api.gandi.net/api/v5/domains/${DOMAIN}/records/_acme-challenge.${CERTBOT_DOMAIN}./TXT -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"https://api.gandi.net/v5/livedns/domains/${ACME_BASE_DOMAIN}/records/${ACME_DOMAIN}./TXT"
} }
HANDLER=$1; shift; HANDLER=$1; shift;