Skip to content

Commit a13301a

Browse files
committed
Enable YAML configuration
This patch allows configuring nginx-certbot with a config.yml file. In particular this allows to directly declare the certificates that should be requested by certbot with finer granularity compared to the automatic discovery based on the nginx config files. Main motivations: - Currently, since automatic discovery is implemented on a per file basis, all domain names in a file are attached ot all certificates in that file. This means that for e.g. ```nginx server { server_name example.com *.example.com; ssl_certificate /etc/letsencrypt/live/example-com/fullchain.pem; # [...] } server { server_name a.example.com; ssl_certificate /etc/letsencrypt/live/a-example-com/fullchain.pem; # [...] } ``` both `example-com` and `a-example-com` will contain the domain names `example.com`, `*.example.com`, and `a.example.com`. With this patch it is possible to instead do ```yaml certificates: - name: example-com domains: [example.com, *.example.com] - name: a-example-com domains: [a.example.com] ``` - Currently the authenticator credentials can't be specified on a per certificate basis (see e.g. #315). With this patch that is possible: ```yaml certbot: authenticator: dns-cloudflare certificates: - name: example-com domains: [example.com] credentials: /etc/letsencrypt/example-com-cloudflare.ini - name: example-se domains: [example.se] credentials: /etc/letsencrypt/example-se-cloudflare.ini ``` - Authenticator and key type can currently be specified on a per-certificate basis by naming them appropriately. This works okay, but it becomes a bit clunky to support more such per-certificate configurations (such as e.g. the elliptic curve or the authenticator credentials). This patch allows to directly specify everything for each certificate: ```yaml certbot: authenticator: dns-cloudflare key-type: ecdsa certificates: - name: example-com-rsa domains: [example.com] key-type: rsa - name: example-com domains: [example.com] ```
1 parent 763d267 commit a13301a

File tree

3 files changed

+220
-86
lines changed

3 files changed

+220
-86
lines changed

examples/config.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Configuration for this docker image
2+
nginx-certbot:
3+
dhparam-size: 2048
4+
renewal-interval: 8d
5+
debug: false
6+
7+
# Certbot parameters.
8+
certbot:
9+
# Default certbot authenticator (see certbots --authenticator flag). Falls back to the
10+
# CERTBOT_AUTHENTICATOR environment variable or, if that is unset, to 'webroot'. The
11+
# authenticator can be overriden on the certificate level.
12+
authenticator: webroot
13+
# Default certbot authenticator credentials (see certbots --<authenticator>-credentials
14+
# flag). This is required for the various DNS authenticators. Falls back to
15+
# '/etc/letsencrypt/<authenticator>.ini'.
16+
credentials: ''
17+
# Default elliptic curve (see certbots --elliptic-curve flag). Falls back to the
18+
# ELLIPTIC_CURVE environment variable or, if that is unset, to 'secp256r1'.
19+
elliptic-curve: secp256r1
20+
# Default key type (see certbots --key-type flag). Falls back to 'ecdsa' (or if
21+
# USE_ECDSA=0 to 'rsa'). The key type can be overriden on the certificate level.
22+
key-type: ecdsa
23+
# Default RSA key size (see certbots --rsa-key-size flag). Falls back to the RSA_KEY_SIZE
24+
# environment variable or, if that is unset, to 2048. The key size can be overriden on the
25+
# certificate level.
26+
rsa-key-size: 2048
27+
28+
# Array of certificate specifications.
29+
# If the 'certificates' key exist (even if the array is empty) the automatic discovery of
30+
# certificate names and domains is disabled and instead nginx-certbot will request
31+
# certificates based on the specifications in the array.
32+
# A minimum requirement for each certificate is to specifiy 'name' and 'domains'.
33+
certificates:
34+
# Certificate name (see certbots --cert-name flag). Generated certificates will be
35+
# placed in the /etc/letsencrypt/live/<certificate name>/ folder. This is a required
36+
# parameter.
37+
- name: example-com
38+
# Required list of domains for which the certificate should be valid for (see certbots
39+
# --domain flag). This is a required parameter.
40+
domains: ["a.example.com", "b.example.com", "*.c.example.com"]
41+
# Authenticator to use for this certificate. Falls back to certbot.authenticator.
42+
authenticator: ''
43+
# Credential file for this certificates authenticator. Falls back to
44+
# certbot.credentials.
45+
credentials: ''
46+
# Key type for the certificate. Falls back to certbot.key-type.
47+
key-type: ''
48+
# RSA key size for the certificate. Falls back to certbot.rsa-key-size.
49+
rsa-key-size: ''

