Skip to content

Commit

Permalink
Added wildcard cert support using AWS Route53
Browse files Browse the repository at this point in the history
  • Loading branch information
richturner authored Apr 29, 2024
2 parents 18f8a6a + edcc6ae commit 7ddaeea
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 29 deletions.
14 changes: 7 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ MAINTAINER support@openremote.io

USER root

ARG ACME_PLUGIN_VERSION=0.1.1
ENV DOMAINNAME ${DOMAINNAME}
ENV DOMAINNAMES ${DOMAINNAMES}
ENV TERM xterm
Expand All @@ -25,10 +24,11 @@ ENV CERT_DIR /deployment/certs
ENV LE_DIR /deployment/letsencrypt
ENV CHROOT_DIR /etc/haproxy/webroot

# Install certbot
# Install certbot and Route53 DNS plugin
RUN apk update \
&& apk add --no-cache certbot inotify-tools tar curl openssl \
&& rm -f /var/cache/apk/*
&& apk add --no-cache certbot py-pip inotify-tools tar curl openssl \
&& rm -f /var/cache/apk/* \
&& pip install certbot-dns-route53 --break-system-packages

# Add ACME LUA plugin
ADD acme-plugin.tar.gz /etc/haproxy/lua/
Expand All @@ -39,9 +39,9 @@ RUN mkdir -p ${CHROOT_DIR} \
&& mkdir -p ${LE_DIR} && chown haproxy:haproxy ${LE_DIR} \
&& mkdir -p /etc/letsencrypt \
&& mkdir -p /var/lib/letsencrypt \
&& touch /etc/periodic/daily/certbot-renew \
&& printf "#!/bin/sh\ncertbot renew --deploy-hook \"/entrypoint.sh sync-haproxy\"\n" > /etc/periodic/daily/certbot-renew \
&& chmod +x /etc/periodic/daily/certbot-renew \
&& touch /etc/periodic/daily/cert-renew \
&& printf "#!/bin/sh\n/entrypoint.sh auto-renew\n" > /etc/periodic/daily/cert-renew \
&& chmod +x /etc/periodic/daily/cert-renew \
&& chown -R haproxy:haproxy /etc/letsencrypt \
&& chown -R haproxy:haproxy /etc/haproxy \
&& chown -R haproxy:haproxy /var/lib/letsencrypt \
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# HAProxy docker image
[![Docker Image](https://github.com/openremote/proxy/actions/workflows/proxy.yml/badge.svg)](https://github.com/openremote/proxy/actions/workflows/proxy.yml)

HAProxy docker image with Lets Encrypt SSL auto renewal using certbot.
HAProxy docker image with Lets Encrypt SSL auto renewal using certbot with built in support for wildcard certificates using AWS Route53.

## Paths
* `/deployment/letsencrypt` - Certbot config directory where generated certificates are stored
Expand All @@ -22,6 +22,8 @@ requested (this is a multi-value alternative to DOMAINNAME)
* `KEYCLOAK_HOST` - Hostname of the Keycloak server (default: `keycloak`)
* `KEYCLOAK_PORT` - Web server port of Keycloak server (default `8080`)
* `LOGFILE` - Location of log file for entrypoint script to write to in addition to stdout (default `none`)
* `AWS_ROUTE53_ROLE` - AWS Route53 Role ARN to be assumed when trying to generate wildcard certificates using Route53 DNS zone, specifically for cross account updates (default `none`)
* `LE_EXTRA_ARGS` - Can be used to add additional arguments to the certbot command (default `none`)

## Custom certificate format
Any custom certificate volume mapped into `/etc/haproxy/certs` should be in PEM format and must include the full certificate chain and the private key, i.e.:
Expand Down
81 changes: 81 additions & 0 deletions certbot.lexicon.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
#

set -euf -o pipefail

# ************** USAGE **************
#
# This is an example hook that can be used with Certbot.
#
# Example usage (with certbot-auto and this hook file saved in /root/):
#
# sudo ./certbot-auto -d example.org -d www.example.org -a manual -i nginx --preferred-challenges dns \
# --manual-auth-hook "/root/certbot.default.sh auth" --manual-cleanup-hook "/root/certbot.default.sh cleanup"
#
# This hook requires configuration, continue reading.
#
# ************** CONFIGURATION **************
#
# PROXY_DNS_PROVIDER and PROXY_DNS_PROVIDER_CREDENTIALS must be supplied as environment variables.
#
# PROXY_DNS_PROVIDER:
# Set this to whatever DNS host your domain is using:
#
# route53 cloudflare cloudns cloudxns digitalocean
# dnsimple dnsmadeeasy dnspark dnspod easydns gandi
# glesys godaddy linode luadns memset namecheap namesilo
# nsone ovh pointhq powerdns rackspace rage4 softlayer
# transip vultr yandex zonomi
#
# The full list is in Lexicon's README.
#
# PROXY_DNS_PROVIDER_CREDENTIALS:
# Lexicon needs to know how to authenticate to your DNS Host.
# This will vary from DNS host to host.
# To figure out which flags to use, you can look at the Lexicon help.
# For example, for help with Cloudflare:
#
# lexicon cloudflare -h
#
# Example cloudflare credentials: "--auth-username=MY_USERNAME" "--auth-token=MY_API_KEY"

if [ -z $PROXY_DNS_PROVIDER ]; then
echo "PROXY_DNS_PROVIDER is not set"
exit 1
fi
if [ -z $PROXY_DNS_PROVIDER_CREDENTIALS ]; then
echo "PROXY_DNS_PROVIDER_CREDENTIALS is not set"
exit 1
fi

#
# PROVIDER_UPDATE_DELAY:
# How many seconds to wait after updating your DNS records. This may be required,
# depending on how slow your DNS host is to begin serving new DNS records after updating
# them via the API. 30 seconds is a safe default, but some providers can be very slow
# (e.g. Linode).
#
# Defaults to 30 seconds.
#
if [ -z $PROXY_DNS_PROVIDER_UPDATE_DELAY ]; then
PROXY_DNS_PROVIDER_UPDATE_DELAY=30
fi

# To be invoked via Certbot's --manual-auth-hook
function auth {
lexicon --resolve-zone-name "${PROXY_DNS_PROVIDER}" "${PROXY_DNS_PROVIDER_CREDENTIALS[@]}" \
create "${CERTBOT_DOMAIN}" TXT --name "_acme-challenge.${CERTBOT_DOMAIN}" --content "${CERTBOT_VALIDATION}"

sleep "${PROXY_DNS_PROVIDER_UPDATE_DELAY}"
}

# To be invoked via Certbot's --manual-cleanup-hook
function cleanup {
lexicon --resolve-zone-name "${PROXY_DNS_PROVIDER}" "${PROXY_DNS_PROVIDER_CREDENTIALS[@]}" \
delete "${CERTBOT_DOMAIN}" TXT --name "_acme-challenge.${CERTBOT_DOMAIN}" --content "${CERTBOT_VALIDATION}"
}

HANDLER=$1; shift;
if [ -n "$(type -t $HANDLER)" ] && [ "$(type -t $HANDLER)" = function ]; then
$HANDLER "$@"
fi
3 changes: 0 additions & 3 deletions cli.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,3 @@ rsa-key-size = 4096

agree-tos = true
no-eff-email = true

authenticator = webroot
webroot-path = /etc/haproxy/webroot
76 changes: 58 additions & 18 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
IP_REGEX='(^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$)|(^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$)|(^[^\.]+$)'

# Configure letsencrypt
LE_EXTRA_ARGS=""
if [ -n "${LE_EMAIL}" ]; then
LE_EXTRA_ARGS="${LE_EXTRA_ARGS} --email ${LE_EMAIL}"
else
Expand All @@ -14,7 +13,7 @@ if [ -n "${LE_RSA_KEY_SIZE}" ]; then
LE_EXTRA_ARGS="${LE_EXTRA_ARGS} --rsa-key-size ${LE_RSA_KEY_SIZE}"
fi

LE_CMD="certbot certonly --logs-dir - -w ${CHROOT_DIR} ${LE_EXTRA_ARGS}"
LE_CMD="certbot certonly -n --logs-dir - -w ${CHROOT_DIR} ${LE_EXTRA_ARGS}"

# Configure haproxy
HAPROXY_CMD="haproxy -W -db -f ${HAPROXY_CONFIG} ${HAPROXY_USER_PARAMS}"
Expand Down Expand Up @@ -70,14 +69,26 @@ run_proxy() {
log_info "LUA_PATH: ${LUA_PATH}"
log_info "CERT_DIR: ${CERT_DIR}"
log_info "LE_DIR: ${LE_DIR}"
log_info "LE_CMD: ${LE_CMD}"
log_info "AWS_ROUTE53_ROLE: ${AWS_ROUTE53_ROLE}"

if check_proxy; then

log_info "Starting crond"
crond

if [ -n "${AWS_ROUTE53_ROLE}" ]; then
log_info "Creating AWS CLI config file"
mkdir ~/.aws/config
rm -f ~/.aws/config 2> /dev/null
echo "[default]" >> ~/.aws/config
echo "role_arn = ${AWS_ROUTE53_ROLE}" >> ~/.aws/config
echo "credential_source = Ec2InstanceMetadata" >> ~/.aws/config
echo "" >> ~/.aws/config
fi

cert_init&

log_info "Starting monitoring process"
monitor&

Expand Down Expand Up @@ -129,7 +140,11 @@ add() {
fi

DOMAIN="${1}"
RENEWED_LINEAGE="${LE_DIR}/live/${DOMAIN}"
FNAME="${DOMAIN}"
if [[ ${DOMAIN:0:2} == "*." ]]; then
FNAME="_${DOMAIN:1}"
fi
RENEWED_LINEAGE="${LE_DIR}/live/${FNAME}"
DOMAIN_FOLDER=$RENEWED_LINEAGE

# Basic invalid DOMAIN check
Expand All @@ -153,7 +168,15 @@ add() {
fi
done

eval "$LE_CMD $DOMAIN_ARGS"
# For wildcard domains we use route 53 DNS plugin
if [[ ${DOMAIN:0:2} == "*." ]]; then
log_info "Wildcard domain cert requested, using route53 plugin: ${DOMAIN}"
CMD="${LE_CMD} --dns-route53 --cert-name _${DOMAIN:1}"
else
CMD="${LE_CMD} --webroot"
fi

eval "$CMD $DOMAIN_ARGS"
ret=$?

if [ $ret -ne 0 ]; then
Expand Down Expand Up @@ -181,7 +204,11 @@ renew() {
fi

DOMAIN="${1}"
DOMAIN_FOLDER="${LE_DIR}/live/${DOMAIN}"
FNAME="${DOMAIN}"
if [[ ${DOMAIN:0:2} == "*." ]]; then
FNAME="_${DOMAIN:1}"
fi
DOMAIN_FOLDER="${LE_DIR}/live/${FNAME}"

if [ ! -d "${DOMAIN_FOLDER}" ]; then
log_error "Domain ${DOMAIN} does not exist! Cannot renew it."
Expand All @@ -197,7 +224,7 @@ renew() {
fi
done

eval "$LE_CMD --force-renewal --deploy-hook \"/entrypoint.sh sync-haproxy\" --expand $DOMAIN_ARGS"
eval "$LE_CMD --force-renewal --deploy-hook \"/entrypoint.sh sync-haproxy\" --expand $DOMAIN_ARGS --cert-name $FNAME"

LE_RESULT=$?

Expand Down Expand Up @@ -226,6 +253,10 @@ print_pin() {
fi

DOMAIN="${1}"
FNAME="${DOMAIN}"
if [[ ${DOMAIN:0:2} == "*." ]]; then
FNAME="_${DOMAIN:1}"
fi
DOMAIN_FOLDER="${LE_DIR}/live/${DOMAIN}"

if [ ! -d "${DOMAIN_FOLDER}" ]; then
Expand All @@ -252,9 +283,13 @@ remove() {
fi

DOMAIN=$1
DOMAIN_LIVE_FOLDER="${LE_DIR}/live/${DOMAIN}"
DOMAIN_ARCHIVE_FOLDER="${LE_DIR}/archive/${DOMAIN}"
DOMAIN_RENEWAL_CONFIG="${LE_DIR}/renewal/${DOMAIN}.conf"
FNAME="${DOMAIN}"
if [[ ${DOMAIN:0:2} == "*." ]]; then
FNAME="_${DOMAIN:1}"
fi
DOMAIN_LIVE_FOLDER="${LE_DIR}/live/${FNAME}"
DOMAIN_ARCHIVE_FOLDER="${LE_DIR}/archive/${FNAME}"
DOMAIN_RENEWAL_CONFIG="${LE_DIR}/renewal/${FNAME}.conf"

log_info "Removing domain \"${DOMAIN}\"..."

Expand All @@ -263,11 +298,8 @@ remove() {
return 5
fi

rm -rf "${DOMAIN_LIVE_FOLDER}" || die "Failed to remove domain live directory ${DOMAIN_FOLDER}"
rm -rf "${DOMAIN_ARCHIVE_FOLDER}" || die "Failed to remove domain archive directory ${DOMAIN_ARCHIVE_FOLDER}"
rm -f "${DOMAIN_RENEWAL_CONFIG}" || die "Failed to remove domain renewal config ${DOMAIN_RENEWAL_CONFIG}"
rm -f "${CERT_DIR}/${DOMAIN}" 2>/dev/null

certbot revoke -n --cert-name ${FNAME}
certbot delete -n --cert-name ${FNAME}
log_info "Removed domain \"${DOMAIN}\"..."
}

Expand Down Expand Up @@ -299,14 +331,18 @@ cert_init() {
i=0
for DOMAIN in $DOMAINNAMES; do
i=$((i+1))
if [ ! -d "${LE_DIR}/live/${DOMAIN}" ]; then
FNAME="${DOMAIN}"
if [[ ${DOMAIN:0:2} == "*." ]]; then
FNAME="_${DOMAIN:1}"
fi
if [ ! -d "${LE_DIR}/live/${FNAME}" ]; then
log_info "Initialising certificate for '${DOMAIN}'..."
rm -rf "${LE_DIR}/live/${DOMAIN}" 2>/dev/null
rm -rf "${LE_DIR}/live/${FNAME}" 2>/dev/null
add "${DOMAIN}"
fi
if [ $i -eq 1 ]; then
log_info "Symlinking first domain to built in cert directory to take precedence over self signed cert"
ln -sfT ${CERT_DIR}/${DOMAIN} /etc/haproxy/certs/00-cert
ln -sfT ${CERT_DIR}/${FNAME} /etc/haproxy/certs/00-cert
fi
done
IFS=$IFS_OLD
Expand All @@ -321,10 +357,14 @@ cert_init() {
continue
fi
CERT=$(basename $d)
if [[ ${CERT:0:2} == "_." ]]; then
CERT="*${CERT:1}"
fi
if [[ "$DOMAINNAMES" != "$CERT"* ]] && [[ "$DOMAINNAMES" != *",$CERT"* ]]; then
log_info "Removing obsolete certificate for '$CERT'"
remove "$CERT"
else
CERT=$(basename $d)
RENEWED_LINEAGE="$LE_DIR/live/$CERT"
sync_haproxy
fi
Expand Down

0 comments on commit 7ddaeea

Please sign in to comment.