diff --git a/readme-vars.yml b/readme-vars.yml index 787c6622..5a0514e8 100644 --- a/readme-vars.yml +++ b/readme-vars.yml @@ -31,7 +31,11 @@ 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` 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/internal ACME server (e.g. [step-ca](https://smallstep.com/docs/step-ca/)), which requires `CERTPROVIDERURL` (see below). Otherwise defaults to Let's Encrypt."} + - {env_var: "CERTPROVIDERURL", env_value: "", desc: "Only used when `CERTPROVIDER=custom`. The ACME directory URL of your custom ACME server, ie. `https://ca.example.com/acme/acme/directory`."} + - {env_var: "ACMECABUNDLE", env_value: "", desc: "Only used when `CERTPROVIDER=custom`. Trust an internal CA so SWAG can connect to your ACME server over TLS. Either a path to a CA bundle PEM file mounted into the container, or the base64-encoded contents of that PEM."} + - {env_var: "EAB_KID", env_value: "", desc: "Only used when `CERTPROVIDER=custom`. External Account Binding key identifier, if your custom ACME server requires EAB. Must be set together with `EAB_HMAC_KEY`."} + - {env_var: "EAB_HMAC_KEY", env_value: "", desc: "Only used when `CERTPROVIDER=custom`. External Account Binding HMAC key, if your custom ACME server requires EAB. Must be set together with `EAB_KID`."} - {env_var: "DNSPLUGIN", env_value: "cloudflare", desc: "Required if `VALIDATION` is set to `dns`. Options are `acmedns`, `aliyun`, `azure`, `bunny`, `cloudflare`, `cpanel`, `desec`, `digitalocean`, `directadmin`, `dnsimple`, `dnsmadeeasy`, `dnspod`, `do`, `domeneshop`, `dreamhost`, `duckdns`, `dynu`, `freedns`, `gandi`, `gehirn`, `glesys`, `godaddy`, `google`, `he`, `hetzner`, `hetzner-cloud`, `infomaniak`, `inwx`, `ionos`, `linode`, `loopia`, `luadns`, `namecheap`, `netcup`, `njalla`, `nsone`, `ovh`, `porkbun`, `rfc2136`, `route53`, `sakuracloud`, `standalone`, `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: "EMAIL", env_value: "", desc: "Optional e-mail address used for cert expiration notifications (Required for ZeroSSL)."} @@ -219,6 +223,7 @@ init_diagram: | "swag:latest" <- Base Images # changelog changelogs: + - {date: "19.06.26:", desc: "Add support for custom/internal ACME servers via `CERTPROVIDER=custom` with `CERTPROVIDERURL`, optional `ACMECABUNDLE` for internal CA trust, and optional `EAB_KID`/`EAB_HMAC_KEY` for External Account Binding."} - {date: "01.06.26:", desc: "Remove obsolete old cert check logic."} - {date: "23.01.26:", desc: "Reorder init to fix proxy conf version checks."} - {date: "21.12.25:", desc: "Add support for hetzner-cloud dns validation."} diff --git a/root/app/le-renew.sh b/root/app/le-renew.sh index c597f359..5ac4ca8c 100755 --- a/root/app/le-renew.sh +++ b/root/app/le-renew.sh @@ -5,5 +5,10 @@ echo "<------------------------------------------------->" echo echo "<------------------------------------------------->" echo "cronjob running on $(date)" +# Trust the custom/internal CA (CERTPROVIDER=custom) when renewing, since +# REQUESTS_CA_BUNDLE is an env var and is not persisted in cli.ini. +if [[ -f /config/cabundle.pem ]]; then + export REQUESTS_CA_BUNDLE="/config/cabundle.pem" +fi echo "Running certbot renew" certbot renew --non-interactive --config-dir /config/etc/letsencrypt --logs-dir /config/log/letsencrypt --work-dir /tmp/letsencrypt --config /config/etc/letsencrypt/cli.ini diff --git a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run index e018d62e..df4a4351 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run +++ b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run @@ -12,6 +12,7 @@ EXTRA_DOMAINS=${EXTRA_DOMAINS}\\n\ ONLY_SUBDOMAINS=${ONLY_SUBDOMAINS}\\n\ VALIDATION=${VALIDATION}\\n\ CERTPROVIDER=${CERTPROVIDER}\\n\ +CERTPROVIDERURL=${CERTPROVIDERURL}\\n\ DNSPLUGIN=${DNSPLUGIN}\\n\ EMAIL=${EMAIL}\\n\ STAGING=${STAGING}\\n" @@ -23,6 +24,12 @@ for i in "${SANED_VARS[@]}"; do export echo "${i}"="$(echo "${!i}" | tr '[:upper:]' '[:lower:]')" done +# Custom ACME vars are case-sensitive (URL paths, base64 EAB keys / CA bundle), +# so only strip surrounding quotes; do not lowercase them. +for i in CERTPROVIDERURL ACMECABUNDLE EAB_KID EAB_HMAC_KEY; do + export "${i}=${!i//\"/}" +done + # Check for and install requested DNS plugins if grep -q "universal-package-install" <<< "${DOCKER_MODS}" && grep -q "certbot-dns" <<< "${INSTALL_PIP_PACKAGES}"; then echo "**** Installing requested dns plugins ****" @@ -80,7 +87,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}\" 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}\" ORIGCERTPROVIDER=\"${CERTPROVIDER}\" ORIGCERTPROVIDERURL=\"${CERTPROVIDERURL}\" ORIGEMAIL=\"${EMAIL}\"" >/config/.donoteditthisfile.conf echo "Created .donoteditthisfile.conf" fi @@ -186,15 +193,23 @@ if [[ ! "${URL}" = "${ORIGURL}" ]] || [[ ! "${DNSPLUGIN}" = "${ORIGDNSPLUGIN}" ]] || [[ ! "${PROPAGATION}" = "${ORIGPROPAGATION}" ]] || [[ ! "${STAGING}" = "${ORIGSTAGING}" ]] || - [[ ! "${CERTPROVIDER}" = "${ORIGCERTPROVIDER}" ]]; then + [[ ! "${CERTPROVIDER}" = "${ORIGCERTPROVIDER}" ]] || + [[ ! "${CERTPROVIDERURL}" = "${ORIGCERTPROVIDERURL}" ]]; then echo "Different validation parameters entered than what was used before. Revoking and deleting existing certificate, and an updated one will be created" - if [[ "${ORIGCERTPROVIDER}" = "zerossl" ]]; then + if [[ "${ORIGCERTPROVIDER}" = "custom" ]]; then + REV_ACMESERVER=("${ORIGCERTPROVIDERURL}") + elif [[ "${ORIGCERTPROVIDER}" = "zerossl" ]]; then REV_ACMESERVER=("https://acme.zerossl.com/v2/DV90") elif [[ "${ORIGSTAGING}" = "true" ]]; then REV_ACMESERVER=("https://acme-staging-v02.api.letsencrypt.org/directory") else REV_ACMESERVER=("https://acme-v02.api.letsencrypt.org/directory") fi + # if the previous provider was a custom internal CA, trust its bundle so the + # revocation request can verify TLS + if [[ "${ORIGCERTPROVIDER}" = "custom" ]] && [[ -f /config/cabundle.pem ]]; then + export REQUESTS_CA_BUNDLE="/config/cabundle.pem" + fi if [[ -f /config/etc/letsencrypt/live/"${ORIGDOMAIN}"/fullchain.pem ]]; then certbot revoke --config-dir /config/etc/letsencrypt --logs-dir /config/log/letsencrypt --work-dir /tmp/letsencrypt --config /config/etc/letsencrypt/cli.ini --non-interactive --cert-path /config/etc/letsencrypt/live/"${ORIGDOMAIN}"/fullchain.pem --key-path /config/etc/letsencrypt/live/"${ORIGDOMAIN}"/privkey.pem --server "${REV_ACMESERVER[@]}" || true else @@ -204,13 +219,34 @@ if [[ ! "${URL}" = "${ORIGURL}" ]] || 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}\" 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}\" ORIGCERTPROVIDER=\"${CERTPROVIDER}\" ORIGCERTPROVIDERURL=\"${CERTPROVIDERURL}\" ORIGEMAIL=\"${EMAIL}\"" >/config/.donoteditthisfile.conf # 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 +if [[ "${CERTPROVIDER}" = "custom" ]]; then + if [[ -z "${CERTPROVIDERURL}" ]]; then + echo "CERTPROVIDER is set to custom, but CERTPROVIDERURL is not set. Please set CERTPROVIDERURL to the ACME directory URL of your custom ACME server (e.g. https://ca.example.com/acme/acme/directory). Sleeping." + sleep infinity + fi + echo "Using custom ACME server as the cert provider: ${CERTPROVIDERURL}" + if [[ "${STAGING}" = "true" ]]; then + echo "STAGING has no effect with a custom ACME server, ignoring STAGING variable" + fi + ACMESERVER="${CERTPROVIDERURL}" + # Trust a custom/internal CA so certbot can reach the ACME server over TLS. + # ACMECABUNDLE may be a path to a mounted PEM file or a base64-encoded PEM. + if [[ -n "${ACMECABUNDLE}" ]]; then + if [[ -f "${ACMECABUNDLE}" ]]; then + cp "${ACMECABUNDLE}" /config/cabundle.pem + else + echo "${ACMECABUNDLE}" | base64 -d - >/config/cabundle.pem + fi + lsiown abc:abc /config/cabundle.pem + echo "Custom CA bundle written to /config/cabundle.pem" + fi +elif [[ "${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 @@ -227,6 +263,20 @@ fi set_ini_value "server" "${ACMESERVER}" /config/etc/letsencrypt/cli.ini +# Trust the custom CA for this issuance run; clean up custom remnants when not +# using a custom provider so a previous internal CA / EAB is not reused. +if [[ "${CERTPROVIDER}" = "custom" ]] && [[ -f /config/cabundle.pem ]]; then + export REQUESTS_CA_BUNDLE="/config/cabundle.pem" +elif [[ "${CERTPROVIDER}" != "custom" ]]; then + unset REQUESTS_CA_BUNDLE + rm -f /config/cabundle.pem + if [[ "${ORIGCERTPROVIDER}" = "custom" ]]; then + # drop EAB creds left over from a custom provider so Let's Encrypt / + # ZeroSSL re-register cleanly + sed -i -e "/^eab-kid\b/d" -e "/^eab-hmac-key\b/d" /config/etc/letsencrypt/cli.ini + fi +fi + # figuring out domain only vs domain & subdomains vs subdomains only DOMAINS_ARRAY=() if [[ -z "${SUBDOMAINS}" ]] || [[ "${ONLY_SUBDOMAINS}" != true ]]; then @@ -321,7 +371,22 @@ fi # generating certs if necessary if [[ ! -f "/config/keys/letsencrypt/fullchain.pem" ]]; then - if [[ "${CERTPROVIDER}" = "zerossl" ]] && [[ -n "${EMAIL}" ]]; then + if [[ "${CERTPROVIDER}" = "custom" ]]; then + # Optional External Account Binding for custom ACME servers (e.g. step-ca + # ACME provisioners configured to require EAB). Only consumed at account + # registration, so it has no effect on later renewals. + if [[ -n "${EAB_KID}" ]] && [[ -n "${EAB_HMAC_KEY}" ]]; then + echo "Registering with custom ACME server using provided EAB credentials" + set_ini_value "eab-kid" "${EAB_KID}" /config/etc/letsencrypt/cli.ini + set_ini_value "eab-hmac-key" "${EAB_HMAC_KEY}" /config/etc/letsencrypt/cli.ini + elif [[ -n "${EAB_KID}" ]] || [[ -n "${EAB_HMAC_KEY}" ]]; then + echo "Only one of EAB_KID / EAB_HMAC_KEY is set. Both are required to use External Account Binding. Sleeping." + sleep infinity + else + # remove any EAB values left over from a previous provider + sed -i -e "/^eab-kid\b/d" -e "/^eab-hmac-key\b/d" /config/etc/letsencrypt/cli.ini + fi + elif [[ "${CERTPROVIDER}" = "zerossl" ]] && [[ -n "${EMAIL}" ]]; then echo "Retrieving EAB from ZeroSSL" EAB_CREDS=$(curl -s https://api.zerossl.com/acme/eab-credentials-email --data "email=${EMAIL}") ZEROSSL_EAB_KID=$(echo "${EAB_CREDS}" | jq .eab_kid)