src/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ RUN set -ex && \
7171
pip3 install -r /requirements.txt && \
7272
# And the supported extra authenticators.
7373
pip3 install $(echo $CERTBOT_DNS_AUTHENTICATORS | sed 's/\(^\| \)/\1certbot-dns-/g') && \
74+
# Install shyaml
75+
pip3 install shyaml && \
7476
# Remove everything that is no longer necessary.
7577
apt-get remove --purge -y \
7678
build-essential \

src/scripts/run_certbot.sh

Lines changed: 169 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,60 @@
11
#!/bin/bash
22
set -e
33

4-
# URLs used when requesting certificates.
5-
# These are picked up from the environment if they are set, which enables
6-
# advanced usage of custom ACME servers, else it will use the default Let's
7-
# Encrypt servers defined here.
8-
: "${CERTBOT_PRODUCTION_URL=https://acme-v02.api.letsencrypt.org/directory}"
9-
: "${CERTBOT_STAGING_URL=https://acme-staging-v02.api.letsencrypt.org/directory}"
10-
114
# Source in util.sh so we can have our nice tools.
125
. "$(cd "$(dirname "$0")"; pwd)/util.sh"
136

147
info "Starting certificate renewal process"
158

9+
# If we have a config file we parse it and let definitions within take
10+
# precedence over any environment variables.
11+
config_file="${NGINX_CERTBOT_CONFIG_FILE:-/etc/nginx-certbot/config.yml}"
12+
if [ -f "${config_file}" ]; then
13+
certbot_authenticator="$(shyaml get-value certbot.authenticator '' < "${config_file}")"
14+
certbot_elliptic_curve="$(shyaml get-value certbot.elliptic-curve '' < "${config_file}")"
15+
certbot_email="$(shyaml get-value certbot.email '' < "${config_file}")"
16+
certbot_key_type="$(shyaml get-value certbot.key-type '' < "${config_file}")"
17+
certbot_rsa_key_size="$(shyaml get-value certbot.rsa-key-size '' < "${config_file}")"
18+
certbot_staging="$(shyaml get-value certbot.staging '' < "${config_file}")"
19+
certbot_production_url="$(shyaml get-value certbot.production_url '' < "${config_file}")"
20+
certbot_staging_url="$(shyaml get-value certbot.staging_url '' < "${config_file}")"
21+
fi
22+
23+
# Environment variable fallbacks
24+
: "${certbot_authenticator:=${CERTBOT_AUTHENTICATOR:-webroot}}"
25+
: "${certbot_elliptic_curve:=${ELLIPTIC_CURVE:-secp256r1}}"
26+
: "${certbot_email:=${CERTBOT_EMAIL}}"
27+
: "${certbot_key_type:=$( [[ ${USE_ECDSA} == "0" ]] && echo "rsa" || echo "ecdsa")}"
28+
: "${certbot_rsa_key_size:=${RSA_KEY_SIZE:-2048}}"
29+
: "${certbot_staging:=${STAGING}}"
30+
31+
# URLs used when requesting certificates.
32+
# These are picked up from the environment if they are set, which enables
33+
# advanced usage of custom ACME servers, else it will use the default Let's
34+
# Encrypt servers defined here.
35+
: "${certbot_production_url:=${CERTBOT_PRODUCTION_URL:-https://acme-v02.api.letsencrypt.org/directory}}"
36+
: "${certbot_staging_url:=${CERTBOT_STAGING_URL:-https://acme-staging-v02.api.letsencrypt.org/directory}}"
37+
1638
# We require an email to be able to request a certificate.
17-
if [ -z "${CERTBOT_EMAIL}" ]; then
39+
if [ -z "${certbot_email}" ]; then
1840
error "CERTBOT_EMAIL environment variable undefined; certbot will do nothing!"
1941
exit 1
2042
fi
2143

