Skip to content

Commit e8facb6

Browse files
authored
Merge pull request #654 from nginx-proxy/dev
Merge standalone certificate feature to master
2 parents 7e3d341 + 478f3a1 commit e8facb6

File tree

11 files changed

+164
-18
lines changed

11 files changed

+164
-18
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ nginx.tmpl
99
test/local_test_env.sh
1010
test/tests/docker_api/expected-std-out.txt
1111
test/tests/container_restart/docker_event_out.txt
12+
test/tests/certs_standalone/letsencrypt_user_data

app/entrypoint.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ if [[ "$*" == "/bin/bash /app/start.sh" ]]; then
161161
check_writable_directory '/etc/nginx/certs'
162162
check_writable_directory '/etc/nginx/vhost.d'
163163
check_writable_directory '/usr/share/nginx/html'
164+
[[ -f /app/letsencrypt_user_data ]] && check_writable_directory '/etc/nginx/conf.d'
164165
check_deprecated_env_var
165166
check_default_cert_key
166167
check_dh_group

app/functions.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,40 @@ function add_location_configuration {
102102
fi
103103
}
104104

105+
function add_standalone_configuration {
106+
local domain="${1:?}"
107+
if grep -q "$domain" "/etc/nginx/conf.d/default.conf"; then
108+
# If the domain is already present in nginx's conf, use the location configuration.
109+
add_location_configuration "$domain"
110+
else
111+
# Else use the standalone configuration.
112+
cat > "/etc/nginx/conf.d/standalone-cert-$domain.conf" << EOF
113+
server {
114+
server_name $domain;
115+
listen 80;
116+
access_log /var/log/nginx/access.log vhost;
117+
location ^~ /.well-known/acme-challenge/ {
118+
auth_basic off;
119+
auth_request off;
120+
allow all;
121+
root /usr/share/nginx/html;
122+
try_files \$uri =404;
123+
break;
124+
}
125+
}
126+
EOF
127+
fi
128+
}
129+
130+
function remove_all_standalone_configurations {
131+
local old_shopt_options; old_shopt_options=$(shopt -p) # Backup shopt options
132+
shopt -s nullglob
133+
for file in "/etc/nginx/conf.d/standalone-cert-"*".conf"; do
134+
rm -f "$file"
135+
done
136+
eval "$old_shopt_options" # Restore shopt options
137+
}
138+
105139
function remove_all_location_configurations {
106140
for file in "${VHOST_DIR}"/*; do
107141
[[ -e "$file" ]] || continue

app/letsencrypt_service

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ function create_links {
5050
}
5151

5252
function cleanup_links {
53+
local -a LETSENCRYPT_CONTAINERS
54+
local -a LETSENCRYPT_STANDALONE_CERTS
5355
local -a ENABLED_DOMAINS
5456
local -a SYMLINKED_DOMAINS
5557
local -a DISABLED_DOMAINS
@@ -65,9 +67,10 @@ function cleanup_links {
6567
[[ "$DEBUG" == true ]] && echo "Symlinked domains: ${SYMLINKED_DOMAINS[*]}"
6668

6769
# Create an array containing domains that are considered
68-
# enabled (ie present on /app/letsencrypt_service_data).
69-
# shellcheck source=/dev/null
70-
source /app/letsencrypt_service_data
70+
# enabled (ie present on /app/letsencrypt_service_data or /app/letsencrypt_user_data).
71+
[[ -f /app/letsencrypt_service_data ]] && source /app/letsencrypt_service_data
72+
[[ -f /app/letsencrypt_user_data ]] && source /app/letsencrypt_user_data
73+
LETSENCRYPT_CONTAINERS+=( "${LETSENCRYPT_STANDALONE_CERTS[@]}" )
7174
for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
7275
host_varname="LETSENCRYPT_${cid}_HOST"
7376
hosts_array="${host_varname}[@]"
@@ -80,7 +83,7 @@ function cleanup_links {
8083

8184
# Create an array containing only domains for which a symlinked private key exists
8285
# in /etc/nginx/certs but that no longer have a corresponding LETSENCRYPT_HOST set
83-
# on an active container.
86+
# on an active container or on /app/letsencrypt_user_data
8487
if [[ ${#SYMLINKED_DOMAINS[@]} -gt 0 ]]; then
8588
mapfile -t DISABLED_DOMAINS < <(echo "${SYMLINKED_DOMAINS[@]}" \
8689
"${ENABLED_DOMAINS[@]}" \
@@ -120,15 +123,34 @@ function cleanup_links {
120123
}
121124

122125
function update_certs {
126+
local -a LETSENCRYPT_CONTAINERS
127+
local -a LETSENCRYPT_STANDALONE_CERTS
123128

124129
check_nginx_proxy_container_run || return
125130

126-
[[ -f /app/letsencrypt_service_data ]] || return
127-
128131
# Load relevant container settings
129-
unset LETSENCRYPT_CONTAINERS
130-
# shellcheck source=/dev/null
131-
source /app/letsencrypt_service_data
132+
if [[ -f /app/letsencrypt_service_data ]]; then
133+
source /app/letsencrypt_service_data
134+
else
135+
echo "Warning: /app/letsencrypt_service_data not found, skipping data from containers."
136+
fi
137+
138+
# Load settings for standalone certs
139+
if [[ -f /app/letsencrypt_user_data ]]; then
140+
if source /app/letsencrypt_user_data; then
141+
for cid in "${LETSENCRYPT_STANDALONE_CERTS[@]}"; do
142+
host_varname="LETSENCRYPT_${cid}_HOST"
143+
hosts_array="${host_varname}[@]"
144+
for domain in "${!hosts_array}"; do
145+
add_standalone_configuration "$domain"
146+
done
147+
done
148+
reload_nginx
149+
LETSENCRYPT_CONTAINERS+=( "${LETSENCRYPT_STANDALONE_CERTS[@]}" )
150+
else
151+
echo "Warning: could not source /app/letsencrypt_user_data, skipping user data"
152+
fi
153+
fi
132154

133155
should_reload_nginx='false'
134156
for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
@@ -145,15 +167,15 @@ function update_certs {
145167

146168
# Use container's LETSENCRYPT_EMAIL if set, fallback to DEFAULT_EMAIL
147169
email_varname="LETSENCRYPT_${cid}_EMAIL"
148-
email_address="${!email_varname}"
170+
email_address="${!email_varname:-"<no value>"}"
149171
if [[ "$email_address" != "<no value>" ]]; then
150172
params_d_arr+=(--email "$email_address")
151173
elif [[ -n "${DEFAULT_EMAIL:-}" ]]; then
152174
params_d_arr+=(--email "$DEFAULT_EMAIL")
153175
fi
154176

155177
keysize_varname="LETSENCRYPT_${cid}_KEYSIZE"
156-
cert_keysize="${!keysize_varname}"
178+
cert_keysize="${!keysize_varname:-"<no value>"}"
157179
if [[ "$cert_keysize" == "<no value>" ]]; then
158180
cert_keysize=$DEFAULT_KEY_SIZE
159181
fi
@@ -173,7 +195,7 @@ function update_certs {
173195
fi
174196

175197
account_varname="LETSENCRYPT_${cid}_ACCOUNT_ALIAS"
176-
account_alias="${!account_varname}"
198+
account_alias="${!account_varname:-"<no value>"}"
177199
if [[ "$account_alias" == "<no value>" ]]; then
178200
account_alias=default
179201
fi
@@ -182,7 +204,7 @@ function update_certs {
182204
[[ $REUSE_PRIVATE_KEYS == true ]] && params_d_arr+=(--reuse_key)
183205

184206
min_validity="LETSENCRYPT_${cid}_MIN_VALIDITY"
185-
min_validity="${!min_validity}"
207+
min_validity="${!min_validity:-"<no value>"}"
186208
if [[ "$min_validity" == "<no value>" ]]; then
187209
min_validity=$DEFAULT_MIN_VALIDITY
188210
fi
@@ -310,6 +332,13 @@ function update_certs {
310332
docker_restart "${cid}"
311333
fi
312334

335+
for domain in "${!hosts_array}"; do
336+
if [[ -f "/etc/nginx/conf.d/standalone-cert-$domain.conf" ]]; then
337+
[[ $DEBUG == true ]] && echo "Debug: removing standalone configuration file /etc/nginx/conf.d/standalone-cert-$domain.conf"
338+
rm -f "/etc/nginx/conf.d/standalone-cert-$domain.conf" && should_reload_nginx='true'
339+
fi
340+
done
341+
313342
done
314343

315344
cleanup_links && should_reload_nginx='true'

app/start.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ term_handler() {
88
# shellcheck source=functions.sh
99
source /app/functions.sh
1010
remove_all_location_configurations
11+
remove_all_standalone_configurations
1112

1213
exit 0
1314
}

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
[Persistent data](./Persistent-data.md)
2020

21-
[Standalone certificates](./Standalone-certificates-(beta).md) **(Beta)**
21+
[Standalone certificates](./Standalone-certificates.md)
2222

2323
#### Troubleshooting:
2424

docs/Standalone-certificates-(beta).md renamed to docs/Standalone-certificates.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
## Standalone certificates
22

3-
**This feature is only present on the `dev` branch / image.**
4-
5-
**It's still considered experimental and subject to change.**
6-
73
You can generate certificate that are not tied to containers environment variable by mounting a user configuration file inside the container at `/app/letsencrypt_user_data`. This feature also require sharing the `/etc/nginx/conf.d` folder between the **nginx-proxy** and **letsencrypt-nginx-proxy-companion** container (and the **docker-gen** container if you are running a [three container setup](./Advanced-usage.md)):
84

95
```bash

test/config.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ imageTests+=(
1313
certs_single
1414
certs_san
1515
certs_single_domain
16+
certs_standalone
1617
force_renew
1718
certs_validity
1819
container_restart

test/setup/setup-nginx-proxy.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ case $SETUP in
99
--name $NGINX_CONTAINER_NAME \
1010
--env "DHPARAM_BITS=256" \
1111
-v /etc/nginx/vhost.d \
12+
-v /etc/nginx/conf.d \
1213
-v /usr/share/nginx/html \
1314
-v /var/run/docker.sock:/tmp/docker.sock:ro \
1415
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.test_suite \
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Started letsencrypt container for test certs_standalone
2+
Symlink to le1.wtf certificate has been generated.
3+
The link is pointing to the file ./le1.wtf/fullchain.pem
4+
Domain le1.wtf is on certificate.
5+
Symlink to le2.wtf certificate has been generated.
6+
The link is pointing to the file ./le2.wtf/fullchain.pem
7+
Domain le2.wtf is on certificate.
8+
Domain le3.wtf is on certificate.

test/tests/certs_standalone/run.sh

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/bin/bash
2+
3+
## Test for standalone certificates.
4+
5+
if [[ -z $TRAVIS_CI ]]; then
6+
le_container_name="$(basename ${0%/*})_$(date "+%Y-%m-%d_%H.%M.%S")"
7+
else
8+
le_container_name="$(basename ${0%/*})"
9+
fi
10+
11+
# Create the $domains array from comma separated domains in TEST_DOMAINS.
12+
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
13+
14+
# Cleanup function with EXIT trap
15+
function cleanup {
16+
# Cleanup the files created by this run of the test to avoid foiling following test(s).
17+
docker exec "$le_container_name" bash -c 'rm -rf /etc/nginx/certs/le?.wtf*'
18+
# Stop the LE container
19+
docker stop "$le_container_name" > /dev/null
20+
}
21+
trap cleanup EXIT
22+
23+
# Create letsencrypt_user_data with a single domain cert
24+
cat > ${TRAVIS_BUILD_DIR}/test/tests/certs_standalone/letsencrypt_user_data <<EOF
25+
LETSENCRYPT_STANDALONE_CERTS=('single')
26+
LETSENCRYPT_single_HOST=('${domains[0]}')
27+
EOF
28+
29+
run_le_container ${1:?} "$le_container_name" \
30+
"--volume ${TRAVIS_BUILD_DIR}/test/tests/certs_standalone/letsencrypt_user_data:/app/letsencrypt_user_data"
31+
32+
# Wait for a symlink at /etc/nginx/certs/${domains[0]}.crt
33+
# then grab the certificate in text form ...
34+
wait_for_symlink "${domains[0]}" "$le_container_name"
35+
created_cert="$(docker exec "$le_container_name" \
36+
openssl x509 -in /etc/nginx/certs/${domains[0]}/cert.pem -text -noout)"
37+
38+
# Check if the domain is on the certificate.
39+
if grep -q "${domains[0]}" <<< "$created_cert"; then
40+
echo "Domain ${domains[0]} is on certificate."
41+
else
42+
echo "Domain ${domains[0]} did not appear on certificate."
43+
fi
44+
45+
docker exec "$le_container_name" bash -c "[[ -f /etc/nginx/conf.d/standalone-cert-${domains[0]}.conf ]]" \
46+
&& echo "Standalone configuration for ${domains[0]} wasn't correctly removed."
47+
48+
# Add another (SAN) certificate to letsencrypt_user_data
49+
cat > ${TRAVIS_BUILD_DIR}/test/tests/certs_standalone/letsencrypt_user_data <<EOF
50+
LETSENCRYPT_STANDALONE_CERTS=('single' 'san')
51+
LETSENCRYPT_single_HOST=('${domains[0]}')
52+
LETSENCRYPT_san_HOST=('${domains[1]}' '${domains[2]}')
53+
EOF
54+
55+
# Manually trigger the service loop
56+
docker exec "$le_container_name" /app/signal_le_service > /dev/null
57+
58+
# Wait for a symlink at /etc/nginx/certs/${domains[1]}.crt
59+
# then grab the certificate in text form ...
60+
wait_for_symlink "${domains[1]}" "$le_container_name"
61+
created_cert="$(docker exec "$le_container_name" \
62+
openssl x509 -in /etc/nginx/certs/${domains[1]}/cert.pem -text -noout)"
63+
64+
for domain in "${domains[1]}" "${domains[2]}"; do
65+
# Check if the domain is on the certificate.
66+
if grep -q "$domain" <<< "$created_cert"; then
67+
echo "Domain $domain is on certificate."
68+
else
69+
echo "Domain $domain did not appear on certificate."
70+
fi
71+
done
72+
73+
docker exec "$le_container_name" bash -c "[[ ! -f /etc/nginx/conf.d/standalone-cert-${domains[1]}.conf ]]" \
74+
|| echo "Standalone configuration for ${domains[1]} wasn't correctly removed."

0 commit comments

Comments
 (0)