Skip to content

Commit

Permalink
Merge pull request #295 from dokku/dns-01-challenge
Browse files Browse the repository at this point in the history
Add support for wildcard domains via DNS-01 challenge
  • Loading branch information
josegonzalez authored Jan 28, 2023
2 parents 61f09b4 + b1fccec commit 1904043
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 40 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,16 @@ dokku letsencrypt:cron-job --add

Variable | Default | Description
---------------------|-------------------|-------------------------------------------------------------------------
`dns-provicer` | (none) | The name of a [valid lego dns-provider](https://go-acme.github.io/lego/dns/)
`email` | (none) | **REQUIRED:** E-mail address to use for registering with Let's Encrypt.
`graceperiod` | 2592000 (30 days) | Time in seconds left on a certificate before it should get renewed
`lego-docker-args` | (none) | Extra arguments to pass via `docker run`. See the [lego CLI documentation](https://go-acme.github.io/lego/usage/cli/) for available options.
`server` | default | Which ACME server to use. Can be 'default', 'staging' or a URL

You can set a setting using `dokku letsencrypt:set $APP $SETTING_NAME $SETTING_VALUE`. When looking for a setting, the plugin will first look if it was defined for the current app and fall back to settings defined by `--global`.

> Note: See "DNS-01 Challenge" for more information on configuration a dns-provider for DNS-01 based challenges and wildcard support.
## Redirecting from HTTP to HTTPS

Dokku's default nginx template will automatically redirect HTTP requests to HTTPS when a certificate is present.
Expand Down Expand Up @@ -180,6 +183,27 @@ dokku domains:add 00-default mydomain.com
dokku letsencrypt:enable 00-default
```

## DNS-01 Challenge

> Functionality sponsored by [Orca Scan Ltd](https://orcascan.com/).
In order to provide a Letsencrypt certificate for a wildcard domain, a DNS-01 challenge must be used. To configure, the `dns-provider` property must be set to a [supported Lego provider](https://go-acme.github.io/lego/dns/). Additionally, the environment variables used by the DNS provider must be set as letsencrypt properties with the prefix `dns-provider-`. Both global and app-specific properties are supported.

> Warning: Before using a DNS-based challenge, ensure all DNS records - including wildcard records - are pointing at your server.
```shell
# set the provider to namecheap
dokku letsencrypt:set --global dns-provider namecheap

# set the properties necessary for namecheap usage
dokku letsencrypt:set --global dns-provider-NAMECHEAP_API_USER user
dokku letsencrypt:set --global dns-provider-NAMECHEAP_API_KEY key
```

Due to limitations in how certain DNS providers work, environment variables _must not_ use the `_FILE` based method for referring to values in files.

Please see the Lego documentation for your DNS provider for more information on what configuration is necessary to utilize DNS-01 challenges.

## Conditional enabling

`dokku letsencrypt:enable <app>` enables letsencrypt for an application or renews the certificate. This may lead to hitting rate limits with letsencrypt.
Expand Down
9 changes: 6 additions & 3 deletions command-functions
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ cmd-letsencrypt-report-single() {
local flag_map=(
"--letsencrypt-active: $(fn-letsencrypt-is-active "$APP")"
"--letsencrypt-autorenew: $(fn-letsencrypt-is-autorenew-enabled "$APP")"
"--letsencrypt-computed-dns-provider: $(fn-letsencrypt-computed-dns-provider "$APP")"
"--letsencrypt-global-dns-provider: $(fn-letsencrypt-global-dns-provider)"
"--letsencrypt-dns-provider: $(fn-letsencrypt-dns-provider "$APP")"
"--letsencrypt-computed-email: $(fn-letsencrypt-computed-email "$APP")"
"--letsencrypt-global-email: $(fn-letsencrypt-global-email)"
"--letsencrypt-email: $(fn-letsencrypt-email "$APP")"
Expand Down Expand Up @@ -290,13 +293,13 @@ cmd-letsencrypt-set() {
declare cmd="letsencrypt:set"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1" KEY="$2" VALUE="$3"
local VALID_KEYS=("email" "graceperiod" "server" "lego-docker-args")
local VALID_KEYS=("dns-provider" "email" "graceperiod" "server" "lego-docker-args")
[[ "$APP" == "--global" ]] || verify_app_name "$APP"

[[ -z "$KEY" ]] && dokku_log_fail "No key specified"

if ! fn-in-array "$KEY" "${VALID_KEYS[@]}"; then
dokku_log_fail "Invalid key specified, valid keys include: email, graceperiod, server, lego-docker-args"
if ! fn-in-array "$KEY" "${VALID_KEYS[@]}" && [[ "$KEY" != dns-provider-* ]]; then
dokku_log_fail "Invalid key specified, valid keys include: dns-provider, dns-provider-*, email, graceperiod, server, lego-docker-args"
fi

if [[ -n "$VALUE" ]]; then
Expand Down
143 changes: 106 additions & 37 deletions internal-functions
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,26 @@ set -eo pipefail

fn-letsencrypt-acme-execute-challenge() {
declare desc="perform actual ACME validation procedure"
declare app="$1"
declare APP="$1"
local FAKE_NGINX_CONF=false
local config_dir
local challenge_mode config_dir

if [[ ! -f "$DOKKU_ROOT/$app/nginx.conf" ]]; then
if [[ ! -f "$DOKKU_ROOT/$APP/nginx.conf" ]]; then
FAKE_NGINX_CONF=true
fi

fn-letsencrypt-create-root "$app"
fn-letsencrypt-create-root "$APP"

challenge_mode="HTTP-01"
dns_provider="$(fn-letsencrypt-computed-dns-provider "$APP")"
if [[ -n "$dns_provider" ]]; then
challenge_mode="DNS-01"
fi

dokku_log_info1 "Getting letsencrypt certificate for ${app}..."
dokku_log_info1 "Getting letsencrypt certificate for ${APP} via ${challenge_mode}"

# read arguments from appropriate config file into the config array
config_dirs="$(fn-letsencrypt-configure-and-get-dir "$app")"
config_dirs="$(fn-letsencrypt-configure-and-get-dir "$APP")"
host_config_dir="$(echo "$config_dirs" | cut -d: -f1)"
container_config_dir="$(echo "$config_dirs" | cut -d: -f2)"
read -r -a config <"$container_config_dir/config"
Expand All @@ -34,6 +40,7 @@ fn-letsencrypt-acme-execute-challenge() {
export DOKKU_GID=$(id -g)
mkdir -p "$DOKKU_LIB_ROOT/data/letsencrypt/$APP"
docker run --rm \
--env-file "$host_config_dir/docker.env" \
--user $DOKKU_UID:$DOKKU_GID \
-v "$host_config_dir:/certs" \
-v "$DOKKU_LIB_ROOT/data/letsencrypt/$APP:/webroot" \
Expand All @@ -44,7 +51,7 @@ fn-letsencrypt-acme-execute-challenge() {
set -e

if [[ "$FAKE_NGINX_CONF" == "true" ]]; then
rm "$DOKKU_ROOT/$app/nginx.conf"
rm "$DOKKU_ROOT/$APP/nginx.conf"
fi

if [[ $exit_code != 0 ]]; then
Expand All @@ -54,21 +61,27 @@ fn-letsencrypt-acme-execute-challenge() {

# got certificate
dokku_log_info1 "Certificate retrieved successfully."
fn-letsencrypt-symlink-certs "$app" "$container_config_dir"
plugn trigger proxy-build-config "$app"
fn-letsencrypt-symlink-certs "$APP" "$container_config_dir"
plugn trigger proxy-build-config "$APP"
}

fn-letsencrypt-acme-revoke() {
declare desc="perform actual certificate revocation"
local app="$1"
local APP="$1"

fn-letsencrypt-create-root "$app"
fn-letsencrypt-create-root "$APP"

dokku_log_info1 "Revoking letsencrypt certificate for ${app}..."
challenge_mode="HTTP-01"
dns_provider="$(fn-letsencrypt-computed-dns-provider "$APP")"
if [[ -n "$dns_provider" ]]; then
challenge_mode="DNS-01"
fi

dokku_log_info1 "Revoking letsencrypt certificate for ${APP} via ${challenge_mode}"
local acme_port=$(get_available_port)

# read arguments from appropriate config file into the config array
config_dirs="$(fn-letsencrypt-configure-and-get-dir "$app")"
config_dirs="$(fn-letsencrypt-configure-and-get-dir "$APP")"
host_config_dir="$(echo "$config_dirs" | cut -d: -f1)"
container_config_dir="$(echo "$config_dirs" | cut -d: -f2)"
read -r -a config <"$container_config_dir/config"
Expand All @@ -80,6 +93,7 @@ fn-letsencrypt-acme-revoke() {
export DOKKU_GID=$(id -g)
mkdir -p "$DOKKU_LIB_ROOT/data/letsencrypt/$APP"
docker run --rm \
--env-file "$host_config_dir/docker.env" \
--user $DOKKU_UID:$DOKKU_GID \
-p "$acme_port:$acme_port" \
-v "$host_config_dir:/certs" \
Expand All @@ -100,24 +114,24 @@ fn-letsencrypt-acme-revoke() {
return
fi

local domain="$(get_app_domains "$app" | xargs | awk '{print $1}')"
local domain="$(get_app_domains "$APP" | xargs | awk '{print $1}')"

# removing the certificate will automatically reconfigure nginx
if [[ -z $DOKKU_APP_NAME ]]; then
dokku certs:remove "$app"
dokku certs:remove "$APP"
else
dokku certs:remove
fi
}

fn-letsencrypt-acme-proxy-disable() {
declare desc="disable ACME proxy for an app"
local app="$1"
local APP="$1"

local app_root="$DOKKU_ROOT/$app"
local app_root="$DOKKU_ROOT/$APP"
local app_config_dir="$app_root/nginx.conf.d"

dokku_log_info1 "Disabling ACME proxy for $app..."
dokku_log_info1 "Disabling ACME proxy for $APP..."

[[ -f "$app_config_dir/letsencrypt.conf" ]] && rm "$app_config_dir/letsencrypt.conf"

Expand All @@ -126,25 +140,50 @@ fn-letsencrypt-acme-proxy-disable() {

fn-letsencrypt-acme-proxy-enable() {
declare desc="enable ACME proxy for an app"
local app="$1"
local APP="$1"

local app_root="$DOKKU_ROOT/$app"
local app_root="$DOKKU_ROOT/$APP"
local app_config_dir="$app_root/nginx.conf.d"

dokku_log_info1 "Enabling ACME proxy for ${app}..."
dokku_log_info1 "Enabling ACME proxy for ${APP}..."

# ensure the nginx.conf.d directory exists
[[ -d "$app_config_dir" ]] || mkdir "$app_config_dir"

# generate letsencrypt config
sigil -f "$PLUGIN_AVAILABLE_PATH/letsencrypt/templates/letsencrypt.conf.sigil" \
APP="$app" \
APP="$APP" \
DOKKU_LIB_ROOT="$DOKKU_LIB_ROOT" \
>"$app_config_dir/letsencrypt.conf"

restart_nginx | sed "s/^/ /"
}

fn-letsencrypt-computed-dns-provider() {
declare desc="get configured dns provider"
declare APP="$1"

value="$(fn-letsencrypt-dns-provider "$APP")"
if [[ -z "$value" ]]; then
value="$(fn-letsencrypt-global-dns-provider)"
fi

echo "$value"
}

fn-letsencrypt-global-dns-provider() {
declare desc="get configured dns provider"

fn-plugin-property-get-default "letsencrypt" "--global" "dns-provider" ""
}

fn-letsencrypt-dns-provider() {
declare desc="get configured dns provider"
declare APP="$1"

fn-plugin-property-get-default "letsencrypt" "$APP" "dns-provider" ""
}

fn-letsencrypt-computed-lego-docker-args() {
declare desc="get configured lego docker args"
declare APP="$1"
Expand Down Expand Up @@ -186,6 +225,7 @@ fn-letsencrypt-check-email() {
fn-letsencrypt-configure-and-get-dir() {
declare desc="assemble lego command line arguments and create a config hash directory for them"
declare APP="$1"
local config config_dir config_hash dns_provider domain_args domains email extra_args graceperiod key server value

local app_root="$DOKKU_ROOT/$APP"
local le_root="$app_root/letsencrypt"
Expand All @@ -195,25 +235,56 @@ fn-letsencrypt-configure-and-get-dir() {
# this will be used to determine the folder name for the account key and certificates

# get the selected ACME server
local server="$(fn-letsencrypt-computed-server "$APP")"
server="$(fn-letsencrypt-computed-server "$APP")"

# construct domain arguments
local domains="$(get_app_domains "$APP")"
local domain_args=''
domains="$(get_app_domains "$APP")"
domain_args=''
for domain in $domains; do
dokku_log_verbose " - Domain '$domain'" >&2
domain_args="$domain_args --domains $domain"
done

local graceperiod="$(fn-letsencrypt-computed-graceperiod "$APP")"
local email="$(fn-letsencrypt-computed-email "$APP")"
local extra_args="$(fn-letsencrypt-computed-lego-docker-args "$APP")"
local config="--pem --http --http.webroot /webroot --accept-tos --cert.timeout $graceperiod --path /certs --server $server --email $email $extra_args $domain_args"
graceperiod="$(fn-letsencrypt-computed-graceperiod "$APP")"
email="$(fn-letsencrypt-computed-email "$APP")"
extra_args="$(fn-letsencrypt-computed-lego-docker-args "$APP")"
config="--pem --accept-tos --cert.timeout $graceperiod --path /certs --server $server --email $email $extra_args $domain_args"

dns_provider="$(fn-letsencrypt-computed-dns-provider "$APP")"
if [[ -n "$dns_provider" ]]; then
config="--dns $dns_provider $config"
else
config="--http --http.webroot /webroot $config"
fi

local config_hash=$(echo "$config" | sha1sum | awk '{print $1}')
local config_dir="$le_root/certs/$config_hash"
config_hash=$(echo "$config" | sha1sum | awk '{print $1}')
config_dir="$le_root/certs/$config_hash"
mkdir -p "$config_dir"

rm -f "$config_dir/docker.env"
touch "$config_dir/docker.env"
if [[ -n "$dns_provider" ]]; then
fn-plugin-property-get-all "letsencrypt" "--global" | while read -r line; do
[[ -n "$line" ]] || continue
key="$(cut -d" " -f1 <<<"$line")"
if [[ "$key" == dns-provider-* ]]; then
value="$(cut -d" " -f2 <<<"$line")"
echo "${key#"dns-provider-"}=$value" >>"$config_dir/docker.env"
fi
done
fn-plugin-property-get-all "letsencrypt" "$APP" | while read -r line; do
[[ -n "$line" ]] || continue
key="$(cut -d" " -f1 <<<"$line")"
if [[ "$key" == dns-provider-* ]]; then
value="$(cut -d" " -f2 <<<"$line")"
echo "${key#"dns-provider-"}=$value" >>"$config_dir/docker.env"
fi
done
fi

# ensure the permissions are set correctly on anything that may expose api keys
chmod 0755 "$config_dir/docker.env"

# store config settings
echo "$config" >"$config_dir/config"

Expand Down Expand Up @@ -526,11 +597,9 @@ fn-letsencrypt-server() {

fn-letsencrypt-symlink-certs() {
declare desc="symlink let's encrypt certificates so they can be found by dokku"
declare APP="$1" config_dir="$2"

local app="$1"
local config_dir="$2"

local app_root="$DOKKU_ROOT/$app"
local app_root="$DOKKU_ROOT/$APP"
local le_root="$app_root/letsencrypt"
local domain

Expand All @@ -541,8 +610,8 @@ fn-letsencrypt-symlink-certs() {

# install the let's encrypt certificate for the app
unset DOKKU_APP_NAME
domain="$(get_app_domains "$app" | xargs | awk '{print $1}')"
dokku certs:add "$app" "$config_dir/certificates/$domain.pem" "$config_dir/certificates/$domain.key"
domain="$(get_app_domains "$APP" | xargs | awk '{print $1}')"
dokku certs:add "$APP" "$config_dir/certificates/$domain.pem" "$config_dir/certificates/$domain.key"
rm -f "$app_root/tls/server.letsencrypt.crt" "$app_root/tls/server.crt"
cp "$config_dir/certificates/$domain.crt" "$app_root/tls/server.letsencrypt.crt"
cp "$config_dir/certificates/$domain.crt" "$app_root/tls/server.crt"
Expand Down
1 change: 1 addition & 0 deletions plugin.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[plugin]
description = "Automated installation of let's encrypt TLS certificates"
version = "0.19.0"
sponsors = ["orca-scan"]
[plugin.config]

0 comments on commit 1904043

Please sign in to comment.