From 57e503c830a1656b82f98ef3b08a0331d6d08908 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sun, 3 Nov 2024 20:10:32 +0100 Subject: [PATCH 1/7] feat: trust default certificate --- docs/README.md | 2 +- nginx.tmpl | 16 +++++++--------- test/test_fallback.py | 4 ++-- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/README.md b/docs/README.md index 208ed35..2ee008a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -582,7 +582,7 @@ By default, [HTTP Strict Transport Security (HSTS)](https://developer.mozilla.or If no matching certificate is found for a given virtual host, nginx-proxy will: -- configure nginx to use the default certificate (`default.crt` with `default.key`) and return a 500 error for HTTPS, +- configure nginx to use the default certificate (`default.crt` with `default.key`), - force enable HTTP; i.e. `HTTPS_METHOD` will switch to `noredirect` if it was set to `nohttp` or `redirect`. If this switch to HTTP is not wanted set `ENABLE_HTTP_ON_MISSING_CERT=false` (default is `true`). diff --git a/nginx.tmpl b/nginx.tmpl index a15e7e8..b9b0598 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -918,15 +918,21 @@ server { {{- end }} {{- end }} - {{- if $vhost.cert_ok }} + {{- if or $vhost.cert_ok $globals.config.default_cert_ok }} {{- template "ssl_policy" (dict "ssl_policy" $vhost.ssl_policy) }} ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; ssl_session_tickets off; + {{- if $vhost.cert_ok }} ssl_certificate /etc/nginx/certs/{{ (printf "%s.crt" $vhost.cert) }}; ssl_certificate_key /etc/nginx/certs/{{ (printf "%s.key" $vhost.cert) }}; + {{- else }} + # No vhost certificate found, using the default certificate. + ssl_certificate /etc/nginx/certs/default.crt; + ssl_certificate_key /etc/nginx/certs/default.key; + {{- end }} {{- if (exists (printf "/etc/nginx/certs/%s.dhparam.pem" $vhost.cert)) }} ssl_dhparam {{ printf "/etc/nginx/certs/%s.dhparam.pem" $vhost.cert }}; @@ -945,14 +951,6 @@ server { } add_header Strict-Transport-Security $sts_header always; {{- end }} - {{- else if $globals.config.default_cert_ok }} - # No certificate found for this vhost, so use the default certificate and - # return an error code if the user connects via https. - ssl_certificate /etc/nginx/certs/default.crt; - ssl_certificate_key /etc/nginx/certs/default.key; - if ($https) { - return 500; - } {{- else }} # No certificate for this vhost nor default certificate found, so reject SSL handshake. ssl_reject_handshake on; diff --git a/test/test_fallback.py b/test/test_fallback.py index a8a673a..13af825 100644 --- a/test/test_fallback.py +++ b/test/test_fallback.py @@ -44,7 +44,7 @@ INTERNAL_ERR_RE = re.compile("TLSV1_UNRECOGNIZED_NAME") ("withdefault.yml", "http://http-only.nginx-proxy.test/", 200, None), ("withdefault.yml", "https://http-only.nginx-proxy.test/", 503, None), ("withdefault.yml", "http://missing-cert.nginx-proxy.test/", 200, None), - ("withdefault.yml", "https://missing-cert.nginx-proxy.test/", 500, None), + ("withdefault.yml", "https://missing-cert.nginx-proxy.test/", 200, None), ("withdefault.yml", "http://unknown.nginx-proxy.test/", 503, None), ("withdefault.yml", "https://unknown.nginx-proxy.test/", 503, None), # Same as withdefault.yml, except there is no default.crt. @@ -73,7 +73,7 @@ INTERNAL_ERR_RE = re.compile("TLSV1_UNRECOGNIZED_NAME") ("nohttp-with-missing-cert.yml", "http://https-only.nginx-proxy.test/", 503, None), ("nohttp-with-missing-cert.yml", "https://https-only.nginx-proxy.test/", 200, None), ("nohttp-with-missing-cert.yml", "http://missing-cert.nginx-proxy.test/", 200, None), - ("nohttp-with-missing-cert.yml", "https://missing-cert.nginx-proxy.test/", 500, None), + ("nohttp-with-missing-cert.yml", "https://missing-cert.nginx-proxy.test/", 200, None), ("nohttp-with-missing-cert.yml", "http://unknown.nginx-proxy.test/", 503, None), ("nohttp-with-missing-cert.yml", "https://unknown.nginx-proxy.test/", 503, None), # HTTPS_METHOD=nohttps on nginx-proxy, HTTPS_METHOD unset on the app container. From 023a3d17daa7c274b57af9089bca236c617491c0 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sun, 3 Nov 2024 21:06:23 +0100 Subject: [PATCH 2/7] fix: force enable HTTP when both vhost and default cert are missing --- docs/README.md | 10 ++++------ nginx.tmpl | 6 ++++-- test/test_fallback.data/nohttp-on-app.yml | 2 +- .../nohttp-with-missing-cert.yml | 2 +- test/test_fallback.data/nohttp.yml | 2 +- test/test_fallback.py | 16 ++++++++-------- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/README.md b/docs/README.md index 2ee008a..0462922 100644 --- a/docs/README.md +++ b/docs/README.md @@ -580,13 +580,11 @@ By default, [HTTP Strict Transport Security (HSTS)](https://developer.mozilla.or ### Missing Certificate -If no matching certificate is found for a given virtual host, nginx-proxy will: +If no matching certificate is found for a given virtual host, nginx-proxy will configure nginx to use the default certificate (`default.crt` with `default.key`). -- configure nginx to use the default certificate (`default.crt` with `default.key`), -- force enable HTTP; i.e. `HTTPS_METHOD` will switch to `noredirect` if it was set to `nohttp` or `redirect`. - If this switch to HTTP is not wanted set `ENABLE_HTTP_ON_MISSING_CERT=false` (default is `true`). - -If the default certificate is also missing, nginx-proxy will configure nginx to reject the SSL handshake. Client browsers will render a TLS error page. As of October 2024, web browsers display the following error messages: +If the default certificate is also missing, nginx-proxy will: +- force enable HTTP; i.e. `HTTPS_METHOD` will switch to `noredirect` if it was set to `nohttp` or `redirect`. If this switch to HTTP is not wanted set `ENABLE_HTTP_ON_MISSING_CERT=false` (default is `true`). +- configure nginx to reject the SSL handshake for this vhost. Client browsers will render a TLS error page. As of October 2024, web browsers display the following error messages: #### Chrome: diff --git a/nginx.tmpl b/nginx.tmpl index b9b0598..42b9911 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -681,8 +681,10 @@ proxy_set_header Proxy ""; {{- $default := eq $globals.config.default_host $hostname }} {{- $https_method := groupByKeys $vhost_containers "Env.HTTPS_METHOD" | first | default $globals.config.https_method }} {{- $enable_http_on_missing_cert := groupByKeys $vhost_containers "Env.ENABLE_HTTP_ON_MISSING_CERT" | first | default $globals.config.enable_http_on_missing_cert | parseBool }} - {{- /* When the certificate is missing we want to ensure that HTTP is enabled; hence switching from 'nohttp' or 'redirect' to 'noredirect' */}} - {{- if (and $enable_http_on_missing_cert (not $cert_ok) (or (eq $https_method "nohttp") (eq $https_method "redirect"))) }} + {{- /* When both the vhost and default certificates are missing we want to ensure that HTTP is enabled; hence switching from 'nohttp' or 'redirect' to 'noredirect' */}} + {{- $https_method_disable_http := list "nohttp" "redirect" | has $https_method }} + {{- $no_vhost_or_default_cert := or $cert_ok $globals.config.default_cert_ok | not }} + {{- if and $https_method_disable_http $no_vhost_or_default_cert $enable_http_on_missing_cert }} {{- $https_method = "noredirect" }} {{- end }} {{- $http2_enabled := groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http2.enable" | keys | first | default $globals.config.enable_http2 | parseBool }} diff --git a/test/test_fallback.data/nohttp-on-app.yml b/test/test_fallback.data/nohttp-on-app.yml index b6ca82a..6d6e390 100644 --- a/test/test_fallback.data/nohttp-on-app.yml +++ b/test/test_fallback.data/nohttp-on-app.yml @@ -5,7 +5,7 @@ services: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - - ./withdefault.certs:/etc/nginx/certs:ro + - ./nodefault.certs:/etc/nginx/certs:ro environment: HTTPS_METHOD: redirect diff --git a/test/test_fallback.data/nohttp-with-missing-cert.yml b/test/test_fallback.data/nohttp-with-missing-cert.yml index 70ece7b..eb54584 100644 --- a/test/test_fallback.data/nohttp-with-missing-cert.yml +++ b/test/test_fallback.data/nohttp-with-missing-cert.yml @@ -5,7 +5,7 @@ services: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - - ./withdefault.certs:/etc/nginx/certs:ro + - ./nodefault.certs:/etc/nginx/certs:ro environment: HTTPS_METHOD: nohttp diff --git a/test/test_fallback.data/nohttp.yml b/test/test_fallback.data/nohttp.yml index 9d47d51..0f9db05 100644 --- a/test/test_fallback.data/nohttp.yml +++ b/test/test_fallback.data/nohttp.yml @@ -5,7 +5,7 @@ services: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - - ./withdefault.certs:/etc/nginx/certs:ro + - ./nodefault.certs:/etc/nginx/certs:ro environment: HTTPS_METHOD: nohttp diff --git a/test/test_fallback.py b/test/test_fallback.py index 13af825..019eafc 100644 --- a/test/test_fallback.py +++ b/test/test_fallback.py @@ -43,7 +43,7 @@ INTERNAL_ERR_RE = re.compile("TLSV1_UNRECOGNIZED_NAME") ("withdefault.yml", "https://https-only.nginx-proxy.test/", 200, None), ("withdefault.yml", "http://http-only.nginx-proxy.test/", 200, None), ("withdefault.yml", "https://http-only.nginx-proxy.test/", 503, None), - ("withdefault.yml", "http://missing-cert.nginx-proxy.test/", 200, None), + ("withdefault.yml", "http://missing-cert.nginx-proxy.test/", 301, None), ("withdefault.yml", "https://missing-cert.nginx-proxy.test/", 200, None), ("withdefault.yml", "http://unknown.nginx-proxy.test/", 503, None), ("withdefault.yml", "https://unknown.nginx-proxy.test/", 503, None), @@ -58,24 +58,24 @@ INTERNAL_ERR_RE = re.compile("TLSV1_UNRECOGNIZED_NAME") ("nodefault.yml", "https://missing-cert.nginx-proxy.test/", None, INTERNAL_ERR_RE), ("nodefault.yml", "http://unknown.nginx-proxy.test/", 503, None), ("nodefault.yml", "https://unknown.nginx-proxy.test/", None, INTERNAL_ERR_RE), - # HTTPS_METHOD=nohttp on nginx-proxy, HTTPS_METHOD unset on the app container. + # HTTPS_METHOD=nohttp on nginx-proxy, no default.crt, HTTPS_METHOD unset on the app container. ("nohttp.yml", "http://https-only.nginx-proxy.test/", 503, None), ("nohttp.yml", "https://https-only.nginx-proxy.test/", 200, None), ("nohttp.yml", "http://unknown.nginx-proxy.test/", 503, None), - ("nohttp.yml", "https://unknown.nginx-proxy.test/", 503, None), - # HTTPS_METHOD=redirect on nginx-proxy, HTTPS_METHOD=nohttp on the app container. + ("nohttp.yml", "https://unknown.nginx-proxy.test/", None, INTERNAL_ERR_RE), + # HTTPS_METHOD=redirect on nginx-proxy, no default.crt, HTTPS_METHOD=nohttp on the app container. ("nohttp-on-app.yml", "http://https-only.nginx-proxy.test/", 503, None), ("nohttp-on-app.yml", "https://https-only.nginx-proxy.test/", 200, None), ("nohttp-on-app.yml", "http://unknown.nginx-proxy.test/", 503, None), - ("nohttp-on-app.yml", "https://unknown.nginx-proxy.test/", 503, None), - # Same as nohttp.yml, except there is a vhost with a missing cert. This causes its + ("nohttp-on-app.yml", "https://unknown.nginx-proxy.test/", None, INTERNAL_ERR_RE), + # Same as nohttp.yml, except there is a vhost with a missing cert. This causes its # HTTPS_METHOD=nohttp setting to effectively become HTTPS_METHOD=noredirect. ("nohttp-with-missing-cert.yml", "http://https-only.nginx-proxy.test/", 503, None), ("nohttp-with-missing-cert.yml", "https://https-only.nginx-proxy.test/", 200, None), ("nohttp-with-missing-cert.yml", "http://missing-cert.nginx-proxy.test/", 200, None), - ("nohttp-with-missing-cert.yml", "https://missing-cert.nginx-proxy.test/", 200, None), + ("nohttp-with-missing-cert.yml", "https://missing-cert.nginx-proxy.test/", None, INTERNAL_ERR_RE), ("nohttp-with-missing-cert.yml", "http://unknown.nginx-proxy.test/", 503, None), - ("nohttp-with-missing-cert.yml", "https://unknown.nginx-proxy.test/", 503, None), + ("nohttp-with-missing-cert.yml", "https://unknown.nginx-proxy.test/", None, INTERNAL_ERR_RE), # HTTPS_METHOD=nohttps on nginx-proxy, HTTPS_METHOD unset on the app container. ("nohttps.yml", "http://http-only.nginx-proxy.test/", 200, None), ("nohttps.yml", "https://http-only.nginx-proxy.test/", None, INTERNAL_ERR_RE), From 469765bbb719233d6053fb8d5fe9bc67237f7020 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sun, 3 Nov 2024 23:56:39 +0100 Subject: [PATCH 3/7] feat: default certificate optional trust --- nginx.tmpl | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index 42b9911..06d7b58 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -19,6 +19,7 @@ {{- $_ := set $config "external_https_port" ($globals.Env.HTTPS_PORT | default "443") }} {{- $_ := set $config "sha1_upstream_name" ($globals.Env.SHA1_UPSTREAM_NAME | default "false" | parseBool) }} {{- $_ := set $config "default_root_response" ($globals.Env.DEFAULT_ROOT | default "404") }} +{{- $_ := set $config "trust_default_cert" ($globals.Env.TRUST_DEFAULT_CERT | default "true") }} {{- $_ := set $config "trust_downstream_proxy" ($globals.Env.TRUST_DOWNSTREAM_PROXY | default "true" | parseBool) }} {{- $_ := set $config "enable_access_log" ($globals.Env.DISABLE_ACCESS_LOGS | default "false" | parseBool | not) }} {{- $_ := set $config "enable_ipv6" ($globals.Env.ENABLE_IPV6 | default "false" | parseBool) }} @@ -674,17 +675,18 @@ proxy_set_header Proxy ""; {{- $vhostCert := closest (dir "/etc/nginx/certs") (printf "%s.crt" $hostname) }} {{- $vhostCert = trimSuffix ".crt" $vhostCert }} {{- $vhostCert = trimSuffix ".key" $vhostCert }} - {{- $cert := or $certName $vhostCert }} + {{- $trust_default_cert := groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.trust-default-cert" | keys | first | default $globals.config.trust_default_cert | parseBool }} + {{- $cert := and $trust_default_cert $globals.config.default_cert_ok | ternary "default" "" }} + {{- $cert = or $certName $vhostCert $cert }} {{- $cert_ok := and (ne $cert "") (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert)) }} {{- $enable_debug_endpoint := groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.debug-endpoint" | keys | first | default $globals.config.enable_debug_endpoint | parseBool }} {{- $default := eq $globals.config.default_host $hostname }} {{- $https_method := groupByKeys $vhost_containers "Env.HTTPS_METHOD" | first | default $globals.config.https_method }} {{- $enable_http_on_missing_cert := groupByKeys $vhost_containers "Env.ENABLE_HTTP_ON_MISSING_CERT" | first | default $globals.config.enable_http_on_missing_cert | parseBool }} - {{- /* When both the vhost and default certificates are missing we want to ensure that HTTP is enabled; hence switching from 'nohttp' or 'redirect' to 'noredirect' */}} + {{- /* When no trusted certs (default and/or vhost) are present we want to ensure that HTTP is enabled; hence switching from 'nohttp' or 'redirect' to 'noredirect' */}} {{- $https_method_disable_http := list "nohttp" "redirect" | has $https_method }} - {{- $no_vhost_or_default_cert := or $cert_ok $globals.config.default_cert_ok | not }} - {{- if and $https_method_disable_http $no_vhost_or_default_cert $enable_http_on_missing_cert }} + {{- if and $https_method_disable_http (not $cert_ok) $enable_http_on_missing_cert }} {{- $https_method = "noredirect" }} {{- end }} {{- $http2_enabled := groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http2.enable" | keys | first | default $globals.config.enable_http2 | parseBool }} @@ -722,6 +724,7 @@ proxy_set_header Proxy ""; "acme_http_challenge_enabled" $acme_http_challenge_enabled "server_tokens" $server_tokens "ssl_policy" $ssl_policy + "trust_default_cert" $trust_default_cert "upstream_name" $upstream_name "vhost_root" $vhost_root ) }} @@ -920,21 +923,15 @@ server { {{- end }} {{- end }} - {{- if or $vhost.cert_ok $globals.config.default_cert_ok }} + {{- if $vhost.cert_ok }} {{- template "ssl_policy" (dict "ssl_policy" $vhost.ssl_policy) }} ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; ssl_session_tickets off; - {{- if $vhost.cert_ok }} ssl_certificate /etc/nginx/certs/{{ (printf "%s.crt" $vhost.cert) }}; ssl_certificate_key /etc/nginx/certs/{{ (printf "%s.key" $vhost.cert) }}; - {{- else }} - # No vhost certificate found, using the default certificate. - ssl_certificate /etc/nginx/certs/default.crt; - ssl_certificate_key /etc/nginx/certs/default.key; - {{- end }} {{- if (exists (printf "/etc/nginx/certs/%s.dhparam.pem" $vhost.cert)) }} ssl_dhparam {{ printf "/etc/nginx/certs/%s.dhparam.pem" $vhost.cert }}; @@ -953,6 +950,9 @@ server { } add_header Strict-Transport-Security $sts_header always; {{- end }} + {{- else if not $vhost.trust_default_cert | and $globals.config.default_cert_ok }} + # No certificate found for this vhost, and the default certificate isn't trusted, so reject SSL handshake. + ssl_reject_handshake on; {{- else }} # No certificate for this vhost nor default certificate found, so reject SSL handshake. ssl_reject_handshake on; From 40c153e8b01817c4b71c13e64719b2efbc10c67d Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Mon, 4 Nov 2024 10:19:09 +0100 Subject: [PATCH 4/7] test: default cert optional trust --- test/test_fallback.data/nohttp-on-app.yml | 2 +- .../nohttp-with-missing-cert.yml | 12 ++++++++++- test/test_fallback.data/nohttp.yml | 2 +- test/test_fallback.data/withdefault.yml | 10 +++++++++ test/test_fallback.py | 21 ++++++++++++------- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/test/test_fallback.data/nohttp-on-app.yml b/test/test_fallback.data/nohttp-on-app.yml index 6d6e390..b6ca82a 100644 --- a/test/test_fallback.data/nohttp-on-app.yml +++ b/test/test_fallback.data/nohttp-on-app.yml @@ -5,7 +5,7 @@ services: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - - ./nodefault.certs:/etc/nginx/certs:ro + - ./withdefault.certs:/etc/nginx/certs:ro environment: HTTPS_METHOD: redirect diff --git a/test/test_fallback.data/nohttp-with-missing-cert.yml b/test/test_fallback.data/nohttp-with-missing-cert.yml index eb54584..292ad74 100644 --- a/test/test_fallback.data/nohttp-with-missing-cert.yml +++ b/test/test_fallback.data/nohttp-with-missing-cert.yml @@ -5,7 +5,7 @@ services: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - - ./nodefault.certs:/etc/nginx/certs:ro + - ./withdefault.certs:/etc/nginx/certs:ro environment: HTTPS_METHOD: nohttp @@ -24,3 +24,13 @@ services: environment: WEB_PORTS: "84" VIRTUAL_HOST: missing-cert.nginx-proxy.test + + missing-cert-default-untrusted: + image: web + expose: + - "85" + environment: + WEB_PORTS: "85" + VIRTUAL_HOST: missing-cert.default-untrusted.nginx-proxy.test + labels: + com.github.nginx-proxy.nginx-proxy.trust-default-cert: "false" diff --git a/test/test_fallback.data/nohttp.yml b/test/test_fallback.data/nohttp.yml index 0f9db05..9d47d51 100644 --- a/test/test_fallback.data/nohttp.yml +++ b/test/test_fallback.data/nohttp.yml @@ -5,7 +5,7 @@ services: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - - ./nodefault.certs:/etc/nginx/certs:ro + - ./withdefault.certs:/etc/nginx/certs:ro environment: HTTPS_METHOD: nohttp diff --git a/test/test_fallback.data/withdefault.yml b/test/test_fallback.data/withdefault.yml index 46ab126..00ed4b8 100644 --- a/test/test_fallback.data/withdefault.yml +++ b/test/test_fallback.data/withdefault.yml @@ -40,3 +40,13 @@ services: environment: WEB_PORTS: "84" VIRTUAL_HOST: missing-cert.nginx-proxy.test + + missing-cert-default-untrusted: + image: web + expose: + - "85" + environment: + WEB_PORTS: "85" + VIRTUAL_HOST: missing-cert.default-untrusted.nginx-proxy.test + labels: + com.github.nginx-proxy.nginx-proxy.trust-default-cert: "false" diff --git a/test/test_fallback.py b/test/test_fallback.py index 019eafc..dd0fc77 100644 --- a/test/test_fallback.py +++ b/test/test_fallback.py @@ -45,6 +45,8 @@ INTERNAL_ERR_RE = re.compile("TLSV1_UNRECOGNIZED_NAME") ("withdefault.yml", "https://http-only.nginx-proxy.test/", 503, None), ("withdefault.yml", "http://missing-cert.nginx-proxy.test/", 301, None), ("withdefault.yml", "https://missing-cert.nginx-proxy.test/", 200, None), + ("withdefault.yml", "http://missing-cert.default-untrusted.nginx-proxy.test/", 200, None), + ("withdefault.yml", "https://missing-cert.default-untrusted.nginx-proxy.test/", None, INTERNAL_ERR_RE), ("withdefault.yml", "http://unknown.nginx-proxy.test/", 503, None), ("withdefault.yml", "https://unknown.nginx-proxy.test/", 503, None), # Same as withdefault.yml, except there is no default.crt. @@ -58,24 +60,27 @@ INTERNAL_ERR_RE = re.compile("TLSV1_UNRECOGNIZED_NAME") ("nodefault.yml", "https://missing-cert.nginx-proxy.test/", None, INTERNAL_ERR_RE), ("nodefault.yml", "http://unknown.nginx-proxy.test/", 503, None), ("nodefault.yml", "https://unknown.nginx-proxy.test/", None, INTERNAL_ERR_RE), - # HTTPS_METHOD=nohttp on nginx-proxy, no default.crt, HTTPS_METHOD unset on the app container. + # HTTPS_METHOD=nohttp on nginx-proxy, HTTPS_METHOD unset on the app container. ("nohttp.yml", "http://https-only.nginx-proxy.test/", 503, None), ("nohttp.yml", "https://https-only.nginx-proxy.test/", 200, None), ("nohttp.yml", "http://unknown.nginx-proxy.test/", 503, None), - ("nohttp.yml", "https://unknown.nginx-proxy.test/", None, INTERNAL_ERR_RE), - # HTTPS_METHOD=redirect on nginx-proxy, no default.crt, HTTPS_METHOD=nohttp on the app container. + ("nohttp.yml", "https://unknown.nginx-proxy.test/", 503, None), + # HTTPS_METHOD=redirect on nginx-proxy, HTTPS_METHOD=nohttp on the app container. ("nohttp-on-app.yml", "http://https-only.nginx-proxy.test/", 503, None), ("nohttp-on-app.yml", "https://https-only.nginx-proxy.test/", 200, None), ("nohttp-on-app.yml", "http://unknown.nginx-proxy.test/", 503, None), - ("nohttp-on-app.yml", "https://unknown.nginx-proxy.test/", None, INTERNAL_ERR_RE), - # Same as nohttp.yml, except there is a vhost with a missing cert. This causes its + ("nohttp-on-app.yml", "https://unknown.nginx-proxy.test/", 503, None), + # Same as nohttp.yml, except there are two vhosts with a missing cert, the second + # one being configured not to trust the default certificate. This causes its # HTTPS_METHOD=nohttp setting to effectively become HTTPS_METHOD=noredirect. ("nohttp-with-missing-cert.yml", "http://https-only.nginx-proxy.test/", 503, None), ("nohttp-with-missing-cert.yml", "https://https-only.nginx-proxy.test/", 200, None), - ("nohttp-with-missing-cert.yml", "http://missing-cert.nginx-proxy.test/", 200, None), - ("nohttp-with-missing-cert.yml", "https://missing-cert.nginx-proxy.test/", None, INTERNAL_ERR_RE), + ("nohttp-with-missing-cert.yml", "http://missing-cert.nginx-proxy.test/", 503, None), + ("nohttp-with-missing-cert.yml", "https://missing-cert.nginx-proxy.test/", 200, None), + ("nohttp-with-missing-cert.yml", "http://missing-cert.default-untrusted.nginx-proxy.test/", 200, None), + ("nohttp-with-missing-cert.yml", "https://missing-cert.default-untrusted.nginx-proxy.test/", None, INTERNAL_ERR_RE), ("nohttp-with-missing-cert.yml", "http://unknown.nginx-proxy.test/", 503, None), - ("nohttp-with-missing-cert.yml", "https://unknown.nginx-proxy.test/", None, INTERNAL_ERR_RE), + ("nohttp-with-missing-cert.yml", "https://unknown.nginx-proxy.test/", 503, None), # HTTPS_METHOD=nohttps on nginx-proxy, HTTPS_METHOD unset on the app container. ("nohttps.yml", "http://http-only.nginx-proxy.test/", 200, None), ("nohttps.yml", "https://http-only.nginx-proxy.test/", None, INTERNAL_ERR_RE), From ce2e076d65842cfc83908918793c19d31df45c38 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sun, 1 Dec 2024 19:35:06 +0100 Subject: [PATCH 5/7] chore: add wip folder to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5daab4f..7a6f79f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ **/__pycache__/ **/.cache/ .idea/ +wip From 07b469d8acac6b9725cd2720c98addf8d7600541 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sun, 1 Dec 2024 19:58:30 +0100 Subject: [PATCH 6/7] docs: default certificate optional trust --- docs/README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/README.md b/docs/README.md index 0462922..3d6776a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -568,8 +568,8 @@ Complete list of policies available through the `SSL_POLICY` environment variabl The default behavior for the proxy when port 80 and 443 are exposed is as follows: - If a virtual host has a usable cert, port 80 will redirect to 443 for that virtual host so that HTTPS is always preferred when available. -- If the virtual host does not have a usable cert, but `default.crt` and `default.key` exist, those will be used as the virtual host's certificate and the client browser will receive a 500 error. -- If the virtual host does not have a usable cert, and `default.crt` and `default.key` do not exist, SSL handshake will be rejected (see [Missing Certificate](#missing-certificate) below). +- If the virtual host does not have a usable cert, but `default.crt` and `default.key` exist, those will be used as the virtual host's certificate. +- If the virtual host does not have a usable cert, and `default.crt` and `default.key` do not exist, or if the virtual host is configured not to trust the default certificate, SSL handshake will be rejected (see [Default and Missing Certificate](#default-and-missing-certificate) below). To serve traffic in both SSL and non-SSL modes without redirecting to SSL, you can include the environment variable `HTTPS_METHOD=noredirect` (the default is `HTTPS_METHOD=redirect`). You can also disable the non-SSL site entirely with `HTTPS_METHOD=nohttp`, or disable the HTTPS site with `HTTPS_METHOD=nohttps`. `HTTPS_METHOD` can be specified on each container for which you want to override the default behavior or on the proxy container to set it globally. If `HTTPS_METHOD=noredirect` is used, Strict Transport Security (HSTS) is disabled to prevent HTTPS users from being redirected by the client. If you cannot get to the HTTP site after changing this setting, your browser has probably cached the HSTS policy and is automatically redirecting you back to HTTPS. You will need to clear your browser's HSTS cache or use an incognito window / different browser. @@ -578,7 +578,7 @@ By default, [HTTP Strict Transport Security (HSTS)](https://developer.mozilla.or > [!WARNING] > HSTS will force your users to visit the HTTPS version of your site for the max-age time - even if they type in http:// manually. The only way to get to an HTTP site after receiving an HSTS response is to clear your browser's HSTS cache. -### Missing Certificate +### Default and Missing Certificate If no matching certificate is found for a given virtual host, nginx-proxy will configure nginx to use the default certificate (`default.crt` with `default.key`). @@ -611,6 +611,9 @@ If the default certificate is also missing, nginx-proxy will: > > Safari can't open the page "https://example.test" because Safari can't establish a secure connection to the server "example.test". +> [!NOTE] +> Prior to version `1.7`, nginx-proxy never trusted the default certificate: when the default certificate was present, virtual hosts that did not have a usable per-virtual-host cert used the default cert but always returned a 500 error over HTTPS. If you want to restore this behaviour, you can do it globally by setting the enviroment variable `TRUST_DEFAULT_CERT` to `false` on the proxy container, or per-virtual-host by setting the label `com.github.nginx-proxy.nginx-proxy.trust-default-cert`to `false` on a proxied container. + ⬆️ [back to table of contents](#table-of-contents) ## IPv6 Support From 993bcc07c0c1ffce9e90f368b71fe46e3e17a4da Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sun, 1 Dec 2024 20:24:53 +0100 Subject: [PATCH 7/7] test: globally untrusted default cert --- test/test_fallback.data/untrusteddefault.yml | 44 ++++++++++++++++++++ test/test_fallback.py | 11 +++++ 2 files changed, 55 insertions(+) create mode 100644 test/test_fallback.data/untrusteddefault.yml diff --git a/test/test_fallback.data/untrusteddefault.yml b/test/test_fallback.data/untrusteddefault.yml new file mode 100644 index 0000000..5e9e860 --- /dev/null +++ b/test/test_fallback.data/untrusteddefault.yml @@ -0,0 +1,44 @@ +version: "2" + +services: + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./withdefault.certs:/etc/nginx/certs:ro + environment: + TRUST_DEFAULT_CERT: "false" + + https-and-http: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: https-and-http.nginx-proxy.test + + https-only: + image: web + expose: + - "82" + environment: + WEB_PORTS: "82" + VIRTUAL_HOST: https-only.nginx-proxy.test + HTTPS_METHOD: nohttp + + http-only: + image: web + expose: + - "83" + environment: + WEB_PORTS: "83" + VIRTUAL_HOST: http-only.nginx-proxy.test + HTTPS_METHOD: nohttps + + missing-cert: + image: web + expose: + - "84" + environment: + WEB_PORTS: "84" + VIRTUAL_HOST: missing-cert.nginx-proxy.test diff --git a/test/test_fallback.py b/test/test_fallback.py index dd0fc77..74dc29a 100644 --- a/test/test_fallback.py +++ b/test/test_fallback.py @@ -49,6 +49,17 @@ INTERNAL_ERR_RE = re.compile("TLSV1_UNRECOGNIZED_NAME") ("withdefault.yml", "https://missing-cert.default-untrusted.nginx-proxy.test/", None, INTERNAL_ERR_RE), ("withdefault.yml", "http://unknown.nginx-proxy.test/", 503, None), ("withdefault.yml", "https://unknown.nginx-proxy.test/", 503, None), + # Same as withdefault.yml, except default.crt is not trusted (TRUST_DEFAULT_CERT=false). + ("untrusteddefault.yml", "http://https-and-http.nginx-proxy.test/", 301, None), + ("untrusteddefault.yml", "https://https-and-http.nginx-proxy.test/", 200, None), + ("untrusteddefault.yml", "http://https-only.nginx-proxy.test/", 503, None), + ("untrusteddefault.yml", "https://https-only.nginx-proxy.test/", 200, None), + ("untrusteddefault.yml", "http://http-only.nginx-proxy.test/", 200, None), + ("untrusteddefault.yml", "https://http-only.nginx-proxy.test/", 503, None), + ("untrusteddefault.yml", "http://missing-cert.nginx-proxy.test/", 200, None), + ("untrusteddefault.yml", "https://missing-cert.nginx-proxy.test/", None, INTERNAL_ERR_RE), + ("untrusteddefault.yml", "http://unknown.nginx-proxy.test/", 503, None), + ("untrusteddefault.yml", "https://unknown.nginx-proxy.test/", 503, None), # Same as withdefault.yml, except there is no default.crt. ("nodefault.yml", "http://https-and-http.nginx-proxy.test/", 301, None), ("nodefault.yml", "https://https-and-http.nginx-proxy.test/", 200, None),