Skip to content

Commit 7e3d341

Browse files
authored
Merge pull request #647 from hiqdev/feature/single_domain_cert
Implemented LETSENCRYPT_SINGLE_DOMAIN_CERTS environment variable
2 parents e49c2d5 + 177d60b commit 7e3d341

File tree

5 files changed

+229
-14
lines changed

5 files changed

+229
-14
lines changed

app/letsencrypt_service_data.tmpl

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,44 @@
1-
LETSENCRYPT_CONTAINERS=({{ range $hosts, $containers := groupBy $ "Env.LETSENCRYPT_HOST" }}{{ if trim $hosts }}{{ range $container := $containers }} '{{ printf "%.12s" $container.ID }}' {{ end }}{{ end }}{{ end }})
1+
LETSENCRYPT_CONTAINERS=(
2+
{{ range $hosts, $containers := groupBy $ "Env.LETSENCRYPT_HOST" }}
3+
{{ if trim $hosts }}
4+
{{ range $container := $containers }}
5+
{{ if parseBool (coalesce $container.Env.LETSENCRYPT_SINGLE_DOMAIN_CERTS "false") }}
6+
{{ range $host := split $hosts "," }}
7+
{{ $host := trim $host }}
8+
'{{ printf "%.12s" $container.ID }}_{{ sha1 $host }}'
9+
{{ end }}
10+
{{ else }}
11+
'{{ printf "%.12s" $container.ID }}'
12+
{{ end }}
13+
{{ end }}
14+
{{ end }}
15+
{{ end }}
16+
)
217

318
{{ range $hosts, $containers := groupBy $ "Env.LETSENCRYPT_HOST" }}
4-
5-
{{ $hosts := trimSuffix "," $hosts }}
6-
7-
{{ range $container := $containers }}{{ $cid := printf "%.12s" $container.ID }}
8-
LETSENCRYPT_{{ $cid }}_HOST=( {{ range $host := split $hosts "," }}{{ $host := trim $host }}'{{ $host }}' {{ end }})
9-
LETSENCRYPT_{{ $cid }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}"
10-
LETSENCRYPT_{{ $cid }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}"
11-
LETSENCRYPT_{{ $cid }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}"
12-
LETSENCRYPT_{{ $cid }}_ACCOUNT_ALIAS="{{ $container.Env.LETSENCRYPT_ACCOUNT_ALIAS }}"
13-
LETSENCRYPT_{{ $cid }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}"
14-
LETSENCRYPT_{{ $cid }}_MIN_VALIDITY="{{ $container.Env.LETSENCRYPT_MIN_VALIDITY }}"
15-
{{ end }}
16-
19+
{{ $hosts := trimSuffix "," $hosts }}
20+
{{ range $container := $containers }}
21+
{{ $cid := printf "%.12s" $container.ID }}
22+
{{ if parseBool (coalesce $container.Env.LETSENCRYPT_SINGLE_DOMAIN_CERTS "false") }}
23+
{{ range $host := split $hosts "," }}
24+
{{ $host := trim $host }}
25+
{{ $hostHash := sha1 $host }}
26+
LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_HOST=('{{ $host }}')
27+
LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}"
28+
LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}"
29+
LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}"
30+
LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_ACCOUNT_ALIAS="{{ $container.Env.LETSENCRYPT_ACCOUNT_ALIAS }}"
31+
LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}"
32+
LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_MIN_VALIDITY="{{ $container.Env.LETSENCRYPT_MIN_VALIDITY }}"
33+
{{ end }}
34+
{{ else }}
35+
LETSENCRYPT_{{ $cid }}_HOST=( {{ range $host := split $hosts "," }}{{ $host := trim $host }}'{{ $host }}' {{ end }})
36+
LETSENCRYPT_{{ $cid }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}"
37+
LETSENCRYPT_{{ $cid }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}"
38+
LETSENCRYPT_{{ $cid }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}"
39+
LETSENCRYPT_{{ $cid }}_ACCOUNT_ALIAS="{{ $container.Env.LETSENCRYPT_ACCOUNT_ALIAS }}"
40+
LETSENCRYPT_{{ $cid }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}"
41+
LETSENCRYPT_{{ $cid }}_MIN_VALIDITY="{{ $container.Env.LETSENCRYPT_MIN_VALIDITY }}"
42+
{{ end }}
43+
{{ end }}
1744
{{ end }}

docs/Let's-Encrypt-and-ACME.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ $ docker run --detach \
2626

2727
Let's Encrypt has a limit of [100 domains per certificate](https://letsencrypt.org/fr/docs/rate-limits/), while Buypass limit is [15 domains per certificate](https://www.buypass.com/ssl/products/go-ssl-campaign).
2828

