From 1b82a298f2ae9c68c8a7394102447405594c069d Mon Sep 17 00:00:00 2001 From: Paul Lietar Date: Wed, 27 Nov 2024 15:07:57 +0000 Subject: [PATCH 1/3] Add letsencrypt support. The actual certificate request happens outside of the proxy, but in order to prove our ownership of the domain we need to be able to host ACME challenges at port 80. We expose the `/var/www/.well-known/acme-challenge` directory, which can be mounted from a volume. There is a chicken-and-egg problem when trying to start the container for the first time: nginx won't start without a certificate, and we can't get a certificate without nginx running. We break that cycle by generating a self-signed certificate if no other one is found. As soon as the real certificate is obtained, it can be swapped in by writing it and its key to `/etc/montagu/proxy/` and running `docker exec proxy nginx reload -s` (or equivalent). Instead of getting the Diffie-Hellman parameters alongside our certificates, we now download a set of parameters. DH parameters do not need to be secret nor unique. They only need to have been generated "correctly". The parameters we use come from [RFC7919][rfc7919], following the latest guidance from Mozilla. The directive only applies to TLS1.2. Starting with TLS1.3, [the standard][rfc8446] requires the parameters from RFC7919 to be used, and presumably nginx/openssl hardcodes these. [rfc7919]: https://www.rfc-editor.org/rfc/rfc7919 [rfc8446]: https://www.rfc-editor.org/rfc/rfc8446#section-4.2.8.1 --- Dockerfile | 3 +++ README.md | 37 ++++++++++++++++---------------- entrypoint.sh | 20 +++++++---------- nginx.montagu.conf | 7 +++++- scripts/run-integration-tests.sh | 29 ++++++------------------- 5 files changed, 42 insertions(+), 54 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9b6b48b..8ac1c98 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,8 @@ COPY index.html /usr/share/nginx/html/index.html COPY 404.html /usr/share/nginx/html/404.html COPY resources /usr/share/nginx/html/resources +ADD https://ssl-config.mozilla.org/ffdhe2048.txt /etc/nginx/dhparam.pem + # Copy third party javascript from npm modules ENV THIRDPARTY_JS_PATH /usr/share/nginx/html/resources/js/third_party/ COPY --from=0 /workspace/node_modules/pako/dist/pako.min.js $THIRDPARTY_JS_PATH @@ -26,6 +28,7 @@ COPY --from=0 /workspace/node_modules/bootstrap/dist/css/bootstrap.min.css /usr/ COPY --from=0 /workspace/node_modules/bootstrap/dist/css/bootstrap.min.css.map /usr/share/nginx/html/resources/css/third_party/ RUN rm /etc/nginx/conf.d/default.conf +RUN mkdir -p /etc/montagu/proxy WORKDIR /app COPY entrypoint.sh . diff --git a/README.md b/README.md index 32c87dd..6f5c45b 100644 --- a/README.md +++ b/README.md @@ -3,25 +3,26 @@ A reverse proxy for Montagu. This allows us to expose a single port (443) and map different paths to different apps (containers). ## SSL configuration files -`nginx.montagu.conf` contains references to an SSL certificate, an SSL private key, and a DHE parameter, which it -expects at `/etc/montagu/proxy/certificate.pem`, `/etc/montagu/proxy/ssl_key.pem` and -`/etc/montagu/proxy/dhparam.pem`, respectively. SSL public certificates are stored -in the [montagu repository](https://github.com/vimc/montagu/tree/master/certs), the SSL private key and DHE parameter -files are stored in the vault and all 3 are copied into the running proxy container during deployment. -Secrets are stored in the vault at: - -``` -vault list secret/vimc/ssl/v2/support/key -``` -and - -``` -vault list secret/vimc/ssl/v2/support/dhparam -``` - -### Generating new DHE parameters -The DHE parameter files in the vault were generated by running `openssl dhparam -out workspace/dhparam.pem 4096` +`nginx.montagu.conf` contains references to an X509 certificate and a private +key, which it expects at `/etc/montagu/proxy/certificate.pem` and +`/etc/montagu/proxy/ssl_key.pem`, respectively. The `/etc/montagu/proxy` +directory can be mounted from a volume providing these certificates, or they +can be injected into the container using `docker copy`. + +These certificates can be provisioned using Let's Encrypt (or another ACME +provider). The `/var/www/.well-known/acme-challenge` directory inside the +container will be exposed under the `/.well-known/acme-challenge` path. This +directory can be mounted from a volume that is shared with a certbot image. + +When the container starts, if no certificate is present, a self-signed +certificate will be generated. This helps avoid a chicken and egg problem where +nginx cannot start without a certificate, and a certificate cannot be obtained +without nginx running. + +When a new the certificate is obtained and written to `/etc/montagu/proxy`, +nginx needs to be reloaded by entering the container and running +`nginx -s reload`. ## Build and run locally Run `./scripts/dev.sh`. This runs up the proxy along with the apis and portals, in order to manually test links, logins etc. diff --git a/entrypoint.sh b/entrypoint.sh index eb57ce3..7043826 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -18,18 +18,14 @@ sed -e "s/_PORT_/$port/g" \ root="/etc/montagu/proxy" mkdir -p $root -a="$root/certificate.pem" -b="$root/ssl_key.pem" -c="$root/dhparam.pem" +if [[ ! -f $root/certificate.pem ]]; then + echo "Generating self-signed certificate for $host" -echo "Waiting for SSL certificate files at:" -echo "- $a" -echo "- $b" -echo "- $c" - -while [ ! -e $a ] || [ ! -e $b ] || [ ! -e $c ]; do - sleep 2 -done + openssl req -quiet -x509 -newkey rsa:4096 \ + -sha256 -days 365 -noenc \ + -subj "/C=GB/L=Location/O=Vaccine Impact Modelling Consortium/OU=Montagu/CN=$host" \ + -keyout "$root/ssl_key.pem" -out "$root/certificate.pem" +fi -echo "Certificate files detected. Running nginx" +echo "Starting nginx" exec nginx -g "daemon off;" diff --git a/nginx.montagu.conf b/nginx.montagu.conf index 5c52db7..a598ba1 100644 --- a/nginx.montagu.conf +++ b/nginx.montagu.conf @@ -35,7 +35,7 @@ server { ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; - ssl_dhparam /etc/montagu/proxy/dhparam.pem; + ssl_dhparam /etc/nginx/dhparam.pem; root /usr/share/nginx/html; @@ -147,6 +147,11 @@ server { deny all; } + location /.well-known/acme-challenge/ { + root /var/www; + autoindex off; + } + location / { return 301 https://_HOST_$request_uri; } diff --git a/scripts/run-integration-tests.sh b/scripts/run-integration-tests.sh index b1a00a2..4fb7e6b 100755 --- a/scripts/run-integration-tests.sh +++ b/scripts/run-integration-tests.sh @@ -18,13 +18,6 @@ $HERE/run-dependencies.sh export ORG=vimc -echo "Generating SSL keypair" -mkdir workspace -docker run --rm \ - -v $PWD/workspace:/workspace \ - $ORG/montagu-cert-tool:master \ - gen-self-signed /workspace > /dev/null 2> /dev/null - docker run -d \ -p "443:443" -p "80:80" \ --name reverse-proxy \ @@ -32,22 +25,12 @@ docker run -d \ $SHA_TAG 443 localhost docker run -d \ - -p "9113:9113" \ - --network montagu_proxy \ - --name montagu-metrics \ - --restart always \ - nginx/nginx-prometheus-exporter:0.2.0 \ - -nginx.scrape-uri "http://reverse-proxy/basic_status" - -# the real dhparam will be 4096 bits but that takes ages to generate -openssl dhparam -out workspace/dhparam.pem 2048 - -docker cp workspace/certificate.pem reverse-proxy:/etc/montagu/proxy/ -docker cp workspace/ssl_key.pem reverse-proxy:/etc/montagu/proxy/ -docker cp workspace/dhparam.pem reverse-proxy:/etc/montagu/proxy/ -rm -rf workspace - -sleep 2s + -p "9113:9113" \ + --network montagu_proxy \ + --name montagu-metrics \ + --restart always \ + nginx/nginx-prometheus-exporter:0.2.0 \ + -nginx.scrape-uri "http://reverse-proxy/basic_status" docker run \ --rm \ From 491d4781734acd1ee306fce59189a73418c08bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Li=C3=A9tar?= Date: Fri, 6 Dec 2024 16:52:57 +0000 Subject: [PATCH 2/3] Cleanup cert generation --- entrypoint.sh | 2 +- scripts/dev.sh | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 7043826..1dc0b5c 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -21,7 +21,7 @@ mkdir -p $root if [[ ! -f $root/certificate.pem ]]; then echo "Generating self-signed certificate for $host" - openssl req -quiet -x509 -newkey rsa:4096 \ + openssl req -quiet -x509 -newkey rsa:2048 \ -sha256 -days 365 -noenc \ -subj "/C=GB/L=Location/O=Vaccine Impact Modelling Consortium/OU=Montagu/CN=$host" \ -keyout "$root/ssl_key.pem" -out "$root/certificate.pem" diff --git a/scripts/dev.sh b/scripts/dev.sh index 05c49db..2c494b2 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -20,13 +20,7 @@ cleanup # This traps errors and Ctrl+C trap cleanup EXIT -echo "Generating SSL keypair" -mkdir workspace mkdir montagu_emails -docker run --rm \ - -v $PWD/workspace:/workspace \ - $ORG/montagu-cert-tool:master \ - gen-self-signed /workspace > /dev/null 2> /dev/null $here/run-dependencies.sh "$@" @@ -46,15 +40,8 @@ docker run -d \ nginx/nginx-prometheus-exporter:0.2.0 \ -nginx.scrape-uri "http://reverse-proxy/basic_status" -# the real dhparam will be 4096 bits but that takes ages to generate -openssl dhparam -out workspace/dhparam.pem 2048 - -docker cp workspace/certificate.pem reverse-proxy:/etc/montagu/proxy/ -docker cp workspace/ssl_key.pem reverse-proxy:/etc/montagu/proxy/ -docker cp workspace/dhparam.pem reverse-proxy:/etc/montagu/proxy/ docker cp $here/../2020 reverse-proxy:/usr/share/nginx/html docker cp $here/../2021 reverse-proxy:/usr/share/nginx/html -rm -rf workspace sleep 2s docker logs reverse-proxy From 382c81b11981dd6c7d437624bdfdbc2840ac2d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Li=C3=A9tar?= Date: Fri, 6 Dec 2024 17:01:02 +0000 Subject: [PATCH 3/3] Make www dir --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 8ac1c98..f589700 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,7 @@ COPY --from=0 /workspace/node_modules/bootstrap/dist/css/bootstrap.min.css.map / RUN rm /etc/nginx/conf.d/default.conf RUN mkdir -p /etc/montagu/proxy +RUN mkdir -p /var/www WORKDIR /app COPY entrypoint.sh .