diff --git a/app/letsencrypt_service_data.tmpl b/app/letsencrypt_service_data.tmpl index 15d4752a..b12ae00e 100644 --- a/app/letsencrypt_service_data.tmpl +++ b/app/letsencrypt_service_data.tmpl @@ -1,14 +1,38 @@ +{{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }} +{{ $scopedContainersString := "" }} + +{{ range $hosts, $containers := groupBy $ "Env.LETSENCRYPT_HOST" }} + {{ if trim $hosts }} + {{ range $container := $containers }} + {{ $cid := printf "%.12s" $container.ID }} + {{ if $CurrentContainer.Env.NETWORK_SCOPE }} + {{ range $containerNetwork := $container.Networks }} + {{ if eq $CurrentContainer.Env.NETWORK_SCOPE $containerNetwork.Name }} + {{ $scopedContainersString = (printf "%s %s" $scopedContainersString $cid) }} + {{ end }} + {{ end }} + {{ else }} + {{ $scopedContainersString = (printf "%s %s" $scopedContainersString $cid) }} + {{ end }} + {{ end }} + {{ end }} +{{ end }} + +{{ $scopedContainersSlice := split (trim $scopedContainersString) " " }} + LETSENCRYPT_CONTAINERS=( {{ range $hosts, $containers := groupBy $ "Env.LETSENCRYPT_HOST" }} {{ if trim $hosts }} {{ range $container := $containers }} - {{ if parseBool (coalesce $container.Env.LETSENCRYPT_SINGLE_DOMAIN_CERTS "false") }} - {{ range $host := split $hosts "," }} - {{ $host := trim $host }} - '{{ printf "%.12s" $container.ID }}_{{ sha1 $host }}' + {{ $cid := printf "%.12s" $container.ID }} + {{ if intersect $scopedContainersSlice (split $cid " ") }} + {{ if parseBool (coalesce $container.Env.LETSENCRYPT_SINGLE_DOMAIN_CERTS "false") }} + {{ range $host := split $hosts "," }} + '{{ printf "%s_%s" $cid (sha1 (trim $host)) }}' + {{ end }} + {{ else }} + '{{ $cid }}' {{ end }} - {{ else }} - '{{ printf "%.12s" $container.ID }}' {{ end }} {{ end }} {{ end }} @@ -19,27 +43,28 @@ LETSENCRYPT_CONTAINERS=( {{ $hosts := trimSuffix "," $hosts }} {{ range $container := $containers }} {{ $cid := printf "%.12s" $container.ID }} - {{ if parseBool (coalesce $container.Env.LETSENCRYPT_SINGLE_DOMAIN_CERTS "false") }} - {{ range $host := split $hosts "," }} - {{ $host := trim $host }} - {{ $host := trimSuffix "." $host }} - {{ $hostHash := sha1 $host }} - LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_HOST=('{{ $host }}') - LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}" - LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}" - LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}" - LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_ACCOUNT_ALIAS="{{ $container.Env.LETSENCRYPT_ACCOUNT_ALIAS }}" - LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}" - LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_MIN_VALIDITY="{{ $container.Env.LETSENCRYPT_MIN_VALIDITY }}" + {{ if intersect $scopedContainersSlice (split $cid " ") }} + {{ if parseBool (coalesce $container.Env.LETSENCRYPT_SINGLE_DOMAIN_CERTS "false") }} + {{ range $host := split $hosts "," }} + {{ $host := trim $host }} + {{ $host := trimSuffix "." $host }} + {{ $hostHash := sha1 $host }} + LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_HOST=('{{ $host }}') + LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}" + LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}" + LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}" + LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_ACCOUNT_ALIAS="{{ $container.Env.LETSENCRYPT_ACCOUNT_ALIAS }}" + LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}" + LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_MIN_VALIDITY="{{ $container.Env.LETSENCRYPT_MIN_VALIDITY }}" + {{ end }} + {{ else }} + LETSENCRYPT_{{ $cid }}_HOST=( {{ range $host := split $hosts "," }}{{ $host := trim $host }}{{ $host := trimSuffix "." $host }}'{{ $host }}' {{ end }}) LETSENCRYPT_{{ $cid }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}" + LETSENCRYPT_{{ $cid }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}" + LETSENCRYPT_{{ $cid }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}" + LETSENCRYPT_{{ $cid }}_ACCOUNT_ALIAS="{{ $container.Env.LETSENCRYPT_ACCOUNT_ALIAS }}" + LETSENCRYPT_{{ $cid }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}" + LETSENCRYPT_{{ $cid }}_MIN_VALIDITY="{{ $container.Env.LETSENCRYPT_MIN_VALIDITY }}" {{ end }} - {{ else }} - LETSENCRYPT_{{ $cid }}_HOST=( {{ range $host := split $hosts "," }}{{ $host := trim $host }}{{ $host := trimSuffix "." $host }}'{{ $host }}' {{ end }}) - LETSENCRYPT_{{ $cid }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}" - LETSENCRYPT_{{ $cid }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}" - LETSENCRYPT_{{ $cid }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}" - LETSENCRYPT_{{ $cid }}_ACCOUNT_ALIAS="{{ $container.Env.LETSENCRYPT_ACCOUNT_ALIAS }}" - LETSENCRYPT_{{ $cid }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}" - LETSENCRYPT_{{ $cid }}_MIN_VALIDITY="{{ $container.Env.LETSENCRYPT_MIN_VALIDITY }}" {{ end }} {{ end }} {{ end }} diff --git a/docs/Container-configuration.md b/docs/Container-configuration.md index ff5d1e50..4f63ef6d 100644 --- a/docs/Container-configuration.md +++ b/docs/Container-configuration.md @@ -24,3 +24,20 @@ You can also create test certificates per container (see [Test certificates](./L * `REUSE_PRIVATE_KEYS` - Set it to `true` to make `simp_le` reuse previously generated private key for each certificate instead of creating a new one on certificate renewal. Recommended if you intend to use [HPKP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning) (please not however that HPKP has been deprecated by Google's Chrome and that its use is therefore not recommended). * `DHPARAM_BITS` - Change the size of the Diffie-Hellman key generated by the container from the default value of 2048 bits. For example `--env DHPARAM_BITS=1024` to support some older clients like Java 6 and 7. + +* `NETWORK_SCOPE` – The network name, that the container requesting a certificate MUST be connected to, in order to be discovered. You may find this option useful, when the host machine has multiple public IP addresses and you want to run separate nginx-proxy containers that will handle separate services with a proper networking isolation. + +If you set this environment variable, you MUST connect the nginx-proxy container to the same network. For example: + +```bash +$ docker run --detach \ + --name nginx-proxy-letsencrypt \ + --volumes-from nginx-proxy \ + --volume /path/to/certs:/etc/nginx/certs:rw \ + --volume /var/run/docker.sock:/var/run/docker.sock:ro \ + --network domains_group_a + --env "NETWORK_SCOPE=domains_group_a" \ + jrcs/letsencrypt-nginx-proxy-companion +``` + +The created companion will discover only the containers, that are also connected to the `domains_group_a` network. \ No newline at end of file diff --git a/test/config.sh b/test/config.sh index c8ca50b8..791f82d7 100755 --- a/test/config.sh +++ b/test/config.sh @@ -20,5 +20,6 @@ imageTests+=( permissions_default permissions_custom symlinks + networks_segregation ' ) diff --git a/test/tests/networks_segregation/expected-std-out.txt b/test/tests/networks_segregation/expected-std-out.txt new file mode 100644 index 00000000..6e234e5f --- /dev/null +++ b/test/tests/networks_segregation/expected-std-out.txt @@ -0,0 +1,11 @@ +Started letsencrypt container for test networks_segregation +Started test web server for le1.wtf in net boulder_bluenet +Started test web server for le2.wtf in net le_test_other_net1 +Started test web server for le3.wtf in net le_test_other_net2 +le1.wtf is in boulder_bluenet, cert should be generated +Symlink to le1.wtf certificate has been generated. +The link is pointing to the file ./le1.wtf/fullchain.pem +le2.wtf is not in boulder_bluenet, cert should not be generated +Domain le2.wtf was not included in the service_data. +le3.wtf is not in boulder_bluenet, cert should not be generated +Domain le3.wtf was not included in the service_data. diff --git a/test/tests/networks_segregation/run.sh b/test/tests/networks_segregation/run.sh new file mode 100755 index 00000000..accb56ac --- /dev/null +++ b/test/tests/networks_segregation/run.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +## Test for single domain certificates. + +if [[ -z $TRAVIS ]]; then + le_container_name="$(basename ${0%/*})_$(date "+%Y-%m-%d_%H.%M.%S")" +else + le_container_name="$(basename ${0%/*})" +fi +desired_network="boulder_bluenet" +run_le_container ${1:?} "$le_container_name" "--env NETWORK_SCOPE=$desired_network" + +# Create the $domains array from comma separated domains in TEST_DOMAINS. +IFS=',' read -r -a domains <<< "$TEST_DOMAINS" + +# Cleanup function with EXIT trap +function cleanup { + # Remove any remaining Nginx container(s) silently. + for domain in "${domains[@]}"; do + docker rm --force "$domain" > /dev/null 2>&1 + done + # Cleanup the files created by this run of the test to avoid foiling following test(s). + docker exec "$le_container_name" bash -c 'rm -rf /etc/nginx/certs/le?.wtf*' + # Stop the LE container + docker stop "$le_container_name" > /dev/null + # Drop temp network + docker network rm "le_test_other_net1" > /dev/null + docker network rm "le_test_other_net2" > /dev/null +} +trap cleanup EXIT + +docker network create "le_test_other_net1" > /dev/null +docker network create "le_test_other_net2" > /dev/null + +networks_map=("$desired_network" le_test_other_net1 le_test_other_net2) + +# Run a separate nginx container for each domain in the $domains array. +# Start all the containers in a row so that docker-gen debounce timers fire only once. +i=0 +for domain in "${domains[@]}"; do + docker run --rm -d \ + --name "$domain" \ + -e "VIRTUAL_HOST=${domain}" \ + -e "LETSENCRYPT_HOST=${domain}" \ + --network "${networks_map[i]}" \ + nginx:alpine > /dev/null && echo "Started test web server for $domain in net ${networks_map[${i}]}" + + i=$(( $i + 1 )) +done + +i=0 +for domain in "${domains[@]}"; do + if [ "${networks_map[i]}" != "$desired_network" ]; then + echo "$domain is not in $desired_network, cert should not be generated"; + + service_data="$(docker exec "$le_container_name" cat /app/letsencrypt_service_data)" + if grep -q "$domain" <<< "$service_data"; then + echo "Domain $domain is on data list, but MUST not!" + else + echo "Domain $domain was not included in the service_data." + fi + else + echo "$domain is in $desired_network, cert should be generated"; + + # Wait for a symlink at /etc/nginx/certs/$domain.crt + wait_for_symlink "$domain" "$le_container_name" + fi + # Stop the Nginx container silently. + docker stop "$domain" > /dev/null + i=$(( $i + 1 )) +done