From 9f09cc19c89066e972270c965005b54ac0ec0896 Mon Sep 17 00:00:00 2001
From: Carl Tashian <carl@smallstep.com>
Date: Tue, 11 Jan 2022 13:28:49 -0800
Subject: [PATCH] Allow custom ACME server (Draft fix of #186)

---
 readme-vars.yml                |  5 +++-
 root/app/le-renew.sh           |  5 ++++
 root/etc/cont-init.d/50-config | 55 +++++++++++++++++++++-------------
 3 files changed, 44 insertions(+), 21 deletions(-)

diff --git a/readme-vars.yml b/readme-vars.yml
index a033ea47..192011cf 100755
--- a/readme-vars.yml
+++ b/readme-vars.yml
@@ -50,7 +50,9 @@ cap_add_param_vars:
 opt_param_usage_include_env: true
 opt_param_env_vars:
   - { env_var: "SUBDOMAINS", env_value: "www,", desc: "Subdomains you'd like the cert to cover (comma separated, no spaces) ie. `www,ftp,cloud`. For a wildcard cert, set this _exactly_ to `wildcard` (wildcard cert is available via `dns` and `duckdns` validation only)" }
-  - { env_var: "CERTPROVIDER", env_value: "", desc: "Optionally define the cert provider. Set to `zerossl` for ZeroSSL certs (requires existing [ZeroSSL account](https://app.zerossl.com/signup) and the e-mail address entered in `EMAIL` env var). Otherwise defaults to Let's Encrypt." }
+  - { env_var: "CERTPROVIDER", env_value: "", desc: "Optionally define the cert provider. Set to `zerossl` for ZeroSSL certs (requires existing [ZeroSSL account](https://app.zerossl.com/signup) and the e-mail address entered in `EMAIL` env var). Set to `custom` to use a custom ACME server. Defaults to Let's Encrypt unless `ACMESERVER` is set. " }
+  - { env_var: "ACMESERVER", env_value: "", desc: "The URL of a custom ACME server to use." }
+  - { env_var: "ACMECABUNDLE", env_value: "", desc: "A base64-encoded PEM file containing a CA bundle to trust, for use with an internal ACME CA. Required for a custom ACME CA." }
   - { env_var: "DNSPLUGIN", env_value: "cloudflare", desc: "Required if `VALIDATION` is set to `dns`. Options are `aliyun`, `cloudflare`, `cloudxns`, `cpanel`, `desec`, `digitalocean`, `directadmin`, `dnsimple`, `dnsmadeeasy`, `dnspod`, `domeneshop`, `gandi`, `gehirn`, `google`, `he`, `hetzner`, `infomaniak`, `inwx`, `ionos`, `linode`, `luadns`, `netcup`, `njalla`, `nsone`, `ovh`, `rfc2136`, `route53`, `sakuracloud`, `transip` and `vultr`. Also need to enter the credentials into the corresponding ini (or json for some plugins) file under `/config/dns-conf`." }
   - { env_var: "PROPAGATION", env_value: "", desc: "Optionally override (in seconds) the default propagation time for the dns plugins." }
   - { env_var: "DUCKDNSTOKEN", env_value: "", desc: "Required if `VALIDATION` is set to `duckdns`. Retrieve your token from https://www.duckdns.org" }
@@ -154,6 +156,7 @@ app_setup_nginx_reverse_proxy_block: ""
 
 # changelog
 changelogs:
+  - { date: "11.01.22:", desc: "Allow custom ACME servers. Supply URL and CA bundle" }
   - { date: "09.01.22:", desc: "Added a fail2ban jail for nginx unauthorized" }
   - { date: "21.12.21:", desc: "Fixed issue with iptables not working as expected" }
   - { date: "30.11.21:", desc: "Move maxmind to a [new mod](https://github.com/linuxserver/docker-mods/tree/swag-maxmind)" }
diff --git a/root/app/le-renew.sh b/root/app/le-renew.sh
index 5c638a58..bc0e6a7d 100644
--- a/root/app/le-renew.sh
+++ b/root/app/le-renew.sh
@@ -7,6 +7,11 @@ echo
 echo "<------------------------------------------------->"
 echo "cronjob running on "$(date)
 echo "Running certbot renew"
+
+if [ -f "/config/cabundle.pem" ]; then
+	export REQUESTS_CA_BUNDLE="/config/cabundle.pem"
+fi
+
 if [ "$ORIGVALIDATION" = "dns" ] || [ "$ORIGVALIDATION" = "duckdns" ]; then
   certbot -n renew \
     --post-hook "if ps aux | grep [n]ginx: > /dev/null; then s6-svc -h /var/run/s6/services/nginx; fi; \
diff --git a/root/etc/cont-init.d/50-config b/root/etc/cont-init.d/50-config
index abe45b19..a3d30873 100644
--- a/root/etc/cont-init.d/50-config
+++ b/root/etc/cont-init.d/50-config
@@ -11,6 +11,8 @@ EXTRA_DOMAINS=${EXTRA_DOMAINS}\\n\
 ONLY_SUBDOMAINS=${ONLY_SUBDOMAINS}\\n\
 VALIDATION=${VALIDATION}\\n\
 CERTPROVIDER=${CERTPROVIDER}\\n\
+ACMESERVER=${ACMESERVER}\\n\
+ACMECABUNDLE=${ACMECABUNDLE}\\n\
 DNSPLUGIN=${DNSPLUGIN}\\n\
 EMAIL=${EMAIL}\\n\
 STAGING=${STAGING}\\n"
@@ -21,7 +23,7 @@ if [ -n "${TEST_RUN}" ]; then
 fi
 
 # Sanitize variables
-SANED_VARS=( DNSPLUGIN EMAIL EXTRA_DOMAINS ONLY_SUBDOMAINS STAGING SUBDOMAINS URL VALIDATION CERTPROVIDER )
+SANED_VARS=( DNSPLUGIN EMAIL EXTRA_DOMAINS ONLY_SUBDOMAINS STAGING SUBDOMAINS URL VALIDATION CERTPROVIDER ACMESERVER ACMECABUNDLE )
 for i in "${SANED_VARS[@]}"
 do
     export echo "$i"="${!i//\"/}"
@@ -133,7 +135,7 @@ if [ -f "/config/donoteditthisfile.conf" ]; then
     mv /config/donoteditthisfile.conf /config/.donoteditthisfile.conf
 fi
 if [ ! -f "/config/.donoteditthisfile.conf" ]; then
-    echo -e "ORIGURL=\"$URL\" ORIGSUBDOMAINS=\"$SUBDOMAINS\" ORIGONLY_SUBDOMAINS=\"$ONLY_SUBDOMAINS\" ORIGEXTRA_DOMAINS=\"$EXTRA_DOMAINS\" ORIGVALIDATION=\"$VALIDATION\" ORIGDNSPLUGIN=\"$DNSPLUGIN\" ORIGPROPAGATION=\"$PROPAGATION\" ORIGSTAGING=\"$STAGING\" ORIGDUCKDNSTOKEN=\"$DUCKDNSTOKEN\" ORIGCERTPROVIDER=\"$CERTPROVIDER\" ORIGEMAIL=\"$EMAIL\"" > /config/.donoteditthisfile.conf
+    echo -e "ORIGURL=\"$URL\" ORIGSUBDOMAINS=\"$SUBDOMAINS\" ORIGONLY_SUBDOMAINS=\"$ONLY_SUBDOMAINS\" ORIGEXTRA_DOMAINS=\"$EXTRA_DOMAINS\" ORIGVALIDATION=\"$VALIDATION\" ORIGDNSPLUGIN=\"$DNSPLUGIN\" ORIGPROPAGATION=\"$PROPAGATION\" ORIGSTAGING=\"$STAGING\" ORIGDUCKDNSTOKEN=\"$DUCKDNSTOKEN\" ORIGCERTPROVIDER=\"$CERTPROVIDER\" ORIGACMESERVER=\"$ACMESERVER\" ORIGACMECABUNDLE=\"$ACMECABUNDLE\" ORIGEMAIL=\"$EMAIL\"" > /config/.donoteditthisfile.conf
     echo "Created .donoteditthisfile.conf"
 fi
 
@@ -147,23 +149,36 @@ if [ -z "$VALIDATION" ]; then
     echo "VALIDATION parameter not set; setting it to http"
 fi
 
-# if zerossl is selected or staging is set to true, use the relevant server
-if [ "$CERTPROVIDER" = "zerossl" ] && [ "$STAGING" = "true" ]; then
-    echo "ZeroSSL does not support staging mode, ignoring STAGING variable"
-fi
-if [ "$CERTPROVIDER" = "zerossl" ] && [ -n "$EMAIL" ]; then
-    echo "ZeroSSL is selected as the cert provider, registering cert with $EMAIL"
-    ACMESERVER="https://acme.zerossl.com/v2/DV90"
-elif [ "$CERTPROVIDER" = "zerossl" ] && [ -z "$EMAIL" ]; then
-    echo "ZeroSSL is selected as the cert provider, but the e-mail address has not been entered. Please visit https://zerossl.com, register a new account and set the account e-mail address in the EMAIL environment variable"
-    sleep infinity
-elif [ "$STAGING" = "true" ]; then
-    echo "NOTICE: Staging is active"
-    echo "Using Let's Encrypt as the cert provider"
-    ACMESERVER="https://acme-staging-v02.api.letsencrypt.org/directory"
+# Choose the relevant CA server
+if [ -n "$ACMESERVER" ]; then
+	if [ -z "$EMAIL" ]; then
+		echo 'A custom $ACMESERVER URL requires an account $EMAIL to be supplied'
+		sleep infinity
+	fi
+	echo "Using $ACMESERVER as the cert provider; registering cert with $EMAIL"
+elif [ "$CERTPROVIDER" = "zerossl" ]; then
+	ACMESERVER="https://acme.zerossl.com/v2/DV90"
+	if [ "$STAGING" = "true" ]; then
+		echo "ZeroSSL cert provider does not support staging mode, ignoring STAGING variable"
+	fi
+	if [ -z "$EMAIL" ]; then
+		echo "ZeroSSL is selected as the cert provider, but the e-mail address has not been entered. Please visit https://zerossl.com, register a new account and set the account e-mail address in the EMAIL environment variable"
+		sleep infinity
+	fi
+	echo "Using ZeroSSL as the cert provider; registering cert with $EMAIL"
 else
-    echo "Using Let's Encrypt as the cert provider"
-    ACMESERVER="https://acme-v02.api.letsencrypt.org/directory"
+	if [ "$STAGING" = "true" ]; then
+		echo "NOTICE: Staging is active"
+		ACMESERVER="https://acme-staging-v02.api.letsencrypt.org/directory"
+	else
+		ACMESERVER="https://acme-v02.api.letsencrypt.org/directory"
+	fi
+	echo "Using Let's Encrypt as the cert provider"
+fi
+
+if [ -n "$ACMECABUNDLE" ]; then
+	echo "$ACMECABUNDLE" | base64 -d - > /config/cabundle.pem
+	export REQUESTS_CA_BUNDLE="/config/cabundle.pem"
 fi
 
 # figuring out url only vs url & subdomains vs subdomains only
@@ -301,7 +316,7 @@ if [ ! "$URL" = "$ORIGURL" ] || [ ! "$SUBDOMAINS" = "$ORIGSUBDOMAINS" ] || [ ! "
 fi
 
 # saving new variables
-echo -e "ORIGURL=\"$URL\" ORIGSUBDOMAINS=\"$SUBDOMAINS\" ORIGONLY_SUBDOMAINS=\"$ONLY_SUBDOMAINS\" ORIGEXTRA_DOMAINS=\"$EXTRA_DOMAINS\" ORIGVALIDATION=\"$VALIDATION\" ORIGDNSPLUGIN=\"$DNSPLUGIN\" ORIGPROPAGATION=\"$PROPAGATION\" ORIGSTAGING=\"$STAGING\" ORIGDUCKDNSTOKEN=\"$DUCKDNSTOKEN\" ORIGCERTPROVIDER=\"$CERTPROVIDER\" ORIGEMAIL=\"$EMAIL\"" > /config/.donoteditthisfile.conf
+echo -e "ORIGURL=\"$URL\" ORIGSUBDOMAINS=\"$SUBDOMAINS\" ORIGONLY_SUBDOMAINS=\"$ONLY_SUBDOMAINS\" ORIGEXTRA_DOMAINS=\"$EXTRA_DOMAINS\" ORIGVALIDATION=\"$VALIDATION\" ORIGDNSPLUGIN=\"$DNSPLUGIN\" ORIGPROPAGATION=\"$PROPAGATION\" ORIGSTAGING=\"$STAGING\" ORIGDUCKDNSTOKEN=\"$DUCKDNSTOKEN\" ORIGCERTPROVIDER=\"$CERTPROVIDER\" ORIGACMESERVER=\"$ACMESERVER\" ORIGACMECABUNDLE=\"$ACMECABUNDLE\" ORIGEMAIL=\"$EMAIL\"" > /config/.donoteditthisfile.conf
 
 # alter extension for error message
 if [ "$DNSPLUGIN" = "google" ]; then
@@ -311,7 +326,7 @@ else
 fi
 
 # Check if the cert is using the old LE root cert, revoke and regen if necessary
-if [ -f "/config/keys/letsencrypt/chain.pem" ] && ([ "${CERTPROVIDER}" == "letsencrypt" ] || [ "${CERTPROVIDER}" == "" ]) && [ "${STAGING}" != "true" ] && ! openssl x509 -in /config/keys/letsencrypt/chain.pem -noout -issuer | grep -q "ISRG Root X"; then
+if [ -f "/config/keys/letsencrypt/chain.pem" ] && ([ "${CERTPROVIDER}" = "letsencrypt" ] || ([ "${CERTPROVIDER}" = "" ] && [ -z "$ACMECABUNDLE" ])) && [ "${STAGING}" != "true" ] && ! openssl x509 -in /config/keys/letsencrypt/chain.pem -noout -issuer | grep -q "ISRG Root X"; then
     echo "The cert seems to be using the old LE root cert, which is no longer valid. Deleting and revoking."
     REV_ACMESERVER="https://acme-v02.api.letsencrypt.org/directory"
     certbot revoke --non-interactive --cert-path /config/etc/letsencrypt/live/"$ORIGDOMAIN"/fullchain.pem --server $REV_ACMESERVER