From 36b71a0ebbcf3eeb0a0e1c8e4eb4a0bbfc338885 Mon Sep 17 00:00:00 2001 From: MrMeeb Date: Sun, 9 Jul 2023 21:53:06 +0000 Subject: [PATCH] add standalone and webroot methods --- Dockerfile | 27 +++-- README.md | 67 ++++++++++-- root/certbot-prepare.sh | 223 +++++++++++++++++++++++++++++++--------- root/certbot-renew.sh | 16 +-- root/container-init.sh | 10 +- 5 files changed, 255 insertions(+), 88 deletions(-) diff --git a/Dockerfile b/Dockerfile index d7c1b83..3116ad7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,17 +11,30 @@ FROM base-${TARGETARCH}${TARGETVARIANT} ARG S6_OVERLAY_VERSION=3.1.5.0 -ENV DOMAINS= -ENV EMAIL= -ENV INTERVAL="0 */6 * * *" -ENV STAGING=false -ENV PROPOGATION_TIME=10 -ENV GENERATE_DHPARAM=true -ENV TZ=UTC +# Core variables ENV PUID=1000 ENV PGID=1000 +ENV TZ=UTC +ENV GENERATE_DHPARAM=true +ENV INTERVAL="0 */6 * * *" + +# Single domain +ENV DOMAINS= +ENV EMAIL= +ENV STAGING=false + +# Custom CA support +ENV CUSTOM_CA= +ENV CUSTOM_CA_SERVER= + +# Different plugin support (to support Cloudflare but also normal mode) +ENV PLUGIN=standalone +ENV PROPOGATION_TIME=10 ENV CLOUDFLARE_TOKEN= +## Multi-cert support +ENV CERT_COUNT=1 + #Get required packages RUN apk update && apk add curl bash python3 py3-virtualenv procps tzdata nano shadow xz busybox-suid openssl diff --git a/README.md b/README.md index c6cdc14..83b35fa 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Drone (self-hosted) with branch](https://img.shields.io/drone/build/MrMeeb/certbot-cron-docker/master?label=latest&server=https%3A%2F%2Fdrone.mrmeeb.stream&style=for-the-badge) ![Drone (self-hosted) with branch](https://img.shields.io/drone/build/MrMeeb/certbot-cron-docker/develop?label=develop&server=https%3A%2F%2Fdrone.mrmeeb.stream&style=for-the-badge) -Dockerised Certbot that utilises cron to schedule creating and renewing SSL certificates. Uses Cloudflare for DNS-01 verification. Automatic renewal attempt happens every 6 hours by default. +Dockerised Certbot that utilises cron to schedule creating and renewing SSL certificates. Supports standalone, webroot or Cloudflare methods. Automatic renewal attempt happens every 6 hours by default. ## Tags @@ -18,8 +18,9 @@ Dockerised Certbot that utilises cron to schedule creating and renewing SSL cert docker run -d --name certbot \ -e EMAIL=admin@domain.com \ -e DOMAINS=domain.com \ + -e PLUGIN=cloudflare \ -e CLOUDFLARE_TOKEN=123abc - -v /docker/certbot-cron:/config \ + -v ./certbot-cron:/config \ git.mrmeeb.stream/mrmeeb/certbot-cron:latest ``` @@ -36,30 +37,78 @@ services: environment: - EMAIL=admin@domain.com - DOMAINS=domain.com,*.domain.com + - PLUGIN=cloudflare - CLOUDFLARE_TOKEN=123abc ``` ## Environment Variables: -| Variable | Default Value | Description | +### Core Options: + +Core options to the container + +| Variable | Default | Description | | --- | --- | --- | |PUID |int |1000 |Sets the UID of the user certbot runs under | |PGID |int |1000 |Sets the GID of the user certbot runs under | |TZ |[List of valid TZs](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) |UTC |Sets the timezone of the container | +| GENERATE_DHPARAM | true (case-sensitive) | Generate Diffie-Hellman keys in /config/letsencrypt/keys | +| INTERVAL | 0 */6 * * * | How often certbot attempts to renew the certificate. Cron syntax | +| CERT_COUNT | 1 | How many certificates certbot will try to issue (more than 1 not yet implemented) | + +### Certificate Options + +These options apply when `CERT_COUNT` is `1` + +| Variable | Default | Description | +| --- | --- | --- | | EMAIL | None | Email address for renewal information & other communications | | DOMAINS | None | Domains to be included in the certificate. Comma separated list, no spaces. Wildcards supported | -| INTERVAL | 0 */6 * * * | How often certbot attempts to renew the certificate. Cron syntax | -| STAGING | false (case-sensitive) | Uses the LetsEncrypt staging endpoint for testing - avoids the aggressive rate-limiting of the production endpoint | +| STAGING | false (case-sensitive) | Uses the LetsEncrypt staging endpoint for testing - avoids the aggressive rate-limiting of the production endpoint. **Not supported when using a custom Certificate Authority.** | + +### Plugins + +Plugins that can used for issuing a certificate + +| Variable | Default | Description | +| --- | --- | --- | +| PLUGIN | standalone | Options are `webroot`, `standalone`, or `cloudflare` | + +- `webroot` - relies on a webserver running on the FQDN for which you're trying to issue a certificate to serve validation files + - Requires the webserver's root directory to be mounted to the container as `/config/webroot` +- `standalone` - certbot spawns a webserver on port 80 for validation + - Requires this container to be bound to port 80 on the host +- `cloudflare` - Creates a TXT record with Cloudflare pointing to the domain you're requesting a certificate for + - Requires the domain you're requesting a certificate for to be entered in Cloudflare + +#### Cloudflare Plugin + +Options that affect the behaviour of certbot running with the Cloudflare plugin + +| Variable | Default | Description | +| --- | --- | --- | | PROPOGATION_TIME | 10 | 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 | -| GENERATE_DHPARAM | true (case-sensitive) | Generate Diffie-Hellman keys in /config/letsencrypt/keys | -| CLOUDFLARE_TOKEN | N/A | Cloudflare token for verification | +| CLOUDFLARE_TOKEN | null | Cloudflare token for verification | + +### Custom Certificate Authority + +Options to use a custom Certificate Authority, for example when issuing internal certificates + +| Variable | Default | Description | +| --- | --- | --- | +| 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` | ## 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** -## Other +## Ports -Thanks to [this guy](https://stackoverflow.com/questions/63447441/docker-stop-for-crond-times-out) for explaining how to make cron actually shutdown when stopping the container. \ No newline at end of file +| Port | Purpose | +| --- | --- | +| 80 | Used by ACME to verify domain ownership. **Required if `PLUGIN=standalone` is set** \ No newline at end of file diff --git a/root/certbot-prepare.sh b/root/certbot-prepare.sh index d7c6437..550aa66 100644 --- a/root/certbot-prepare.sh +++ b/root/certbot-prepare.sh @@ -32,54 +32,177 @@ then touch /config/.crontab.txt fi -if [ ! -f /config/.secrets/cloudflare.ini ] +#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 + +function single_domain { + + if [ ! -z $CUSTOM_CA ] + then + + echo "Using a custom CA for issuing certificates" + + 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" + exit 1 + 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 + 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 + fi + + #REQUESTS_CA_BUNDLE=/config/custom_ca/$CUSTOM_CA + + CUSTOM_CA_PATH=/config/custom_ca/$CUSTOM_CA + CUSTOM_CA_SERVER_OPT="--server $CUSTOM_CA_SERVER" + + if [ $STAGING ] + then + + echo "Staging is not supported when using a custom CA, so overriding. To remove this alert, set staging to false." + + STAGING=false + + 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 --email $EMAIL -d $DOMAINS) + + ## Run with Cloudflare plugin + if [ $PLUGIN == "cloudflare" ] + then + + echo "Using Cloudflare plugin" + + if [ ! -f /config/.secrets/cloudflare.ini ] + then + touch /config/.secrets/cloudflare.ini + fi + + if [ -n "$CLOUDFLARE_TOKEN" ] + then + echo "Cloudflare token is present" + + echo "dns_cloudflare_api_token = $CLOUDFLARE_TOKEN" > /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 as an ENV var, or by editing the file directly" + + exit 1 + 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 = true ] + then + echo "Using staging endpoint - THIS SHOULD BE USED FOR TESTING ONLY" + ${BASE_COMMAND[@]} --dns-cloudflare --dns-cloudflare-propagation-seconds $PROPOGATION_TIME --dns-cloudflare-credentials /config/.secrets/cloudflare.ini --staging + # Add to renewal list + echo "REQUESTS_CA_BUNDLE=$CUSTOM_CA_PATH ${BASE_COMMAND[@]} --dns-cloudflare --dns-cloudflare-propagation-seconds $PROPOGATION_TIME --dns-cloudflare-credentials /config/.secrets/cloudflare.ini --staging" >> /config/renew-list.sh + echo "Creation/renewal attempt complete" + elif [ $STAGING = false ] + then + echo "Using production endpoint" + ${BASE_COMMAND[@]} --dns-cloudflare --dns-cloudflare-propagation-seconds $PROPOGATION_TIME --dns-cloudflare-credentials /config/.secrets/cloudflare.ini + # Add to renewal list + echo "REQUESTS_CA_BUNDLE=$CUSTOM_CA_PATH ${BASE_COMMAND[@]} --dns-cloudflare --dns-cloudflare-propagation-seconds $PROPOGATION_TIME --dns-cloudflare-credentials /config/.secrets/cloudflare.ini" >> /config/renew-list.sh + echo "Creation/renewal attempt complete" + else + echo "Unrecognised option for STAGING variable - check your configuration" + + exit 1 + fi + + ## Run with Standalone plugin + elif [ $PLUGIN == "standalone" ] + then + + echo "Using HTTP verification via built-in web-server - please ensure port 80 is exposed." + + if [ $STAGING = true ] + then + echo "Using staging endpoint - THIS SHOULD BE USED FOR TESTING ONLY" + REQUESTS_CA_BUNDLE=$CUSTOM_CA_PATH ${BASE_COMMAND[@]} --standalone --staging + # Add to renewal list + echo "REQUESTS_CA_BUNDLE=$CUSTOM_CA_PATH ${BASE_COMMAND[@]} --standalone --staging" >> /config/renew-list.sh + echo "Creation/renewal attempt complete" + elif [ $STAGING = false ] + then + echo "Using production endpoint" + REQUESTS_CA_BUNDLE=$CUSTOM_CA_PATH ${BASE_COMMAND[@]} --standalone + # Add to renewal list + echo "REQUESTS_CA_BUNDLE=$CUSTOM_CA_PATH ${BASE_COMMAND[@]} --standalone" >> /config/renew-list.sh + echo "Creation/renewal attempt complete" + else + echo "Unrecognised option for STAGING variable - check your configuration" + + exit 1 + fi + + ## Run with webroot plugin + elif [ $PLUGIN == "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 = true ] + then + echo "Using staging endpoint - THIS SHOULD BE USED FOR TESTING ONLY" + REQUESTS_CA_BUNDLE=$CUSTOM_CA_PATH ${BASE_COMMAND[@]} --webroot --webroot-path /config/webroot --staging + # Add to renewal list + echo "REQUESTS_CA_BUNDLE=$CUSTOM_CA_PATH ${BASE_COMMAND[@]} --webroot --webroot-path /config/webroot --staging" >> /config/renew-list.sh + echo "Creation/renewal attempt complete" + elif [ $STAGING = false ] + then + echo "Using production endpoint" + REQUESTS_CA_BUNDLE=$CUSTOM_CA_PATH ${BASE_COMMAND[@]} --webroot --webroot-path /config/webroot + # Add to renewal list + echo "REQUESTS_CA_BUNDLE=$CUSTOM_CA_PATH ${BASE_COMMAND[@]} --webroot --webroot-path /config/webroot" >> /config/renew-list.sh + echo "Creation/renewal attempt complete" + else + echo "Unrecognised option for STAGING variable - check your configuration" + + exit 1 + fi + + else + + echo "Unrecognised option for PLUGIN variable - check your configuration" + + 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 + + echo "Starting automatic renewal job. Schedule is $INTERVAL" + crontab /config/.crontab.txt + +} + +if [ $CERT_COUNT == 1 ] then - touch /config/.secrets/cloudflare.ini -fi - -if [ -n "$CLOUDFLARE_TOKEN" ] -then - echo "Cloudflare token is present" - - echo "dns_cloudflare_api_token = $CLOUDFLARE_TOKEN" > /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 as an ENV var, or by editing the file directly" - - exit 22 -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 = true ]] -then - echo "Using staging endpoint - THIS SHOULD BE USED FOR TESTING ONLY" - certbot certonly --staging --non-interactive --config-dir /config/letsencrypt --work-dir /config/.tmp --logs-dir /config/logs --key-path /config/letsencrypt/keys --expand --agree-tos --dns-cloudflare --dns-cloudflare-propagation-seconds $PROPOGATION_TIME --dns-cloudflare-credentials /config/.secrets/cloudflare.ini --email $EMAIL -d $DOMAINS - echo "Creation/renewal attempt complete" -elif [[ $STAGING = false ]] -then - echo "Using production endpoint" - certbot certonly --non-interactive --config-dir /config/letsencrypt --work-dir /config/.tmp --logs-dir /config/logs --key-path /config/letsencrypt/keys --expand --agree-tos --dns-cloudflare --dns-cloudflare-propagation-seconds $PROPOGATION_TIME --dns-cloudflare-credentials /config/.secrets/cloudflare.ini --email $EMAIL -d $DOMAINS - echo "Creation/renewal attempt complete" -else - echo "Unrecognised option for STAGING variable - check your configuration" - - exit 22 -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 - -echo "Starting automatic renewal job. Schedule is $INTERVAL" -crontab /config/.crontab.txt \ No newline at end of file + single_domain +fi \ No newline at end of file diff --git a/root/certbot-renew.sh b/root/certbot-renew.sh index 485a0d5..acedd1c 100644 --- a/root/certbot-renew.sh +++ b/root/certbot-renew.sh @@ -3,18 +3,4 @@ echo '' date echo "Attempting to renew certificates" -if [[ $STAGING = true ]] -then - echo "Using staging endpoint - THIS SHOULD BE USED FOR TESTING ONLY" - certbot certonly --staging --non-interactive --config-dir /config/letsencrypt --work-dir /config/.tmp --logs-dir /config/logs --key-path /config/letsencrypt/keys --expand --agree-tos --dns-cloudflare --dns-cloudflare-propagation-seconds $PROPOGATION_TIME --dns-cloudflare-credentials /config/.secrets/cloudflare.ini --email $EMAIL -d $DOMAINS - echo "Renewal attempt complete" -elif [[ $STAGING = false ]] -then - echo "Using production endpoint" - certbot certonly --non-interactive --config-dir /config/letsencrypt --work-dir /config/.tmp --logs-dir /config/logs --key-path /config/letsencrypt/keys --expand --agree-tos --dns-cloudflare --dns-cloudflare-propagation-seconds $PROPOGATION_TIME --dns-cloudflare-credentials /config/.secrets/cloudflare.ini --email $EMAIL -d $DOMAINS - echo "Renewal attempt complete" -else - echo "Unrecognised option for STAGING variable - check your configuration" - - exit 8 -fi \ No newline at end of file +bash /config/.renew-list.sh \ No newline at end of file diff --git a/root/container-init.sh b/root/container-init.sh index 1f4fe54..180aac6 100644 --- a/root/container-init.sh +++ b/root/container-init.sh @@ -15,18 +15,14 @@ echo "" echo "Initialising container" echo " ---------------------------------------------------------------------- -ENVIRONMENT +ENVIRONMENT (only core variables shown) ---------------------------------------------------------------------- PUID=${PUID} PGID=${PGID} TZ=${TZ} -DOMAINS=${DOMAINS} -EMAIL=${EMAIL} INTERVAL=${INTERVAL} -STAGING=${STAGING} -PROPOGATION_TIME=${PROPOGATION_TIME} GENERATE_DHPARAM=${GENERATE_DHPARAM} -CLOUDFLARE_TOKEN=${CLOUDFLARE_TOKEN} +CERT_COUNT=${CERT_COUNT} ---------------------------------------------------------------------- " @@ -41,7 +37,7 @@ if [[ ! "${PUID}" -eq 0 ]] && [[ ! "${PGID}" -eq 0 ]]; then groupmod -o -g "${PGID}" mrmeeb else echo "Running as root is not supported, please fix your PUID and PGID!" - exit 1 + sleep infinity fi echo "Checking permissions in /config and /app."