44+
# Log the global defaults we have resolved so far
45+
debug "Configuration resolved from config file and environment variables:"
46+
for var in certbot_authenticator certbot_elliptic_curve certbot_email certbot_key_type \
47+
certbot_rsa_key_size certbot_staging certbot_production_url certbot_staging_url; do
48+
debug " - ${var}=${!var}"
49+
done
50+
2251
# Use the correct challenge URL depending on if we want staging or not.
23-
if [ "${STAGING}" = "1" ]; then
52+
if [ "${certbot_staging}" = "1" ]; then
2453
debug "Using staging environment"
25-
letsencrypt_url="${CERTBOT_STAGING_URL}"
54+
letsencrypt_url="${certbot_staging_url}"
2655
else
2756
debug "Using production environment"
28-
letsencrypt_url="${CERTBOT_PRODUCTION_URL}"
29-
fi
30-
31-
# Ensure that an RSA key size is set.
32-
if [ -z "${RSA_KEY_SIZE}" ]; then
33-
debug "RSA_KEY_SIZE unset, defaulting to 2048"
34-
RSA_KEY_SIZE=2048
35-
fi
36-
37-
# Ensure that an elliptic curve is set.
38-
if [ -z "${ELLIPTIC_CURVE}" ]; then
39-
debug "ELLIPTIC_CURVE unset, defaulting to 'secp256r1'"
40-
ELLIPTIC_CURVE="secp256r1"
57+
letsencrypt_url="${certbot_production_url}"
4158
fi
4259