29+
#### Separate certificate for each domain
30+
31+
The example above will issue a single [SAN](https://www.digicert.com/subject-alternative-name.htm) certificate for all the listed in `LETSENCRYPT_HOST` domains. If you need to have a separate certificate for each of the domains, you can add the `LETSENCRYPT_SINGLE_DOMAIN_CERTS=true` environment variable.
32+
33+
Example:
34+
35+
```shell
36+
$ docker run --detach \
37+
--name your-proxyed-app \
38+
--env "VIRTUAL_HOST=yourdomain.tld,www.yourdomain.tld,anotherdomain.tld" \
39+
--env "LETSENCRYPT_HOST=yourdomain.tld,www.yourdomain.tld,anotherdomain.tld" \
40+
--env "LETSENCRYPT_SINGLE_DOMAIN_CERTS=true" \
41+
nginx
42+
```
43+
2944
#### Automatic certificate renewal
3045
Every hour (3600 seconds) the certificates are checked and per default every certificate that will expire in the next [30 days](https://github.com/zenhack/simp_le/blob/a8a8013c097910f8f3cce046f1077b41b745673b/simp_le.py#L73) (90 days / 3) is renewed.
3146

test/config.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ imageTests+=(
1212
default_cert
1313
certs_single
1414
certs_san
15+
certs_single_domain
1516
force_renew
1617
certs_validity
1718
container_restart
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
Started letsencrypt container for test certs_single_domain
2+
Started test web server for le1.wtf,le2.wtf,le3.wtf
3+
Symlink to le1.wtf certificate has been generated.
4+
The link is pointing to the file ./le1.wtf/fullchain.pem
5+
le1.wtf is on certificate.
6+
le2.wtf did not appear on certificate for le1.wtf.
7+
le3.wtf did not appear on certificate for le1.wtf.
8+
Connection to le1.wtf using https was successful.
9+
The correct certificate for le1.wtf was served by Nginx.
10+
Symlink to le2.wtf certificate has been generated.
11+
The link is pointing to the file ./le2.wtf/fullchain.pem
12+
le2.wtf is on certificate.
13+
le1.wtf did not appear on certificate for le2.wtf.
14+
le3.wtf did not appear on certificate for le2.wtf.
15+
Connection to le2.wtf using https was successful.
16+
The correct certificate for le2.wtf was served by Nginx.
17+
Symlink to le3.wtf certificate has been generated.
18+
The link is pointing to the file ./le3.wtf/fullchain.pem
19+
le3.wtf is on certificate.
20+
le1.wtf did not appear on certificate for le3.wtf.
21+
le2.wtf did not appear on certificate for le3.wtf.
22+
Connection to le3.wtf using https was successful.
23+
The correct certificate for le3.wtf was served by Nginx.
24+
Started test web server for le2.wtf, le3.wtf, le1.wtf
25+
Symlink to le1.wtf certificate has been generated.
26+
The link is pointing to the file ./le1.wtf/fullchain.pem
27+
le1.wtf is on certificate.
28+
le2.wtf did not appear on certificate for le1.wtf.
29+
le3.wtf did not appear on certificate for le1.wtf.
30+
Connection to le1.wtf using https was successful.
31+
The correct certificate for le1.wtf was served by Nginx.
32+
Symlink to le2.wtf certificate has been generated.
33+
The link is pointing to the file ./le2.wtf/fullchain.pem
34+
le2.wtf is on certificate.
35+
le1.wtf did not appear on certificate for le2.wtf.
36+
le3.wtf did not appear on certificate for le2.wtf.
37+
Connection to le2.wtf using https was successful.
38+
The correct certificate for le2.wtf was served by Nginx.
39+
Symlink to le3.wtf certificate has been generated.
40+
The link is pointing to the file ./le3.wtf/fullchain.pem
41+
le3.wtf is on certificate.
42+
le1.wtf did not appear on certificate for le3.wtf.
43+
le2.wtf did not appear on certificate for le3.wtf.
44+
Connection to le3.wtf using https was successful.
45+
The correct certificate for le3.wtf was served by Nginx.
46+
Started test web server for le3.wtf, le1.wtf, le2.wtf,
47+
Symlink to le1.wtf certificate has been generated.
48+
The link is pointing to the file ./le1.wtf/fullchain.pem
49+
le1.wtf is on certificate.
50+
le2.wtf did not appear on certificate for le1.wtf.
51+
le3.wtf did not appear on certificate for le1.wtf.
52+
Connection to le1.wtf using https was successful.
53+
The correct certificate for le1.wtf was served by Nginx.
54+
Symlink to le2.wtf certificate has been generated.
55+
The link is pointing to the file ./le2.wtf/fullchain.pem
56+
le2.wtf is on certificate.
57+
le1.wtf did not appear on certificate for le2.wtf.
58+
le3.wtf did not appear on certificate for le2.wtf.
59+
Connection to le2.wtf using https was successful.
60+
The correct certificate for le2.wtf was served by Nginx.
61+
Symlink to le3.wtf certificate has been generated.
62+
The link is pointing to the file ./le3.wtf/fullchain.pem
63+
le3.wtf is on certificate.
64+
le1.wtf did not appear on certificate for le3.wtf.
65+
le2.wtf did not appear on certificate for le3.wtf.
66+
Connection to le3.wtf using https was successful.
67+
The correct certificate for le3.wtf was served by Nginx.

test/tests/certs_single_domain/run.sh

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/bin/bash
2+
3+
## Test for standalone certificates by NGINX container env variables
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+
run_le_container ${1:?} "$le_container_name"
11+
12+
# Create the $domains array from comma separated domains in TEST_DOMAINS.
13+
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
14+
15+
# Cleanup function with EXIT trap
16+
function cleanup {
17+
# Remove any remaining Nginx container(s) silently.
18+
i=1
19+
for hosts in "${letsencrypt_hosts[@]}"; do
20+
docker rm --force "test$i" > /dev/null 2>&1
21+
i=$(( $i + 1 ))
22+
done
23+
# Cleanup the files created by this run of the test to avoid foiling following test(s).
24+
docker exec "$le_container_name" bash -c 'rm -rf /etc/nginx/certs/le?.wtf*'
25+
# Stop the LE container
26+
docker stop "$le_container_name" > /dev/null
27+
}
28+
trap cleanup EXIT
29+
30+
# Create three different comma separated list from the first three domains in $domains.
31+
# testing for regression on spaced lists https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion/issues/288
32+
# and with trailing comma https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion/issues/254
33+
letsencrypt_hosts=( \
34+
[0]="${domains[0]},${domains[1]},${domains[2]}" \ #straight comma separated list
35+
[1]="${domains[1]}, ${domains[2]}, ${domains[0]}" \ #comma separated list with spaces
36+
[2]="${domains[2]}, ${domains[0]}, ${domains[1]}," ) #comma separated list with spaces and a trailing comma
37+
38+
i=1
39+
40+
for hosts in "${letsencrypt_hosts[@]}"; do
41+
42+
# Get the base domain (first domain of the list).
43+
base_domain="$(get_base_domain "$hosts")"
44+
container="test$i"
45+
46+
# Run an Nginx container passing one of the comma separated list as LETSENCRYPT_HOST env var.
47+
docker run --rm -d \
48+
--name "$container" \
49+
-e "VIRTUAL_HOST=${TEST_DOMAINS}" \
50+
-e "LETSENCRYPT_HOST=${hosts}" \
51+
-e "LETSENCRYPT_SINGLE_DOMAIN_CERTS=true" \
52+
--network boulder_bluenet \
53+
nginx:alpine > /dev/null && echo "Started test web server for $hosts"
54+
55+
for domain in "${domains[@]}"; do
56+
## For all the domains in the $domains array ...
57+
wait_for_symlink "${domain}" "$le_container_name"
58+
created_cert="$(docker exec "$le_container_name" \
59+
openssl x509 -in /etc/nginx/certs/${domain}/cert.pem -text -noout)"
60+
# ... as well as the certificate fingerprint.
61+
created_cert_fingerprint="$(docker exec "$le_container_name" \
62+
sh -c "openssl x509 -in "/etc/nginx/certs/${domain}/cert.pem" -fingerprint -noout")"
63+
64+
# Check if the domain is on the certificate.
65+
if grep -q "$domain" <<< "$created_cert"; then
66+
echo "$domain is on certificate."
67+
for otherdomain in "${domains[@]}"; do
68+
if [ "$domain" != "$otherdomain" ]; then
69+
if grep -q "$otherdomain" <<< "$created_cert"; then
70+
echo "$otherdomain is on certificate for $domain, but it must not!"
71+
else
72+
echo "$otherdomain did not appear on certificate for $domain."
73+
fi
74+
fi
75+
done
76+
else
77+
echo "$domain did not appear on certificate."
78+
fi
79+
80+
# Wait for a connection to https://domain then grab the served certificate in text form.
81+
wait_for_conn --domain "$domain"
82+
served_cert_fingerprint="$(echo \
83+
| openssl s_client -showcerts -servername $domain -connect $domain:443 2>/dev/null \
84+
| openssl x509 -fingerprint -noout)"
85+
86+
87+
# Compare the cert on file and what we got from the https connection.
88+
# If not identical, display a full diff.
89+
if [ "$created_cert_fingerprint" != "$served_cert_fingerprint" ]; then
90+
echo "Nginx served an incorrect certificate for $domain."
91+
served_cert="$(echo \
92+
| openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
93+
| openssl x509 -text -noout \
94+
| sed 's/ = /=/g' )"
95+
diff -u <(echo "$created_cert" | sed 's/ = /=/g') <(echo "$served_cert")
96+
else
97+
echo "The correct certificate for $domain was served by Nginx."
98+
fi
99+
done
100+
101+
docker stop "$container" > /dev/null 2>&1
102+
docker exec "$le_container_name" bash -c 'rm -rf /etc/nginx/certs/le?.wtf*'
103+
i=$(( $i + 1 ))
104+
105+
done

0 commit comments

Comments
 (0)