From e69f607bf8313a9c46936d8fd4b9e49f048dd3ce Mon Sep 17 00:00:00 2001 From: MrMeeb Date: Sun, 16 Jul 2023 15:37:12 +0000 Subject: [PATCH] add multi-certificate support --- README.md | 81 +++++- root/certbot-prepare.sh | 603 ++++++++++++++++++++++++++++++++++++---- root/container-init.sh | 45 ++- 3 files changed, 661 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 83b35fa..3c5655b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Dockerised Certbot that utilises cron to schedule creating and renewing SSL cert ## Running ### Docker CLI -``` +```bash docker run -d --name certbot \ -e EMAIL=admin@domain.com \ -e DOMAINS=domain.com \ @@ -25,7 +25,7 @@ docker run -d --name certbot \ ``` ### Docker Compose -``` +```yaml version: "3" services: certbot: @@ -99,12 +99,87 @@ Options to use a custom Certificate Authority, for example when issuing internal | CUSTOM_CA | null | Name of the root certificate Certbot/ACME will trust requesting the certificate, e.g `root.pem`. **Must be placed in `/config/custom_ca`** | | CUSTOM_CA_SERVER | null | Custom server URL used by Certbot/ACME when requesting a certificate, e.g `https://ca.internal/acme/acme/directory` | +### Multiple Certificates + +This container can issue multiple certificates each containing different domains. This could be used to issue a certificate for a public domain on Cloudflare, but then also for a local certificate from an internal Certificate Authority, for example. Another example would be you have a web-server hosting two separate websites and you want them to have dedicated SSL certificates instead of sharing one. + +When issuing multiple certificates, first `CERT_COUNT` must be set to a value greater than 1. + +#### Global Environment Variables + +Some environment variables can be set globally, where they apply to all certificates (unless otherwise specifically specified). The following can be used globally: + +| Variable | DESCRIPTION | +| --- | --- | +|EMAIL| Email address for renewal information & other communications | +|STAGING| Uses the LetsEncrypt staging endpoint for testing - avoids the aggressive rate-limiting of the production endpoint. **Not supported when using a custom Certificate Authority.** | +|CUSTOM_CA| Name of the root certificate Certbot/ACME will trust requesting the certificate, e.g `root.pem`. **Must be placed in `/config/custom_ca`** | +|CUSTOM_CA_SERVER| Custom server URL used by Certbot/ACME when requesting a certificate, e.g `https://ca.internal/acme/acme/directory` | +|PLUGIN| Options are `webroot`, `standalone`, or `cloudflare` | +|PROPOGATION_TIME| **(Applies to Cloudflare plugin)** The amount of time (seconds) that certbot waits for the TXT records to propogate to Cloudflare before verifying - the more domains in the certificate, the longer you might need | + +More detail on these environment variables may be found further up. + +#### Certificate-specific Environment Variables + +Any variable other than those described as **Core Options** can be set per-certificate in a multi-certificate environment. The syntax is `${VARIABLE_NAME}_${CERT_NUMBER}`. The only certificate-specific option that **must** be set is the `DOMAINS` option. + +##### Multi-certificate container using global variables: + +```yaml + certbot: + container_name: certbot + image: git.mrmeeb.stream/mrmeeb/certbot-cron + volumes: + - /docker/certbot-cron:/config + - /docker/nginx/www:/config/webroot + environment: + - PUID=1000 + - PGID=1000 + - TZ=Europe/London + - GENERATE_DHPARAM=false + - CERT_COUNT=2 + - EMAIL=admin@domain.com + - CUSTOM_CA=root.pem + - CUSTOM_CA_SERVER=https://ca.internal/acme/acme/directory + - PLUGIN=webroot + - STAGING=false + - DOMAINS_1=website1.com + - DOMAINS_2=website2.com +``` + +##### Multi-certificate container using different options for each certificate: +```yaml + certbot: + container_name: certbot + image: git.mrmeeb.stream/mrmeeb/certbot-cron + volumes: + - /docker/certbot-cron:/config + - /docker/nginx/www:/config/webroot + environment: + - PUID=1000 + - PGID=1000 + - TZ=Europe/London + - GENERATE_DHPARAM=false + - CERT_COUNT=2 + - EMAIL=admin@domain.com + - DOMAINS_1=website1.com + - CUSTOM_CA_1=root.pem + - CUSTOM_CA_SERVER_1=https://ca.internal/acme/acme/directory + - PLUGIN_1=webroot + - STAGING_1=false + - DOMAINS_2=website2.com + - PLUGIN_2=cloudflare + - CLOUDFLARE_TOKEN_2=abc123 + - PROPOGATION_TIME_2=30 + - STAGING_2=true +``` + ## Volumes | Docker path | Purpose | | --- | --- | | /config | Stores configs and LetsEncrypt output for mounting in other containers -| /config/custom_ca | Mountpoint for a custom Certificate Authority root certificate. **Required if `CUSTOM_CA` is set** | /config/webroot | Mountpoint for the webroot of a separate webserver. **Required if `PLUGIN=webroot` is set** ## Ports diff --git a/root/certbot-prepare.sh b/root/certbot-prepare.sh index edaa9e5..f65f2fd 100644 --- a/root/certbot-prepare.sh +++ b/root/certbot-prepare.sh @@ -1,6 +1,9 @@ #!/command/with-contenv bash # shellcheck shell=bash +# Halt container if anything returns a non-zero exit code +set -e + # Creating needed folders and files if they don't already exist if [ ! -d /config/.secrets ] then @@ -32,6 +35,17 @@ then touch /config/.crontab.txt fi +function better_exit { + + echo "" + echo "" + echo "" + echo "You can ignore the below error messages - they happened because the container exited with a non-0 exit code due misconfiguration" + echo "==========================================================" + exit 1 + +} + # Cleanup renew list and create it fresh, ready for commands to be run and added echo "#!/command/with-contenv bash" > /config/.renew-list.sh echo "" >> /config/.renew-list.sh @@ -39,75 +53,184 @@ echo "" >> /config/.renew-list.sh # Create original config file to track changes to environmental variables if [ ! -f /config/.donoteditthisfile ] then - echo -e "ORIGDOMAINS=\"${DOMAINS}\" ORIGEMAIL=\"${EMAIL}\" ORIGSTAGING=\"${STAGING}\" ORIGCUSTOM_CA=\"${CUSTOM_CA}\" ORIGCUSTOM_CA_SERVER=\"${CUSTOM_CA_SERVER}\" ORIGPLUGIN=\"${PLUGIN}\" ORIGPROPOGATION_TIME=\"${PROPOGATION_TIME}\"" > /config/.donoteditthisfile - echo "Created .donoteditthisfile" + echo -e "ORIGDOMAINS=\"${DOMAINS}\" ORIGEMAIL=\"${EMAIL}\" ORIGSTAGING=\"${STAGING}\" ORIGCUSTOM_CA=\"${CUSTOM_CA}\" ORIGCUSTOM_CA_SERVER=\"${CUSTOM_CA_SERVER}\" ORIGPLUGIN=\"${PLUGIN}\" ORIGPROPOGATION_TIME=\"${PROPOGATION_TIME}\" ORIGCERT_COUNT=${CERT_COUNT}" > /config/.donoteditthisfile fi # Load original config file . /config/.donoteditthisfile -# Checking for changes to config file, revoke certs if necessary -if [ ! "${DOMAINS}" = "${ORIGDOMAINS}" ] || - [ ! "${EMAIL}" = "${ORIGEMAIL}" ] || - [ ! "${STAGING}" = "${ORIGSTAGING}" ] || - [ ! "${CUSTOM_CA}" = "${ORIGCUSTOM_CA}" ] || - [ ! "${CUSTOM_CA_SERVER}" = "${ORIGCUSTOM_CA_SERVER}" ] || - [ ! "${PLUGIN}" = "${ORIGPLUGIN}" ] || - [ ! "${PROPOGATION_TIME}" = "${ORIGPROPOGATION_TIME}" ] -then +# Revoke all certs if CERT_COUNT has decreased, starting fresh +if [ "${CERT_COUNT}" -lt "${ORIGCERT_COUNT}" ]; then - echo "Configuration has changed since the last certificate was issued. Revoking and regenerating certs" - FIRST_DOMAIN=$(echo $ORIGDOMAINS | cut -d \, -f1) + echo "" - if [ ! -z $ORIGCUSTOM_CA ] - then + echo "CERT_COUNT has decreased - revoking all certificates then reissuing to cleanup any lingerers." - echo "A custom CA was used for issuing. Using it to revoke as well." + # Use .donoteditthisfile_cert_* to get details of each issued certificate to revoke with correct parameters - if [ ! -d /config/custom_ca ] + x=1 + while [ $x -le ${ORIGCERT_COUNT} ]; do + + # Load config of particular cert + . /config/.donoteditthisfile_cert_${x} + + # Setting up variables (requires two passes to clean away requirement for indirect variables) + ## Pass 1 + DOMAINS_P1=ORIGDOMAINS_${x} + EMAIL_P1=ORIGEMAIL_${x} + STAGING_P1=ORIGSTAGING_${x} + CUSTOM_CA_P1=ORIGCUSTOM_CA_${x} + CUSTOM_CA_SERVER_P1=ORIGCUSTOM_CA_SERVER_${x} + PLUGIN_P1=ORIGPLUGIN_${x} + PROPOGATION_TIME_P1=ORIGPROPOGATION_TIME_${x} + CLOUDFLARE_TOKEN_P1=ORIGCLOUDFLARE_TOKEN_${x} + + ## Pass 2 + DOMAINS_MULTI=${!DOMAINS_P1} + EMAIL_MULTI=${!EMAIL_P1} + STAGING_MULTI=${!STAGING_P1} + CUSTOM_CA_MULTI=${!CUSTOM_CA_P1} + CUSTOM_CA_SERVER_MULTI=${!CUSTOM_CA_SERVER_P1} + PLUGIN_MULTI=${!PLUGIN_P1} + PROPOGATION_TIME_MULTI=${!PROPOGATION_TIME_P1} + CLOUDFLARE_TOKEN_MULTI=${!CLOUDFLARE_TOKEN_P1} + + FIRST_DOMAIN_MULTI=$(echo ${DOMAINS_MULTI} | cut -d \, -f1) + + echo ${FIRST_DOMAIN_MULTI} + + if [ ! -z ${CUSTOM_CA_MULTI} ] then - mkdir /config/custom_ca - echo "Please place the custom CA root file used to generate the current certificate into /config/custom_ca and restart the container" - exit 1 + + echo "A custom CA was used for issuing certificate ${x}. Using it to revoke as well." + + if [ ! -d /config/custom_ca ] + then + mkdir /config/custom_ca + echo "Please place the custom CA root file used to generate the current certificate ${x} into /config/custom_ca and restart the container" + better_exit + fi + + if [ -z "$(ls -A /config/custom_ca)" ] + then + echo "A root certificate called ${CUSTOM_CA_MULTI} was used to generate a certificate, but the /config/custom_ca dir is now empty. Please place this root certificate back this directory and restart the container so it can be safely revoked" + better_exit + fi + + CUSTOM_CA_PATH_MULTI=/config/custom_ca/${CUSTOM_CA_MULTI} + CUSTOM_CA_SERVER_OPT_MULTI="--server ${CUSTOM_CA_SERVER_MULTI}" + fi - if [ -z "$(ls -A /config/custom_ca)" ] + if [ $STAGING_MULTI = "true" ] then - echo "A root certificate called ${ORIGCUSTOM_CA} was used to generate a certificate, but the /config/custom_ca dir is now empty. Please place this root certificate back this directory and restart the container so it can be safely revoked" - exit 1 + + # Reusing the CUSTOM_CA_SERVER_OPT variable to add staging option if that was selected + CUSTOM_CA_SERVER_OPT_MULTI="--server https://acme-staging-v02.api.letsencrypt.org/directory" + fi - ORIGCUSTOM_CA_PATH=/config/custom_ca/$ORIGCUSTOM_CA - ORIGCUSTOM_CA_SERVER_OPT="--server $ORIGCUSTOM_CA_SERVER" + if [ -f /config/letsencrypt/live/"${FIRST_DOMAIN_MULTI}"/fullchain.pem ] + then - fi + REQUESTS_CA_BUNDLE=$CUSTOM_CA_PATH_MULTI certbot revoke --non-interactive --agree-tos --email $EMAIL_MULTI --config-dir /config/letsencrypt --work-dir /config/.tmp --logs-dir /config/logs --cert-path /config/letsencrypt/live/"${FIRST_DOMAIN_MULTI}"/fullchain.pem ${CUSTOM_CA_SERVER_OPT_MULTI} || true - if [ $ORIGSTAGING = "true" ] - then + rm -rf /config/letsencrypt/archive/"${FIRST_DOMAIN_MULTI}" + rm -rf /config/letsencrypt/live/"${FIRST_DOMAIN_MULTI}" + rm -rf /config/letsencrypt/renewal/"${FIRST_DOMAIN_MULTI}".conf - # Reusing the CUSTOM_CA_SERVER_OPT variable to add staging option if that was selected - ORIGCUSTOM_CA_SERVER_OPT="--server https://acme-staging-v02.api.letsencrypt.org/directory" + fi - fi + # Delete .donoteditthisfile_cert_${x} + rm -rf /config/.donoteditthisfile_cert_${x} - if [ -f /config/letsencrypt/live/"${FIRST_DOMAIN}"/fullchain.pem ] - then + # Scrubbing variables before running next cert revoke to prevent overlap of values + DOMAINS_MULTI= + EMAIL_MULTI= + STAGING_MULTI= + CUSTOM_CA_MULTI= + CUSTOM_CA_SERVER_MULTI= + PLUGIN_MULTI= + PROPOGATION_TIME_MULTI= + CLOUDFLARE_TOKEN_MULTI= + CUSTOM_CA_PATH_MULTI= + CUSTOM_CA_SERVER_OPT_MULTI= - REQUESTS_CA_BUNDLE=$ORIGCUSTOM_CA_PATH certbot revoke --non-interactive --agree-tos --email $ORIGEMAIL --config-dir /config/letsencrypt --work-dir /config/.tmp --logs-dir /config/logs --cert-path /config/letsencrypt/live/"${FIRST_DOMAIN}"/fullchain.pem $ORIGCUSTOM_CA_SERVER_OPT || true + x=$(( $x + 1 )) - rm -rf /config/letsencrypt/archive/"${FIRST_DOMAIN}" - rm -rf /config/letsencrypt/live/"${FIRST_DOMAIN}" - rm -rf /config/letsencrypt/renewal/"${FIRST_DOMAIN}".conf + done - fi + echo "Tidying up any potential lingering ACME challenges in /config/webroot from failed webroot activations" + rm -rf /config/webroot/.well-known/acme-challenge fi -# Update config file with new env vars -echo -e "ORIGDOMAINS=\"${DOMAINS}\" ORIGEMAIL=\"${EMAIL}\" ORIGSTAGING=\"${STAGING}\" ORIGCUSTOM_CA=\"${CUSTOM_CA}\" ORIGCUSTOM_CA_SERVER=\"${CUSTOM_CA_SERVER}\" ORIGPLUGIN=\"${PLUGIN}\" ORIGPROPOGATION_TIME=\"${PROPOGATION_TIME}\"" > /config/.donoteditthisfile - function single_domain { + # Checking for changes to config file, revoke certs if necessary + if [ ! "${DOMAINS}" = "${ORIGDOMAINS}" ] || + [ ! "${EMAIL}" = "${ORIGEMAIL}" ] || + [ ! "${STAGING}" = "${ORIGSTAGING}" ] || + [ ! "${CUSTOM_CA}" = "${ORIGCUSTOM_CA}" ] || + [ ! "${CUSTOM_CA_SERVER}" = "${ORIGCUSTOM_CA_SERVER}" ] || + [ ! "${PLUGIN}" = "${ORIGPLUGIN}" ] || + [ ! "${PROPOGATION_TIME}" = "${ORIGPROPOGATION_TIME}" ] + then + + echo "" + + echo "Configuration has changed since the last certificate was issued. Revoking and regenerating certs" + FIRST_DOMAIN=$(echo $ORIGDOMAINS | cut -d \, -f1) + + if [ ! -z $ORIGCUSTOM_CA ] + then + + echo "A custom CA was used for issuing. Using it to revoke as well." + + if [ ! -d /config/custom_ca ] + then + mkdir /config/custom_ca + echo "Please place the custom CA root file used to generate the current certificate into /config/custom_ca and restart the container" + better_exit + fi + + if [ -z "$(ls -A /config/custom_ca)" ] + then + echo "A root certificate called ${ORIGCUSTOM_CA} was used to generate a certificate, but the /config/custom_ca dir is now empty. Please place this root certificate back this directory and restart the container so it can be safely revoked" + better_exit + fi + + ORIGCUSTOM_CA_PATH=/config/custom_ca/$ORIGCUSTOM_CA + ORIGCUSTOM_CA_SERVER_OPT="--server $ORIGCUSTOM_CA_SERVER" + + fi + + if [ $ORIGSTAGING = "true" ] + then + + # Reusing the CUSTOM_CA_SERVER_OPT variable to add staging option if that was selected + ORIGCUSTOM_CA_SERVER_OPT="--server https://acme-staging-v02.api.letsencrypt.org/directory" + + fi + + if [ -f /config/letsencrypt/live/"${FIRST_DOMAIN}"/fullchain.pem ] + then + + REQUESTS_CA_BUNDLE=$ORIGCUSTOM_CA_PATH certbot revoke --non-interactive --agree-tos --email $ORIGEMAIL --config-dir /config/letsencrypt --work-dir /config/.tmp --logs-dir /config/logs --cert-path /config/letsencrypt/live/"${FIRST_DOMAIN}"/fullchain.pem $ORIGCUSTOM_CA_SERVER_OPT || true + + rm -rf /config/letsencrypt/archive/"${FIRST_DOMAIN}" + rm -rf /config/letsencrypt/live/"${FIRST_DOMAIN}" + rm -rf /config/letsencrypt/renewal/"${FIRST_DOMAIN}".conf + + fi + + fi + + # Update config file with new env vars + echo -e "ORIGDOMAINS=\"${DOMAINS}\" ORIGEMAIL=\"${EMAIL}\" ORIGSTAGING=\"${STAGING}\" ORIGCUSTOM_CA=\"${CUSTOM_CA}\" ORIGCUSTOM_CA_SERVER=\"${CUSTOM_CA_SERVER}\" ORIGPLUGIN=\"${PLUGIN}\" ORIGPROPOGATION_TIME=\"${PROPOGATION_TIME}\" ORIGCERT_COUNT=${CERT_COUNT}" > /config/.donoteditthisfile + + echo "" + if [ ! -z $CUSTOM_CA ] then @@ -117,19 +240,19 @@ function single_domain { then mkdir /config/custom_ca echo "Please place your custom CA file into /config/custom_ca and restart the container" - exit 1 + better_exit fi if [ -z "$(ls -A /config/custom_ca)" ] then echo "The CUSTOM_CA option is populated, but the /config/custom_ca dir is empty. Please place your root certificate in this directory and restart the container" - exit 1 + better_exit fi if [ -z $CUSTOM_CA_SERVER ] then echo "CUSTOM_CA_SERVER has not been defined. It is required for using a custom CA to issue a certificate" - exit 1 + better_exit fi CUSTOM_CA_PATH=/config/custom_ca/$CUSTOM_CA @@ -139,7 +262,7 @@ function single_domain { then echo "Staging option is not supported when using a custom CA. To remove this alert, set staging to false. If your CA has a standing endpoint, use the CUSTOM_CA_SERVER option to point to it instead" - exit 1 + better_exit fi @@ -170,7 +293,7 @@ function single_domain { then echo "cloudflare.ini is empty - please add your Cloudflare credentials or API key before continuing. This can be done by setting CLOUDFLARE_TOKEN, or by editing /config/.secrets/cloudflare.ini directly" - exit 1 + better_exit fi #Securing cloudflare.ini to supress warnings @@ -195,7 +318,7 @@ function single_domain { else echo "Unrecognised option for STAGING variable - check your configuration" - exit 1 + better_exit fi ## Run with Standalone plugin @@ -221,7 +344,7 @@ function single_domain { else echo "Unrecognised option for STAGING variable - check your configuration" - exit 1 + better_exit fi ## Run with webroot plugin @@ -247,7 +370,7 @@ function single_domain { else echo "Unrecognised option for STAGING variable - check your configuration" - exit 1 + better_exit fi else @@ -256,20 +379,384 @@ function single_domain { fi - if [ $GENERATE_DHPARAM = true ] && [ ! -s /config/letsencrypt/keys/ssl-dhparams.pem ] - then - echo "Generating Diffie-Hellman keys, saved to /config/letsencrypt/keys" - openssl dhparam -out /config/letsencrypt/keys/ssl-dhparams.pem 4096 - fi +} - echo "$INTERVAL /certbot-renew.sh >> /config/logs/renew.log" > /config/.crontab.txt +function multi_domain { - echo "Starting automatic renewal job. Schedule is $INTERVAL" - crontab /config/.crontab.txt + # Update config file with new env vars + echo -e "ORIGDOMAINS=\"${DOMAINS}\" ORIGEMAIL=\"${EMAIL}\" ORIGSTAGING=\"${STAGING}\" ORIGCUSTOM_CA=\"${CUSTOM_CA}\" ORIGCUSTOM_CA_SERVER=\"${CUSTOM_CA_SERVER}\" ORIGPLUGIN=\"${PLUGIN}\" ORIGPROPOGATION_TIME=\"${PROPOGATION_TIME}\" ORIGCERT_COUNT=${CERT_COUNT}" > /config/.donoteditthisfile + + ## Start multi-domain looper + x=1 + while [ $x -le $CERT_COUNT ] + do + + # Setting up variables (requires two passes to clean away requirement for indirect variable) + ## Pass 1 + DOMAINS_P1=DOMAINS_${x} + EMAIL_P1=EMAIL_${x} + STAGING_P1=STAGING_${x} + CUSTOM_CA_P1=CUSTOM_CA_${x} + CUSTOM_CA_SERVER_P1=CUSTOM_CA_SERVER_${x} + PLUGIN_P1=PLUGIN_${x} + PROPOGATION_TIME_P1=PROPOGATION_TIME_${x} + CLOUDFLARE_TOKEN_P1=CLOUDFLARE_TOKEN_${x} + + ## Pass 2 + DOMAINS_MULTI=${!DOMAINS_P1} + EMAIL_MULTI=${!EMAIL_P1} + STAGING_MULTI=${!STAGING_P1} + CUSTOM_CA_MULTI=${!CUSTOM_CA_P1} + CUSTOM_CA_SERVER_MULTI=${!CUSTOM_CA_SERVER_P1} + PLUGIN_MULTI=${!PLUGIN_P1} + PROPOGATION_TIME_MULTI=${!PROPOGATION_TIME_P1} + CLOUDFLARE_TOKEN_MULTI=${!CLOUDFLARE_TOKEN_P1} + + # Inheriting global default if undefined for certain variables + if [ -z ${EMAIL_MULTI} ]; then + EMAIL_MULTI=${EMAIL} + fi + + if [ -z ${STAGING_MULTI} ]; then + STAGING_MULTI=${STAGING} + fi + + if [ -z ${CUSTOM_CA_MULTI} ]; then + CUSTOM_CA_MULTI=${CUSTOM_CA} + fi + + if [ -z ${CUSTOM_CA_SERVER_MULTI} ]; then + CUSTOM_CA_SERVER_MULTI=${CUSTOM_CA_SERVER} + fi + + if [ -z ${PLUGIN_MULTI} ]; then + PLUGIN_MULTI=${PLUGIN} + fi + + if [ -z ${PROPOGATION_TIME_MULTI} ]; then + PROPOGATION_TIME_MULTI=${PROPOGATION_TIME} + fi + + # Create original config file to track changes to environmental variables + if [ ! -f /config/.donoteditthisfile_cert_${x} ] + then + echo -e "ORIGDOMAINS_${x}=\"${DOMAINS_MULTI}\" ORIGEMAIL_${x}=\"${EMAIL_MULTI}\" ORIGSTAGING_${x}=\"${STAGING_MULTI}\" ORIGCUSTOM_CA_${x}=\"${CUSTOM_CA_MULTI}\" ORIGCUSTOM_CA_SERVER_${x}=\"${CUSTOM_CA_SERVER_MULTI}\" ORIGPLUGIN_${x}=\"${PLUGIN_MULTI}\" ORIGPROPOGATION_TIME_${x}=\"${PROPOGATION_TIME_MULTI}\"" > /config/.donoteditthisfile_cert_${x} + fi + + # Load original config file + . /config/.donoteditthisfile_cert_${x} + + ORIGDOMAINS_MULTI=ORIGDOMAINS_${x} + ORIGEMAIL_MULTI=ORIGEMAIL_${x} + ORIGSTAGING_MULTI=ORIGSTAGING_${x} + ORIGCUSTOM_CA_MULTI=ORIGCUSTOM_CA_${x} + ORIGCUSTOM_CA_SERVER_MULTI=ORIGCUSTOM_CA_SERVER_${x} + ORIGPLUGIN_MULTI=ORIGPLUGIN_${x} + ORIGPROPOGATION_TIME_MULTI=ORIGPROPOGATION_TIME_${x} + ORIGCLOUDFLARE_TOKEN_MULTI=ORIGCLOUDFLARE_TOKEN_${x} + + # Log variables to console (have to remove indent because bash dumb) + + echo " +---------------------------------------------------------------------- +CERTIFICATE ${x} ENVIRONMENT +----------------------------------------------------------------------" +echo \ +"DOMAINS_${x}=${DOMAINS_MULTI} +EMAIL_${x}=${EMAIL_MULTI} +STAGING_${x}=${STAGING_MULTI} +CUSTOM_CA_${x}=${CUSTOM_CA_MULTI} +CUSTOM_CA_SERVER_${x}=${CUSTOM_CA_SERVER_MULTI} +PLUGIN_${x}=${PLUGIN_MULTI}" +## Get plugin-specific data if single certificate config +if [ ${PLUGIN_MULTI} == 'cloudflare' ]; then +echo \ +"PROPOGATION_TIME_${x}=${PROPOGATION_TIME_MULTI}" +fi +if [ ${PLUGIN_MULTI} == 'cloudflare' ] && [ ! -z ${CLOUDFLARE_TOKEN_MULTI} ]; then +echo \ +"CLOUDFLARE_TOKEN_${x}=[hidden]" +elif [ ${PLUGIN_MULTI} == 'cloudflare' ] && [ -z ${CLOUDFLARE_TOKEN_MULTI} ]; then +echo \ +"CLOUDFLARE_TOKEN_${x}=" +fi +echo \ +"---------------------------------------------------------------------- +" + + # Begin actually requesting the certificate + + echo "Requesting certificate $x" + + # Checking for changes to config file, revoke certs if necessary + if [ ! "${DOMAINS_MULTI}" = "${!ORIGDOMAINS_MULTI}" ] || + [ ! "${EMAIL_MULTI}" = "${!ORIGEMAIL_MULTI}" ] || + [ ! "${STAGING_MULTI}" = "${!ORIGSTAGING_MULTI}" ] || + [ ! "${CUSTOM_CA_MULTI}" = "${!ORIGCUSTOM_CA_MULTI}" ] || + [ ! "${CUSTOM_CA_SERVER_MULTI}" = "${!ORIGCUSTOM_CA_SERVER_MULTI}" ] || + [ ! "${PLUGIN_MULTI}" = "${!ORIGPLUGIN_MULTI}" ] || + [ ! "${PROPOGATION_TIME_MULTI}" = "${!ORIGPROPOGATION_TIME_MULTI}" ] + then + + echo "Configuration has changed since certificate ${x} was last issued. Revoking and regenerating cert ${x}" + FIRST_DOMAIN_MULTI=$(echo ${!ORIGDOMAINS_MULTI} | cut -d \, -f1) + + if [ ! -z ${!ORIGCUSTOM_CA_MULTI} ] + then + + echo "A custom CA was used for issuing certificate ${x}. Using it to revoke as well." + + if [ ! -d /config/custom_ca ] + then + mkdir /config/custom_ca + echo "Please place the custom CA root file used to generate the current certificate ${x} into /config/custom_ca and restart the container" + better_exit + fi + + if [ -z "$(ls -A /config/custom_ca)" ] + then + echo "A root certificate called ${!ORIGCUSTOM_CA_MULTI} was used to generate a certificate, but the /config/custom_ca dir is now empty. Please place this root certificate back this directory and restart the container so it can be safely revoked" + better_exit + fi + + ORIGCUSTOM_CA_PATH_MULTI=/config/custom_ca/${!ORIGCUSTOM_CA_MULTI} + ORIGCUSTOM_CA_SERVER_OPT_MULTI="--server ${!ORIGCUSTOM_CA_SERVER_MULTI}" + + fi + + if [ $ORIGSTAGING_MULTI = "true" ] + then + + # Reusing the CUSTOM_CA_SERVER_OPT variable to add staging option if that was selected + ORIGCUSTOM_CA_SERVER_OPT_MULTI="--server https://acme-staging-v02.api.letsencrypt.org/directory" + + fi + + if [ -f /config/letsencrypt/live/"${FIRST_DOMAIN_MULTI}"/fullchain.pem ] + then + + REQUESTS_CA_BUNDLE=$ORIGCUSTOM_CA_PATH_MULTI certbot revoke --non-interactive --agree-tos --email $ORIGEMAIL_MULTI --config-dir /config/letsencrypt --work-dir /config/.tmp --logs-dir /config/logs --cert-path /config/letsencrypt/live/"${FIRST_DOMAIN_MULTI}"/fullchain.pem ${ORIGCUSTOM_CA_SERVER_OPT_MULTI} || true + + rm -rf /config/letsencrypt/archive/"${FIRST_DOMAIN_MULTI}" + rm -rf /config/letsencrypt/live/"${FIRST_DOMAIN_MULTI}" + rm -rf /config/letsencrypt/renewal/"${FIRST_DOMAIN_MULTI}".conf + + fi + + echo "Tidying up any potential lingering ACME challenges in /config/webroot from failed webroot activations" + rm -rf /config/webroot/.well-known/acme-challenge + + fi + + # Update config file with new cert-specific env vars + echo -e "ORIGDOMAINS_${x}=\"${DOMAINS_MULTI}\" ORIGEMAIL_${x}=\"${EMAIL_MULTI}\" ORIGSTAGING_${x}=\"${STAGING_MULTI}\" ORIGCUSTOM_CA_${x}=\"${CUSTOM_CA_MULTI}\" ORIGCUSTOM_CA_SERVER_${x}=\"${CUSTOM_CA_SERVER_MULTI}\" ORIGPLUGIN_${x}=\"${PLUGIN_MULTI}\" ORIGPROPOGATION_TIME_${x}=\"${PROPOGATION_TIME_MULTI}\"" > /config/.donoteditthisfile_cert_${x} + + if [ ! -z ${CUSTOM_CA_MULTI} ] + then + + echo "Using a custom CA for issuing certificate ${x}" + + if [ ! -d /config/custom_ca ] + then + mkdir /config/custom_ca + echo "Please place your custom CA file into /config/custom_ca and restart the container" + better_exit + fi + + if [ -z "$(ls -A /config/custom_ca)" ] + then + echo "The CUSTOM_CA_${x} option is populated, but the /config/custom_ca dir is empty. Please place your root certificate for certificate ${x} in this directory and restart the container" + better_exit + fi + + if [ -z ${CUSTOM_CA_SERVER_MULTI} ] + then + echo "CUSTOM_CA_SERVER_${x} has not been defined. It is required when using a custom CA to issue certificate ${x}" + better_exit + fi + + CUSTOM_CA_PATH_MULTI=/config/custom_ca/${CUSTOM_CA_MULTI} + CUSTOM_CA_SERVER_OPT_MULTI="--server ${CUSTOM_CA_SERVER_MULTI}" + + if [ ${STAGING_MULTI} = "true" ] + then + + echo "Staging option is not supported when using a custom CA. To remove this alert, set staging to false. If your CA has a standing endpoint, use the CUSTOM_CA_SERVER_${x} option to point to it instead" + better_exit + + fi + + fi + + BASE_COMMAND=(certbot certonly --non-interactive --config-dir /config/letsencrypt --work-dir /config/.tmp --logs-dir /config/logs --key-path /config/letsencrypt/keys --expand --agree-tos "${CUSTOM_CA_SERVER_OPT_MULTI}" --email "${EMAIL_MULTI}" -d "${DOMAINS_MULTI}") + + ## Run with Cloudflare plugin + if [ ${PLUGIN_MULTI} == "cloudflare" ] + then + + echo "Using Cloudflare plugin" + + if [ ! -f /config/.secrets/cloudflare.ini ] + then + touch /config/.secrets/cloudflare.ini + fi + + if [ -n "${CLOUDFLARE_TOKEN_MULTI}" ] + then + echo "Cloudflare token is present" + + echo "dns_cloudflare_api_token = ${CLOUDFLARE_TOKEN_MULTI}" > /config/.secrets/cloudflare.ini + + fi + + if [ ! -s /config/.secrets/cloudflare.ini ] + then + echo "cloudflare.ini is empty - please add your Cloudflare credentials or API key before continuing. This can be done by setting CLOUDFLARE_TOKEN_${x}" + + better_exit + fi + + #Securing cloudflare.ini to supress warnings + chmod 600 /config/.secrets/cloudflare.ini + + echo "Creating certificates, or attempting to renew if they already exist" + + if [ ${STAGING_MULTI} = true ] + then + echo "Using staging endpoint - THIS SHOULD BE USED FOR TESTING ONLY" + ${BASE_COMMAND[@]} --dns-cloudflare --dns-cloudflare-propagation-seconds ${PROPOGATION_TIME_MULTI} --dns-cloudflare-credentials /config/.secrets/cloudflare.ini --staging + # Add to renewal list + echo "## Certificate ${x}" >> /config/.renew-list.sh + echo "${BASE_COMMAND[@]} --dns-cloudflare --dns-cloudflare-propagation-seconds ${PROPOGATION_TIME_MULTI} --dns-cloudflare-credentials /config/.secrets/cloudflare.ini --staging" >> /config/.renew-list.sh + echo "" >> /config/.renew-list.sh + echo "Creation/renewal attempt complete" + elif [ ${STAGING_MULTI} = false ] + then + echo "Using production endpoint" + ${BASE_COMMAND[@]} --dns-cloudflare --dns-cloudflare-propagation-seconds ${PROPOGATION_TIME_MULTI} --dns-cloudflare-credentials /config/.secrets/cloudflare.ini + # Add to renewal list + echo "## Certificate ${x}" >> /config/.renew-list.sh + echo "REQUESTS_CA_BUNDLE=$CUSTOM_CA_PATH ${BASE_COMMAND[@]} --dns-cloudflare --dns-cloudflare-propagation-seconds ${PROPOGATION_TIME_MULTI} --dns-cloudflare-credentials /config/.secrets/cloudflare.ini" >> /config/.renew-list.sh + echo "" >> /config/.renew-list.sh + echo "Creation/renewal attempt complete" + else + echo "Unrecognised option for STAGING variable - check your configuration" + + better_exit + fi + + ## Run with Standalone plugin + elif [ ${PLUGIN_MULTI} == "standalone" ] + then + + echo "Using HTTP verification via built-in web-server - please ensure port 80 is exposed." + + if [ ${STAGING_MULTI} = true ] + then + echo "Using staging endpoint - THIS SHOULD BE USED FOR TESTING ONLY" + REQUESTS_CA_BUNDLE=${CUSTOM_CA_PATH_MULTI} ${BASE_COMMAND[@]} --standalone --staging + # Add to renewal list + echo "## Certificate ${x}" >> /config/.renew-list.sh + echo "REQUESTS_CA_BUNDLE=${CUSTOM_CA_PATH_MULTI} ${BASE_COMMAND[@]} --standalone --staging" >> /config/.renew-list.sh + echo "" >> /config/.renew-list.sh + echo "Creation/renewal attempt complete" + elif [ ${STAGING_MULTI} = false ] + then + echo "Using production endpoint" + REQUESTS_CA_BUNDLE=${CUSTOM_CA_PATH_MULTI} ${BASE_COMMAND[@]} --standalone + # Add to renewal list + echo "## Certificate ${x}" >> /config/.renew-list.sh + echo "REQUESTS_CA_BUNDLE=${CUSTOM_CA_PATH_MULTI} ${BASE_COMMAND[@]} --standalone" >> /config/.renew-list.sh + echo "" >> /config/.renew-list.sh + echo "Creation/renewal attempt complete" + else + echo "Unrecognised option for STAGING variable - check your configuration" + + better_exit + fi + + ## Run with webroot plugin + elif [ ${PLUGIN_MULTI} == "webroot" ] + then + + echo "Using HTTP verification via webroot - please ensure you have mounted a webroot at /config/webroot from a web-server reachable via the domain you are issuing a certificate for." + + if [ ${STAGING_MULTI} = true ] + then + echo "Using staging endpoint - THIS SHOULD BE USED FOR TESTING ONLY" + REQUESTS_CA_BUNDLE=${CUSTOM_CA_PATH_MULTI} ${BASE_COMMAND[@]} --webroot --webroot-path /config/webroot --staging + # Add to renewal list + echo "## Certificate ${x}" >> /config/.renew-list.sh + echo "REQUESTS_CA_BUNDLE=${CUSTOM_CA_PATH_MULTI} ${BASE_COMMAND[@]} --webroot --webroot-path /config/webroot --staging" >> /config/.renew-list.sh + echo "" >> /config/.renew-list.sh + echo "Creation/renewal attempt complete" + elif [ ${STAGING_MULTI} = false ] + then + echo "Using production endpoint" + REQUESTS_CA_BUNDLE=${CUSTOM_CA_PATH_MULTI} ${BASE_COMMAND[@]} --webroot --webroot-path /config/webroot + # Add to renewal list + echo "## Certificate ${x}" >> /config/.renew-list.sh + echo "REQUESTS_CA_BUNDLE=${CUSTOM_CA_PATH_MULTI} ${BASE_COMMAND[@]} --webroot --webroot-path /config/webroot" >> /config/.renew-list.sh + echo "" >> /config/.renew-list.sh + echo "Creation/renewal attempt complete" + else + echo "Unrecognised option for STAGING variable - check your configuration" + + better_exit + fi + + else + + echo "Unrecognised option for PLUGIN variable - check your configuration" + + fi + + # Scrubbing variables before running next cert to prevent overlap of values + DOMAINS_MULTI= + EMAIL_MULTI= + STAGING_MULTI= + CUSTOM_CA_MULTI= + CUSTOM_CA_SERVER_MULTI= + PLUGIN_MULTI= + PROPOGATION_TIME_MULTI= + CLOUDFLARE_TOKEN_MULTI= + CUSTOM_CA_PATH_MULTI= + CUSTOM_CA_SERVER_OPT_MULTI= + ORIGDOMAINS_MULTI= + ORIGEMAIL_MULTI= + ORIGSTAGING_MULTI= + ORIGCUSTOM_CA_MULTI= + ORIGCUSTOM_CA_SERVER_MULTI= + ORIGPLUGIN_MULTI= + ORIGPROPOGATION_TIME_MULTI= + ORIGCLOUDFLARE_TOKEN_MULTI= + FIRST_DOMAIN_MULTI= + ORIGCUSTOM_CA_PATH_MULTI= + ORIGCUSTOM_CA_SERVER_OPT_MULTI= + + x=$(( $x + 1 )) + + done } if [ $CERT_COUNT == 1 ] then single_domain -fi \ No newline at end of file +elif [ $CERT_COUNT -gt 1 ] +then + multi_domain +else + echo "CERT_COUNT varaible not recognised. It needs to be a value of 1 or greater." +fi + +if [ $GENERATE_DHPARAM = true ] && [ ! -s /config/letsencrypt/keys/ssl-dhparams.pem ] +then + echo "Generating Diffie-Hellman keys, saved to /config/letsencrypt/keys. This can take a long time!" + openssl dhparam -out /config/letsencrypt/keys/ssl-dhparams.pem 4096 +fi + +echo "$INTERVAL /certbot-renew.sh >> /config/logs/renew.log" > /config/.crontab.txt + +echo "Starting automatic renewal job. Schedule is $INTERVAL" +crontab /config/.crontab.txt \ No newline at end of file diff --git a/root/container-init.sh b/root/container-init.sh index 180aac6..10683bb 100644 --- a/root/container-init.sh +++ b/root/container-init.sh @@ -13,17 +13,48 @@ echo "| |" echo "================================================" echo "" echo "Initialising container" -echo " ----------------------------------------------------------------------- -ENVIRONMENT (only core variables shown) ----------------------------------------------------------------------- -PUID=${PUID} +if [ ${CERT_COUNT} == 1 ]; then +echo \ +"---------------------------------------------------------------------- +ENVIRONMENT +----------------------------------------------------------------------" +else +echo \ +"---------------------------------------------------------------------- +ENVIRONMENT (Certificate options logged later) +----------------------------------------------------------------------" +fi +echo \ +"PUID=${PUID} PGID=${PGID} TZ=${TZ} INTERVAL=${INTERVAL} GENERATE_DHPARAM=${GENERATE_DHPARAM} -CERT_COUNT=${CERT_COUNT} ----------------------------------------------------------------------- +CERT_COUNT=${CERT_COUNT}" +## Send extra detail to logs if single certificate config +if [ ${CERT_COUNT} == 1 ]; then +echo \ +"DOMAINS=${DOMAINS} +EMAIL=${EMAIL} +STAGING=${STAGING} +CUSTOM_CA=${CUSTOM_CA} +CUSTOM_CA_SERVER=${CUSTOM_CA_SERVER} +PLUGIN=${PLUGIN}" +fi +## Get plugin-specific data if single certificate config +if [ ${CERT_COUNT} == 1 ] && [ ${PLUGIN} == 'cloudflare' ]; then +echo \ +"PROPOGATION_TIME=${PROPOGATION_TIME}" +fi +if [ ${CERT_COUNT} == 1 ] && [ ${PLUGIN} == 'cloudflare' ] && [ ! -z ${CLOUDFLARE_TOKEN} ]; then +echo \ +"CLOUDFLARE_TOKEN=[hidden]" +elif [ ${CERT_COUNT} == 1 ] && [ ${PLUGIN} == 'cloudflare' ] && [ -z ${CLOUDFLARE_TOKEN} ]; then +echo \ +"CLOUDFLARE_TOKEN=" +fi +echo \ +"---------------------------------------------------------------------- " #Setting UID and GID as configured