4360
if [ "${1}" = "force" ]; then
@@ -53,8 +70,17 @@ fi
5370
# $2: String with all requested domains (e.g. -d domain.org -d www.domain.org)
5471
# $3: Type of key algorithm to use (rsa or ecdsa)
5572
# $4: The authenticator to use to solve the challenge
73+
# $5: The RSA key size (--rsa-key-size)
74+
# $6: The elliptic curve (--elliptic-curve)
75+
# $7: Credentials file for the authenticator
5676
get_certificate() {
77+
local cert_name="${1}"
78+
local domain_request="${2}"
79+
local key_type="${3}"
5780
local authenticator="${4,,}"
81+
local rsa_key_size="${5:-certbot_rsa_key_size}"
82+
local elliptic_curve="${6:-certbot_elliptic_curve}"
83+
local credentials="${7}"
5884
local authenticator_params=""
5985
local challenge_type=""
6086

@@ -72,9 +98,9 @@ get_certificate() {
7298
return 1
7399
fi
74100
else
75-
local configfile="/etc/letsencrypt/${authenticator#dns-}.ini"
101+
local configfile="${credentials:-/etc/letsencrypt/${authenticator#dns-}.ini}"
76102
if [ ! -f "${configfile}" ]; then
77-
error "Authenticator is '${authenticator}' but '${configfile}' is missing"
103+
error "Authenticator '${authenticator}' requires credentials but '${configfile}' is missing"
78104
return 1
79105
fi
80106
authenticator_params="--${authenticator}-credentials=${configfile}"
@@ -84,86 +110,143 @@ get_certificate() {
84110
authenticator_params="${authenticator_params} --${authenticator}-propagation-seconds=${CERTBOT_DNS_PROPAGATION_SECONDS}"
85111
fi
86112
else
87-
error "Unknown authenticator '${authenticator}' for '${1}'"
113+
error "Unknown authenticator '${authenticator}' for '${cert_name}'"
88114
return 1
89115
fi
90116

91-
info "Requesting an ${3^^} certificate for '${1}' (${challenge_type} through ${authenticator})"
117+
info "Requesting an ${key_type^^} certificate for '${cert_name}' (${challenge_type} through ${authenticator})"
92118
certbot certonly \
93119
--agree-tos --keep -n --text \
94120
--preferred-challenges ${challenge_type} \
95121
--authenticator ${authenticator} \
96122
${authenticator_params} \
97-
--email "${CERTBOT_EMAIL}" \
123+
--email "${certbot_email}" \
98124
--server "${letsencrypt_url}" \
99-
--rsa-key-size "${RSA_KEY_SIZE}" \
100-
--elliptic-curve "${ELLIPTIC_CURVE}" \
101-
--key-type "${3}" \
102-
--cert-name "${1}" \
103-
${2} \
125+
--rsa-key-size "${rsa_key_size}" \
126+
--elliptic-curve "${elliptic_curve}" \
127+
--key-type "${key_type}" \
128+
--cert-name "${cert_name}" \
129+
${domain_request} \
104130
--debug ${force_renew}
105131
}
106132

107133
# Get all the cert names for which we should create certificate requests and
108134
# have them signed, along with the corresponding server names.
109-
#
110-
# This will return an associative array that looks something like this:
111-
# "cert_name" => "server_name1 server_name2"
112-
declare -A certificates
113-
for conf_file in /etc/nginx/conf.d/*.conf*; do
114-
parse_config_file "${conf_file}" certificates
115-
done
135+
# If we have a config file we request certificates based on the specifications
136+
# within that file otherwise we parse the nginx config files to automatically
137+
# discover certificate names, key types, authenticators, and domains.
138+
if [ -f "${config_file}" ]; then
139+
debug "Using config file '${config_file}' for certificate specifications"
140+
# Loop over the certificates array and request the certificates
141+
while read -r -d '' cert; do
142+
debug "Parsing certificate specification"
116143

117-
# Iterate over each key and make a certificate request for them.
118-
for cert_name in "${!certificates[@]}"; do
119-
server_names=(${certificates["$cert_name"]})
120-
121-
# Determine which type of key algorithm to use for this certificate
122-
# request. Having the algorithm specified in the certificate name will
123-
# take precedence over the environmental variable.
124-
if [[ "${cert_name,,}" =~ (^|[-.])ecdsa([-.]|$) ]]; then
125-
debug "Found variant of 'ECDSA' in name '${cert_name}"
126-
key_type="ecdsa"
127-
elif [[ "${cert_name,,}" =~ (^|[-.])ecc([-.]|$) ]]; then
128-
debug "Found variant of 'ECC' in name '${cert_name}"
129-
key_type="ecdsa"
130-
elif [[ "${cert_name,,}" =~ (^|[-.])rsa([-.]|$) ]]; then
131-
debug "Found variant of 'RSA' in name '${cert_name}"
132-
key_type="rsa"
133-
elif [ "${USE_ECDSA}" == "0" ]; then
134-
key_type="rsa"
135-
else
136-
key_type="ecdsa"
137-
fi
144+
# cert-name (required)
145+
cert_name="$(shyaml get-value cert-name '' <<<"${cert}")"
146+
if [ -z "${cert_name}" ]; then
147+
error "'cert-name' is missing; ignoring this certificate specification"
148+
continue
149+
fi
150+
debug "Certificate cert-name is: ${cert_name}"
138151

139-
# Determine the authenticator to use to solve the authentication challenge.
140-
# Having the authenticator specified in the certificate name will take
141-
# precedence over the environmental variable.
142-
if [[ "${cert_name,,}" =~ (^|[-.])webroot([-.]|$) ]]; then
143-
authenticator="webroot"
144-
debug "Found mention of 'webroot' in name '${cert_name}"
145-
elif [[ "${cert_name,,}" =~ (^|[-.])(dns-($(echo ${CERTBOT_DNS_AUTHENTICATORS} | sed 's/ /|/g')))([-.]|$) ]]; then
146-
authenticator=${BASH_REMATCH[2]}
147-
debug "Found mention of authenticator '${authenticator}' in name '${cert_name}'"
148-
elif [ -n "${CERTBOT_AUTHENTICATOR}" ]; then
149-
authenticator="${CERTBOT_AUTHENTICATOR}"
150-
else
151-
authenticator="webroot"
152-
fi
152+
# domains (required)
153+
domains=()
154+
while read -r -d '' domain; do
155+
domains+=("${domain}")
156+
done < <(shyaml get-values-0 domains '' <<<"${cert}")
157+
if [ "${#domains[@]}" -eq 0 ]; then
158+
error "'domains' are missing; ignoring this certificate specification"
159+
continue
160+
fi
161+
debug "Certificate domains are is: ${domains[*]}"
162+
domain_request=""
163+
for domain in "${domains[@]}"; do
164+
domain_request+=" --domain ${domain}"
165+
done
166+
debug "Certificate domain request is: ${domain_request}"
167+
168+
# key-type (optional)
169+
key_type=$(shyaml get-value key-type "${certbot_key_type}" <<<"${cert}")
170+
debug "Certificate key-type is: ${key_type}"
171+
172+
# authenticator (optional)
173+
authenticator=$(shyaml get-value authenticator "${certbot_authenticator}" <<<"${cert}")
174+
debug "Certificate authenticator is: ${authenticator}"
175+
176+
# credentials (optional)
177+
credentials=$(shyaml get-value credentials '' <<<"${cert}")
178+
debug "Certificate authenticator credentials is: ${credentials}"
153179

154-
# Assemble the list of domains to be included in the request from
155-
# the parsed 'server_names'
156-
domain_request=""
157-
for server_name in "${server_names[@]}"; do
158-
domain_request="${domain_request} -d ${server_name}"
180+
# rsa-key-size (optional)
181+
rsa_key_size=$(shyaml get-value rsa-key-size "${certbot_rsa_key_size}" <<<"${cert}")
182+
debug "Certificate RSA key size is: ${rsa_key_size}"
183+
184+
# elliptic-curve (optional)
185+
elliptic_curve=$(shyaml get-value elliptic-curve "${certbot_elliptic_curve}" <<<"${cert}")
186+
debug "Certificate elliptic curve is: ${elliptic_curve}"
187+
188+
# Hand over all the info required for the certificate request, and
189+
# let certbot decide if it is necessary to update the certificate.
190+
if ! get_certificate "${cert_name}" "${domain_request}" "${key_type}" "${authenticator}" "${rsa_key_size}" "${elliptic_curve}" "${credentials}"; then
191+
error "Certbot failed for '${cert_name}'. Check the logs for details."
192+
fi
193+
done < <(shyaml -y get-values-0 certificates '' < ${config_file})
194+
else
195+
debug "Using automatic discovery of nginx conf file for certificate specifications"
196+
# This will return an associative array that looks something like this:
197+
# "cert_name" => "server_name1 server_name2"
198+
declare -A certificates
199+
for conf_file in /etc/nginx/conf.d/*.conf*; do
200+
parse_config_file "${conf_file}" certificates
159201
done
160202

161-
# Hand over all the info required for the certificate request, and
162-
# let certbot decide if it is necessary to update the certificate.
163-
if ! get_certificate "${cert_name}" "${domain_request}" "${key_type}" "${authenticator}"; then
164-
error "Certbot failed for '${cert_name}'. Check the logs for details."
165-
fi
166-
done
203+
# Iterate over each key and make a certificate request for them.
204+
for cert_name in "${!certificates[@]}"; do
205+
server_names=(${certificates["$cert_name"]})
206+
207+
# Determine which type of key algorithm to use for this certificate
208+
# request. Having the algorithm specified in the certificate name will
209+
# take precedence over the environmental variable.
210+
if [[ "${cert_name,,}" =~ (^|[-.])ecdsa([-.]|$) ]]; then
211+
debug "Found variant of 'ECDSA' in name '${cert_name}"
212+
key_type="ecdsa"
213+
elif [[ "${cert_name,,}" =~ (^|[-.])ecc([-.]|$) ]]; then
214+
debug "Found variant of 'ECC' in name '${cert_name}"
215+
key_type="ecdsa"
216+
elif [[ "${cert_name,,}" =~ (^|[-.])rsa([-.]|$) ]]; then
217+
debug "Found variant of 'RSA' in name '${cert_name}"
218+
key_type="rsa"
219+
else
220+
key_type="${certbot_key_type}"
221+
fi
222+
223+
# Determine the authenticator to use to solve the authentication challenge.
224+
# Having the authenticator specified in the certificate name will take
225+
# precedence over the environmental variable.
226+
if [[ "${cert_name,,}" =~ (^|[-.])webroot([-.]|$) ]]; then
227+
authenticator="webroot"
228+
debug "Found mention of 'webroot' in name '${cert_name}"
229+
elif [[ "${cert_name,,}" =~ (^|[-.])(dns-($(echo ${CERTBOT_DNS_AUTHENTICATORS} | sed 's/ /|/g')))([-.]|$) ]]; then
230+
authenticator=${BASH_REMATCH[2]}
231+
debug "Found mention of authenticator '${authenticator}' in name '${cert_name}'"
232+
else
233+
authenticator="${certbot_authenticator}"
234+
fi
235+
236+
# Assemble the list of domains to be included in the request from
237+
# the parsed 'server_names'
238+
domain_request=""
239+
for server_name in "${server_names[@]}"; do
240+
domain_request="${domain_request} -d ${server_name}"
241+
done
242+
243+
# Hand over all the info required for the certificate request, and
244+
# let certbot decide if it is necessary to update the certificate.
245+
if ! get_certificate "${cert_name}" "${domain_request}" "${key_type}" "${authenticator}"; then
246+
error "Certbot failed for '${cert_name}'. Check the logs for details."
247+
fi
248+
done
249+
fi
167250

168251
# After trying to get all our certificates, auto enable any configs that we
169252
# did indeed get certificates for.

0 commit comments

Comments
 (0)