Explorar el Código

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.
Povilas Kanapickas hace 2 meses
padre
commit
4c8f22ebcc

+ 2 - 0
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.

+ 11 - 0
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 {

+ 34 - 0
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

+ 40 - 0
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"