Skip to content

Commit

Permalink
feat: certificate auto-renew support (single domain only)
Browse files Browse the repository at this point in the history
  • Loading branch information
joseluisq committed Apr 1, 2024
1 parent 3ba988b commit 6c03628
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 4 deletions.
30 changes: 30 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,39 @@ LABEL version="${VERSION}" \
description="A Let's Encrypt Docker image using Lego CLI client." \
maintainer="Jose Quintana <joseluisq.net>"

# 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 \
Expand All @@ -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"]

Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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`
Expand Down
73 changes: 73 additions & 0 deletions certificate_renew.sh
Original file line number Diff line number Diff line change
@@ -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
27 changes: 25 additions & 2 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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 "$@"

0 comments on commit 6c03628

Please sign in to comment.