Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Network Segregation feature #957

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ jobs:
permissions_custom,
symlinks,
acme_hooks,
networks_segregation
]
setup: [2containers, 3containers]
acme-ca: [pebble]
Expand Down
18 changes: 16 additions & 2 deletions app/letsencrypt_service_data.tmpl
Original file line number Diff line number Diff line change
@@ -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 "," }}
Expand All @@ -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 */}}
Expand Down
10 changes: 10 additions & 0 deletions docs/Let's-Encrypt-and-ACME.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions test/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ globalTests+=(
permissions_custom
symlinks
acme_hooks
networks_segregation
)

# The ocsp_must_staple test does not work with Pebble
Expand Down
8 changes: 8 additions & 0 deletions test/tests/networks_segregation/expected-std-out.txt
Original file line number Diff line number Diff line change
@@ -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.
83 changes: 83 additions & 0 deletions test/tests/networks_segregation/run.sh
Original file line number Diff line number Diff line change
@@ -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