diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30249c98..804b55f7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,6 +84,7 @@ jobs: permissions_custom, symlinks, acme_hooks, + networks_segregation ] setup: [2containers, 3containers] acme-ca: [pebble] diff --git a/app/letsencrypt_service_data.tmpl b/app/letsencrypt_service_data.tmpl index b7077ff2..b6c107a4 100644 --- a/app/letsencrypt_service_data.tmpl +++ b/app/letsencrypt_service_data.tmpl @@ -1,7 +1,21 @@ +{{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }} + +{{ $activeContainers := whereExist $ "Env.LETSENCRYPT_HOST" }} +{{ if trim (default "" $CurrentContainer.Env.NETWORK_SCOPE) }} + {{ $filteredContainers := list }} + {{ range $activeContainers }} + {{ if gt (where .Networks "Name" (trim $CurrentContainer.Env.NETWORK_SCOPE) | len) 0 }} + {{ $filteredContainers = append $filteredContainers . }} + {{ end }} + {{ end }} + {{ $activeContainers = $filteredContainers }} +{{ end }} + LETSENCRYPT_CONTAINERS=( - {{ range $hosts, $containers := groupBy $ "Env.LETSENCRYPT_HOST" }} + {{ range $hosts, $containers := groupBy $activeContainers "Env.LETSENCRYPT_HOST" }} {{ if trim $hosts }} {{ range $container := $containers }} + {{ $cid := printf "%.12s" $container.ID }} {{ if parseBool (coalesce $container.Env.LETSENCRYPT_SINGLE_DOMAIN_CERTS "false") }} {{/* Explicit per-domain splitting of the certificate */}} {{ range $host := split $hosts "," }} @@ -17,7 +31,7 @@ LETSENCRYPT_CONTAINERS=( {{ end }} ) -{{ range $hosts, $containers := groupBy $ "Env.LETSENCRYPT_HOST" }} +{{ range $hosts, $containers := groupBy $activeContainers "Env.LETSENCRYPT_HOST" }} {{ $hosts := trimSuffix "," $hosts }} {{ range $container := $containers }} {{/* Trim spaces and set empty values on per-container environment variables */}} diff --git a/docs/Let's-Encrypt-and-ACME.md b/docs/Let's-Encrypt-and-ACME.md index 8633a69b..0d2031d6 100644 --- a/docs/Let's-Encrypt-and-ACME.md +++ b/docs/Let's-Encrypt-and-ACME.md @@ -101,3 +101,13 @@ Reusing private keys can help if you intend to use [HPKP](https://developer.mozi 1. The container will use the special purpose `staging` configuration directory. 1. The directory URI is forced to The Let's Encrypt v2 staging one (`ACME_CA_URI` is ignored) 2. The account email address is forced empty (`DEFAULT_EMAIL` and `LETSENCRYPT_EMAIL` are ignored) + +#### Running multiple **nginx-proxy** and **acme-companion** containers on a same Docker machine + +The `NETWORK_SCOPE` variable must be set in order to run multiple **acme-companion** containers on a same Docker machine. The value should be a name of network, that +connects **nginx-proxy** with the proxied containers. + +When a server has multiple IP addresses, you might run multiple **nginx-proxy** + **acme-companion** instances on it, each for a different set of proxied +containers. By default, **acme-companion** discovers all running containers and tries to generate SSL certificates for them, meaning multiple **acme-companion** +instances will try to generate the same set of certificates. In order to limit **acme-companion** discovery scope to a smaller set of containers, set the +`NETWORK_SCOPE` environment variable. \ No newline at end of file diff --git a/test/config.sh b/test/config.sh index bb370407..c0b0e2e7 100755 --- a/test/config.sh +++ b/test/config.sh @@ -17,6 +17,7 @@ globalTests+=( permissions_custom symlinks acme_hooks + networks_segregation ) # The ocsp_must_staple test does not work with Pebble 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..94f95564 --- /dev/null +++ b/test/tests/networks_segregation/expected-std-out.txt @@ -0,0 +1,8 @@ +Started test web server for le1.wtf in the network 0 +Started test web server for le2.wtf in the network 1 +Started test web server for le3.wtf in the network 2 +le1.wtf is in the primary network, cert should be generated +le2.wtf is not in the primary network, cert should not be generated +Domain le2.wtf was not included in the service_data. +le3.wtf is not in the primary network, 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..b4d68f4b --- /dev/null +++ b/test/tests/networks_segregation/run.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +## Test for Network segregation. + +case $ACME_CA in + pebble) + test_net='acme_net' + + ;; + boulder) + test_net='boulder_bluenet' + ;; + *) + echo "$0 $ACME_CA: invalid option." + exit 1 +esac + +if [[ -z $GITHUB_ACTIONS ]]; then + le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")" +else + le_container_name="$(basename "${0%/*}")" +fi + +run_le_container ${1:?} "$le_container_name" "--env NETWORK_SCOPE=$test_net" + +# 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" /app/cleanup_test_artifacts + # Remove the LE container, as it it network-scoped and may affect following test(s). + docker rm --force "$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=("$test_net" 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 the network ${i}" + + i=$(( $i + 1 )) +done + +i=0 +for domain in "${domains[@]}"; do + if [ "${networks_map[i]}" != "$test_net" ]; then + echo "$domain is not in the primary 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 the primary network, cert should be generated"; + wait_for_symlink "$domain" "$le_container_name" + fi + # Stop the Nginx container silently. + docker stop "$domain" > /dev/null + i=$(( $i + 1 )) +done +