From 4c8f22ebcc4c3bb7a2d192991da2fc1d2abb40fe Mon Sep 17 00:00:00 2001 From: Povilas Kanapickas Date: Fri, 14 Mar 2025 01:28:12 +0200 Subject: [PATCH] feat: support ACME challenges for unknown virtual hosts Currently any ACME challenge for unknown virtual host returns 503. This is inconvenient because if the user does not use wildcard certificates, then the user must match the configuration of certificate renewal script to what virtual hosts are enabled at the time. This must be done automatically, because due to short certificate lifetime the renewal script runs automatically. Additionally, enabling a previously disabled virtual host forces certificate renewal. Accordingly, it's worthwhile supporting unknown virtual hosts for the purposes of passing ACME challenges. This is done by introducing a global ACME_HTTP_CHALLENGE_ACCEPT_UNKNOWN_HOST variable to control this. --- docs/README.md | 2 + nginx.tmpl | 11 +++++ ...-challenge-location-accept-unknown-host.py | 34 ++++++++++++++++ ...challenge-location-accept-unknown-host.yml | 40 +++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 test/test_acme-http-challenge-location/test_acme-http-challenge-location-accept-unknown-host.py create mode 100644 test/test_acme-http-challenge-location/test_acme-http-challenge-location-accept-unknown-host.yml diff --git a/docs/README.md b/docs/README.md index ca1d56c..4e6788d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -459,6 +459,8 @@ By default nginx-proxy generates location blocks to handle ACME HTTP Challenge. - `false`: do not handle ACME HTTP Challenge at all. - `legacy`: legacy behavior for compatibility with older (<= `2.3`) versions of acme-companion, only handle ACME HTTP challenge when there is a certificate for the domain and `HTTPS_METHOD=redirect`. +By default, nginx-proxy does not handle ACME HTTP Challenges for unknown virtual hosts. This may happen in cases when a container is not running at the time of the renewal. To enable handling of unknown virtual hosts, set `ACME_HTTP_CHALLENGE_ACCEPT_UNKNOWN_HOST` environment variable to `true` on the nginx-proxy container. + ### Diffie-Hellman Groups [RFC7919 groups](https://datatracker.ietf.org/doc/html/rfc7919#appendix-A) with key lengths of 2048, 3072, and 4096 bits are [provided by `nginx-proxy`](https://github.com/nginx-proxy/nginx-proxy/dhparam). The ENV `DHPARAM_BITS` can be set to `2048` or `3072` to change from the default 4096-bit key. The DH key file will be located in the container at `/etc/nginx/dhparam/dhparam.pem`. Mounting a different `dhparam.pem` file at that location will override the RFC7919 key. diff --git a/nginx.tmpl b/nginx.tmpl index dd1c444..afc9f16 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -28,6 +28,7 @@ {{- $_ := set $config "enable_debug_endpoint" ($globals.Env.DEBUG_ENDPOINT | default "false") }} {{- $_ := set $config "hsts" ($globals.Env.HSTS | default "max-age=31536000") }} {{- $_ := set $config "acme_http_challenge" ($globals.Env.ACME_HTTP_CHALLENGE_LOCATION | default "true") }} +{{- $_ := set $config "acme_http_challenge_accept_unknown_host" ($globals.Env.ACME_HTTP_CHALLENGE_ACCEPT_UNKNOWN_HOST | default "false" | parseBool) }} {{- $_ := set $config "enable_http2" ($globals.Env.ENABLE_HTTP2 | default "true") }} {{- $_ := set $config "enable_http3" ($globals.Env.ENABLE_HTTP3 | default "false") }} {{- $_ := set $config "enable_http_on_missing_cert" ($globals.Env.ENABLE_HTTP_ON_MISSING_CERT | default "true") }} @@ -861,6 +862,16 @@ server { ssl_reject_handshake on; {{- end }} + {{- if $globals.config.acme_http_challenge_accept_unknown_host }} + location ^~ /.well-known/acme-challenge/ { + auth_basic off; + allow all; + root /usr/share/nginx/html; + try_files $uri =404; + break; + } + {{- end }} + {{- if (exists "/usr/share/nginx/html/errors/50x.html") }} error_page 500 502 503 504 /50x.html; location /50x.html { diff --git a/test/test_acme-http-challenge-location/test_acme-http-challenge-location-accept-unknown-host.py b/test/test_acme-http-challenge-location/test_acme-http-challenge-location-accept-unknown-host.py new file mode 100644 index 0000000..8643b4b --- /dev/null +++ b/test/test_acme-http-challenge-location/test_acme-http-challenge-location-accept-unknown-host.py @@ -0,0 +1,34 @@ +def test_redirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web1.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 200 + +def test_redirect_acme_challenge_location_disabled(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web2.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 301 + +def test_noredirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web3.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 200 + +def test_noredirect_acme_challenge_location_disabled(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web4.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 404 + +def test_unknown_domain_acme_challenge_location_default_enabled(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web-unknown.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 200 diff --git a/test/test_acme-http-challenge-location/test_acme-http-challenge-location-accept-unknown-host.yml b/test/test_acme-http-challenge-location/test_acme-http-challenge-location-accept-unknown-host.yml new file mode 100644 index 0000000..25965e4 --- /dev/null +++ b/test/test_acme-http-challenge-location/test_acme-http-challenge-location-accept-unknown-host.yml @@ -0,0 +1,40 @@ +services: + nginx-proxy: + environment: + ACME_HTTP_CHALLENGE_ACCEPT_UNKNOWN_HOST: "true" + + web1: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: "web1.nginx-proxy.tld" + + web2: + image: web + expose: + - "82" + environment: + WEB_PORTS: "82" + VIRTUAL_HOST: "web2.nginx-proxy.tld" + ACME_HTTP_CHALLENGE_LOCATION: "false" + + web3: + image: web + expose: + - "83" + environment: + WEB_PORTS: "83" + VIRTUAL_HOST: "web3.nginx-proxy.tld" + HTTPS_METHOD: noredirect + + web4: + image: web + expose: + - "84" + environment: + WEB_PORTS: "84" + VIRTUAL_HOST: "web4.nginx-proxy.tld" + HTTPS_METHOD: noredirect + ACME_HTTP_CHALLENGE_LOCATION: "false"