diff --git a/Dockerfile b/Dockerfile index 7144354..82e5443 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,10 +52,39 @@ LABEL version="${VERSION}" \ description="A Let's Encrypt Docker image using Lego CLI client." \ maintainer="Jose Quintana " +# General options +ENV ENV_LEGO_EMAIL= +ENV ENV_LEGO_DOMAINS= +ENV ENV_LEGO_SERVER= +ENV ENV_LEGO_CSR= +ENV ENV_LEGO_ACCEPT_TOS=false +ENV ENV_LEGO_PATH=/etc/ssl/.lego + +# Challenge types +ENV ENV_LEGO_HTTP=false +# See Lego DNS providers supported https://go-acme.github.io/lego/dns/#dns-providers +ENV ENV_LEGO_DNS= + +# Run hooks +ENV ENV_LEGO_RUN_HOOK= + +# Specify if the operation will be a `renewal` +ENV ENV_LEGO_RENEW=false +ENV ENV_LEGO_RENEW_DAYS= +ENV ENV_LEGO_RENEW_HOOK= + +# Certificate auto-renew feature +ENV ENV_CERT_AUTO_RENEW=false +# Renew the certificate 3 days before expiration +ENV ENV_CERT_AUTO_RENEW_DAYS_BEFORE_EXPIRE=3 +# Check certificate expiration once a day by default +ENV ENV_CERT_AUTO_RENEW_CRON_INTERVAL="0 0 * * *" + RUN set -eux \ && DEBIAN_FRONTEND=noninteractive apt-get update -qq \ && DEBIAN_FRONTEND=noninteractive apt-get install -qq -y --no-install-recommends --no-install-suggests \ ca-certificates \ + cron \ tzdata \ # Clean up local repository of retrieved packages and remove the package lists && apt-get clean \ @@ -64,6 +93,7 @@ RUN set -eux \ COPY --from=build /usr/local/bin/lego /usr/local/bin/ COPY ./entrypoint.sh /usr/local/bin/ +COPY ./certificate_renew.sh /usr/local/bin/ ENTRYPOINT ["entrypoint.sh"] diff --git a/README.md b/README.md index 8c129a3..14c6f8f 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,12 @@ docker run -it --rm \ -e ENV_LEGO_ACCEPT_TOS=true \ -e ENV_LEGO_EMAIL=email@domain.com \ -e ENV_LEGO_DOMAINS="*.domain.com" \ + # -e ENV_LEGO_PATH=/etc/ssl/.lego \ -e ENV_LEGO_DNS=cloudflare \ -e CLOUDFLARE_EMAIL=email@domain.com \ -e CLOUDFLARE_DNS_API_TOKEN= \ -w /root \ - -v $PWD:/root/.lego \ + -v $PWD:/etc/ssl/.lego \ joseluisq/docker-lets-encrypt # 2024/01/01 00:00:30 [INFO] [*.domain.com] acme: Obtaining bundled SAN certificate @@ -87,6 +88,7 @@ To activate the environment variables support, set `ENV_LEGO_ENABLE=true`. - `ENV_LEGO_SERVER` - `ENV_LEGO_CSR` - `ENV_LEGO_ACCEPT_TOS=false` +- `ENV_LEGO_PATH=/etc/ssl/.lego` Directory to use for storing the data. ### Challenge types @@ -103,10 +105,21 @@ By default, the **Lego CLI** `run` subcommand will be executed, which will [obta To [renew a certificate](https://go-acme.github.io/lego/usage/cli/renew-a-certificate/), use the following environment variables instead. -- `ENV_LEGO_RENEW=false` +- `ENV_LEGO_RENEW=false` It tells Lego CLI to perform a `renewal` operation on demand. - `ENV_LEGO_RENEW_DAYS` - `ENV_LEGO_RENEW_HOOK` +#### Certificate auto-renew + +**NOTE:** the auto-renew feature is limited to one domain for now. + +- `ENV_CERT_AUTO_RENEW=false` Enable the auto-renew feature +- `ENV_CERT_AUTO_RENEW_DAYS_BEFORE_EXPIRE=3` The days before the certificate expiration to perform a renewal try. +- `ENV_CERT_AUTO_RENEW_CRON_INTERVAL=0 0 * * *` The Crontab interval for the auto-renew checker (default, once a day) + +When the option is `ENV_CERT_AUTO_RENEW=true` then a script will programmatically check the certificate days before the expiration (`ENV_CERT_AUTO_RENEW_DAYS_BEFORE_EXPIRE`) and will perform a renewal try. +Keep in mind that `ENV_LEGO_RENEW` should be disabled (`false`) when using this feature because it refers to the Lego CLI `renew` operation (subcommand). + ### Additional arguments - `ENV_LEGO_ARGS` diff --git a/certificate_renew.sh b/certificate_renew.sh new file mode 100755 index 0000000..5d02a77 --- /dev/null +++ b/certificate_renew.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +# +# Custom script to renew a certificate before it expires. +# This script can be run by a cron-tab to check for the certificate expiration programmatically. +# + +echo "[info] Starting certificate check script..." + +# NOTE: The `ENV_LEGO_RENEW` specifies only a Lego CLI renewal operation +# so we only proceed if it is disabled +if [[ -n "$ENV_LEGO_RENEW" ]] && [[ "$ENV_LEGO_RENEW" = "true" ]]; then + echo "[warn] ENV_LEGO_RENEW should be disabled when using auto-renew. Cancelled!" + exit +fi +# Required envs +if [[ -z "$ENV_LEGO_DOMAINS" ]]; then + echo "[warn] ENV_LEGO_DOMAINS is required by not provided. Cancelled!" + exit +fi +if [[ -z "$ENV_CERT_AUTO_RENEW" ]] || [[ "$ENV_CERT_AUTO_RENEW" != "true" ]]; then + echo "[warn] ENV_CERT_AUTO_RENEW is required by not provided. Cancelled!" + exit +fi +if [[ -z "$ENV_CERT_AUTO_RENEW_DAYS_BEFORE_EXPIRE" ]]; then + echo "[warn] ENV_CERT_AUTO_RENEW_DAYS_BEFORE_EXPIRE is required by not provided. Cancelled!" + exit +fi + +# TODO: Add support for multiple domains + +# Find the cert file based on `ENV_LEGO_DOMAINS` +# NOTE: The auto-renew feature is limited to one domain for now +domain=$(echo $ENV_LEGO_DOMAINS | sed 's/;.*//' | sed 's/*.//') +cert_file=$ENV_LEGO_PATH/certificates/_.$domain.crt + +if [ ! -f $cert_file ]; then + echo "[warn] The certificate file '$cert_file' was not found. Cancelled!" + exit +fi + +end_date=$(openssl x509 -in $cert_file -noout -enddate | sed 's/;.*//' | sed 's/notAfter=//') +end_date_timestamp=$(date -d "$end_date" +"%s") +end_date_before_expire=$(date -d "$end_date $ENV_CERT_AUTO_RENEW_DAYS_BEFORE_EXPIRE days ago" +"%s") +current_time=$(date +%s) +# DEBUG: enable this when testing +# current_time=$(date -d "+90 days" +%s) + +echo "[info] Checking if certificate is closer to expire..." +echo "[info] Cert File: $cert_file" +echo "[info] End Date: $end_date | End Date Timestamp=$end_date_timestamp" +echo "[info] Renew Before: $ENV_CERT_AUTO_RENEW_DAYS_BEFORE_EXPIRE day(s)" + +# Checking certificate expiration +if [ $current_time -lt $end_date_before_expire ]; then + echo "[info] The certificate is still valid until $end_date. Nothing to do." + exit +fi + +# Certificated closer to expire, try to renew it + +echo "[warn] The certificate is closer to expire on $end_date" +echo "[info] Trying to renew the certificate before expire..." +echo + +/usr/local/bin/entrypoint.sh --certificate-renew + +echo "[info] The certificate renewal was performed successfully!" +end_date=$(openssl x509 -in $cert_file -noout -enddate | sed 's/;.*//' | sed 's/notAfter=//') +end_date_timestamp=$(date -d "$end_date" +"%s") +echo "[info] Cert File: $cert_file" +echo "[info] End Date: $end_date | End Date Timestamp=$end_date_timestamp" +echo diff --git a/entrypoint.sh b/entrypoint.sh index 9763545..1766bcd 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -6,13 +6,19 @@ set -e # Check if incoming command contains flags -if [ "${1#-}" != "$1" ]; then +if [[ "${1#-}" != "$1" ]] && [[ "$1" != "--certificate-renew" ]]; then set -- lego "$@" elif [[ -n "$ENV_LEGO_ENABLE" ]] && [[ "$ENV_LEGO_ENABLE" = "true" ]]; then args="" op="" - # Operation types + # Renew operation on demand which also skip auto-renew when file called directly + should_renew=$1 + if [[ -n "$should_renew" ]] && [[ "$should_renew" = "--certificate-renew" ]]; then + ENV_LEGO_RENEW=true + fi + + # Operation types, the default is `run` subcommand if [[ -n "$ENV_LEGO_RENEW" ]] && [[ "$ENV_LEGO_RENEW" = "true" ]]; then op=" renew" if [[ -n "$ENV_LEGO_RENEW_DAYS" ]]; then @@ -25,6 +31,9 @@ elif [[ -n "$ENV_LEGO_ENABLE" ]] && [[ "$ENV_LEGO_ENABLE" = "true" ]]; then if [[ -n "$ENV_LEGO_RUN_HOOK" ]]; then op="$op --run-hook=$ENV_LEGO_RUN_HOOK"; fi fi + if [[ -n "$ENV_LEGO_PATH" ]]; then + args="$args --path=$ENV_LEGO_PATH" + fi # Challenge types if [[ -n "$ENV_LEGO_HTTP" ]] && [[ "$ENV_LEGO_HTTP" = "true" ]]; then args="$args --http" @@ -48,6 +57,20 @@ elif [[ -n "$ENV_LEGO_ENABLE" ]] && [[ "$ENV_LEGO_ENABLE" = "true" ]]; then if [[ -n "$ENV_LEGO_ARGS" ]]; then args="$args$ENV_LEGO_ARGS"; fi set -- lego $args$op + + ## Enable auto-renew on-demand + if [[ -z "$ENV_LEGO_RENEW" ]] || [[ "$ENV_LEGO_RENEW" = "false" ]]; then + if [[ -n "$ENV_CERT_AUTO_RENEW" ]] && [[ "$ENV_CERT_AUTO_RENEW" = "true" ]]; then + # Set the default crontab, redirect output to Docker stdout + declare -p | grep -Ev 'BASHOPTS|BASH_VERSINFO|EUID|PPID|SHELLOPTS|UID' > /container.env + cmd="SHELL=/bin/bash BASH_ENV=/container.env /usr/local/bin/certificate_renew.sh > /proc/1/fd/1 2>&1" + crontab -l | echo "$ENV_CERT_AUTO_RENEW_CRON_INTERVAL $cmd" | crontab - + echo "[info] The certificate auto-renew process is configured and waiting for the iteration..." + echo "[info] Crontab interval: $ENV_CERT_AUTO_RENEW_CRON_INTERVAL" + cron -f + exit + fi + fi fi exec "$@"