diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 203534cb..8afd2a35 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -114,6 +114,7 @@ jobs: container_restart, permissions_default, permissions_custom, + networks_segregation, symlinks, ] setup: [2containers, 3containers] diff --git a/app/letsencrypt_service_data.tmpl b/app/letsencrypt_service_data.tmpl index 478e9967..4c4d8896 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 }} - {{- "\t"}}'{{ 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 "," }} + {{- "\t"}}'{{ printf "%s_%s" $cid (sha1 (trim $host)) }}' + {{ end }} + {{ else }} + '{{ $cid }}' {{ end }} - {{ else }} - {{- "\t"}}'{{ printf "%.12s" $container.ID }}' {{ end }} {{ end }} {{ end }} @@ -19,41 +43,43 @@ 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 }} - {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_HOST=('{{ $host }}') - {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}" - {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}" - {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}" - {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_CA_URI="{{ $container.Env.ACME_CA_URI }}" - {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_PREFERRED_CHAIN="{{ $container.Env.ACME_PREFERRED_CHAIN }}" - {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_OCSP="{{ $container.Env.ACME_OCSP }}" - {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_EAB_KID="{{ $container.Env.ACME_EAB_KID }}" - {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_EAB_HMAC_KEY="{{ $container.Env.ACME_EAB_HMAC_KEY }}" - {{- "\n" }}ZEROSSL_{{ $cid }}_{{ $hostHash }}_API_KEY="{{ $container.Env.ZEROSSL_API_KEY }}" - {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}" + {{ 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 }} + {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_HOST=('{{ $host }}') + {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}" + {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}" + {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}" + {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_CA_URI="{{ $container.Env.ACME_CA_URI }}" + {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_PREFERRED_CHAIN="{{ $container.Env.ACME_PREFERRED_CHAIN }}" + {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_OCSP="{{ $container.Env.ACME_OCSP }}" + {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_EAB_KID="{{ $container.Env.ACME_EAB_KID }}" + {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_EAB_HMAC_KEY="{{ $container.Env.ACME_EAB_HMAC_KEY }}" + {{- "\n" }}ZEROSSL_{{ $cid }}_{{ $hostHash }}_API_KEY="{{ $container.Env.ZEROSSL_API_KEY }}" + {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}" + {{ end }} + {{ else }} + {{- "\n" }}LETSENCRYPT_{{ $cid }}_HOST=( + {{- range $host := split $hosts "," }} + {{- $host := trim $host }} + {{- $host := trimSuffix "." $host -}} + '{{ $host }}'{{ " " }} + {{- end -}} + ) + {{- "\n" }}LETSENCRYPT_{{ $cid }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}" + {{- "\n" }}LETSENCRYPT_{{ $cid }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}" + {{- "\n" }}LETSENCRYPT_{{ $cid }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}" + {{- "\n" }}ACME_{{ $cid }}_CA_URI="{{ $container.Env.ACME_CA_URI }}" + {{- "\n" }}ACME_{{ $cid }}_PREFERRED_CHAIN="{{ $container.Env.ACME_PREFERRED_CHAIN }}" + {{- "\n" }}ACME_{{ $cid }}_OCSP="{{ $container.Env.ACME_OCSP }}" + {{- "\n" }}ACME_{{ $cid }}_EAB_KID="{{ $container.Env.ACME_EAB_KID }}" + {{- "\n" }}ACME_{{ $cid }}_EAB_HMAC_KEY="{{ $container.Env.ACME_EAB_HMAC_KEY }}" + {{- "\n" }}ZEROSSL_{{ $cid }}_API_KEY="{{ $container.Env.ZEROSSL_API_KEY }}" + {{- "\n" }}LETSENCRYPT_{{ $cid }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}" {{ end }} - {{ else }} - {{- "\n" }}LETSENCRYPT_{{ $cid }}_HOST=( - {{- range $host := split $hosts "," }} - {{- $host := trim $host }} - {{- $host := trimSuffix "." $host -}} - '{{ $host }}'{{ " " }} - {{- end -}} - ) - {{- "\n" }}LETSENCRYPT_{{ $cid }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}" - {{- "\n" }}LETSENCRYPT_{{ $cid }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}" - {{- "\n" }}LETSENCRYPT_{{ $cid }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}" - {{- "\n" }}ACME_{{ $cid }}_CA_URI="{{ $container.Env.ACME_CA_URI }}" - {{- "\n" }}ACME_{{ $cid }}_PREFERRED_CHAIN="{{ $container.Env.ACME_PREFERRED_CHAIN }}" - {{- "\n" }}ACME_{{ $cid }}_OCSP="{{ $container.Env.ACME_OCSP }}" - {{- "\n" }}ACME_{{ $cid }}_EAB_KID="{{ $container.Env.ACME_EAB_KID }}" - {{- "\n" }}ACME_{{ $cid }}_EAB_HMAC_KEY="{{ $container.Env.ACME_EAB_HMAC_KEY }}" - {{- "\n" }}ZEROSSL_{{ $cid }}_API_KEY="{{ $container.Env.ZEROSSL_API_KEY }}" - {{- "\n" }}LETSENCRYPT_{{ $cid }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}" {{ end }} {{ end }} {{ end }} diff --git a/docs/Container-configuration.md b/docs/Container-configuration.md index 190f55ee..7d348140 100644 --- a/docs/Container-configuration.md +++ b/docs/Container-configuration.md @@ -23,6 +23,23 @@ You can also create test certificates per container (see [Test certificates](./L * `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. + * `CA_BUNDLE` - This is a test only variable [for use with Pebble](https://github.com/letsencrypt/pebble#avoiding-client-https-errors). It changes the trusted root CA used by `acme.sh`, from the default Alpine trust store to the CA bundle file located at the provided path (inside the container). Do **not** use it in production unless you are running your own ACME CA. -* `CERTS_UPDATE_INTERVAL` - 3600 seconds by default, this defines how often the container will check if the certificates require update. \ No newline at end of file +* `CERTS_UPDATE_INTERVAL` - 3600 seconds by default, this defines how often the container will check if the certificates require update. diff --git a/test/config.sh b/test/config.sh index f2b535cf..0a2505c7 100755 --- a/test/config.sh +++ b/test/config.sh @@ -15,6 +15,7 @@ globalTests+=( container_restart permissions_default permissions_custom + networks_segregation symlinks ) 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..8b137891 --- /dev/null +++ b/test/tests/networks_segregation/expected-std-out.txt @@ -0,0 +1 @@ + diff --git a/test/tests/networks_segregation/run.sh b/test/tests/networks_segregation/run.sh new file mode 100755 index 00000000..7e49acd4 --- /dev/null +++ b/test/tests/networks_segregation/run.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +## Test for single domain certificates. + +if [[ -z $GITHUB_ACTIONS ]]; 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" /app/cleanup_test_artifacts + # 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 + if ! docker run --rm -d \ + --name "$domain" \ + -e "VIRTUAL_HOST=${domain}" \ + -e "LETSENCRYPT_HOST=${domain}" \ + --network "${networks_map[i]}" \ + nginx:alpine > /dev/null; + then + echo "Could not start test web server for $domain" + elif [[ "${DRY_RUN:-}" == 1 ]]; then + echo "Started test web server for $domain" + fi + + i=$(( i + 1 )) +done + +i=0 +for domain in "${domains[@]}"; do + if [ "${networks_map[i]}" != "$desired_network" ]; then + [[ "${DRY_RUN:-}" == 1 ]] && 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 + [[ "${DRY_RUN:-}" == 1 ]] && echo "Domain $domain was not included in the service_data." + fi + else + [[ "${DRY_RUN:-}" == 1 ]] && 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