From 8fed348ff729f3ae70a9d78c61648e8ced5e1a65 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Wed, 9 Oct 2024 20:01:01 +0200 Subject: [PATCH 1/8] refactor: move global config properties to a sub dict --- nginx.tmpl | 96 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index 2348ea7..d9b758f 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -11,15 +11,19 @@ {{- $_ := set $globals "Env" $.Env }} {{- $_ := set $globals "Docker" $.Docker }} {{- $_ := set $globals "CurrentContainer" (where $globals.containers "ID" $globals.Docker.CurrentContainerID | first) }} -{{- $_ := set $globals "default_cert_ok" (and (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }} -{{- $_ := set $globals "external_http_port" (coalesce $globals.Env.HTTP_PORT "80") }} -{{- $_ := set $globals "external_https_port" (coalesce $globals.Env.HTTPS_PORT "443") }} -{{- $_ := set $globals "sha1_upstream_name" (parseBool (coalesce $globals.Env.SHA1_UPSTREAM_NAME "false")) }} -{{- $_ := set $globals "default_root_response" (coalesce $globals.Env.DEFAULT_ROOT "404") }} -{{- $_ := set $globals "trust_downstream_proxy" (parseBool (coalesce $globals.Env.TRUST_DOWNSTREAM_PROXY "true")) }} -{{- $_ := set $globals "access_log" (or (and (not $globals.Env.DISABLE_ACCESS_LOGS) "access_log /var/log/nginx/access.log vhost;") "") }} -{{- $_ := set $globals "enable_ipv6" (parseBool (coalesce $globals.Env.ENABLE_IPV6 "false")) }} -{{- $_ := set $globals "ssl_policy" (or ($globals.Env.SSL_POLICY) "Mozilla-Intermediate") }} + +{{- $config := dict }} +{{- $_ := set $config "default_cert_ok" (and (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }} +{{- $_ := set $config "external_http_port" (coalesce $globals.Env.HTTP_PORT "80") }} +{{- $_ := set $config "external_https_port" (coalesce $globals.Env.HTTPS_PORT "443") }} +{{- $_ := set $config "sha1_upstream_name" (parseBool (coalesce $globals.Env.SHA1_UPSTREAM_NAME "false")) }} +{{- $_ := set $config "default_root_response" (coalesce $globals.Env.DEFAULT_ROOT "404") }} +{{- $_ := set $config "trust_downstream_proxy" (parseBool (coalesce $globals.Env.TRUST_DOWNSTREAM_PROXY "true")) }} +{{- $_ := set $config "access_log" (or (and (not $globals.Env.DISABLE_ACCESS_LOGS) "access_log /var/log/nginx/access.log vhost;") "") }} +{{- $_ := set $config "enable_ipv6" (parseBool (coalesce $globals.Env.ENABLE_IPV6 "false")) }} +{{- $_ := set $config "ssl_policy" (or ($globals.Env.SSL_POLICY) "Mozilla-Intermediate") }} +{{- $_ := set $globals "config" $config }} + {{- $_ := set $globals "vhosts" (dict) }} {{- $_ := set $globals "networks" (dict) }} # Networks available to the container running docker-gen (which are assumed to @@ -347,19 +351,19 @@ upstream {{ $vpath.upstream }} { # If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the # scheme used to connect to this server map $http_x_forwarded_proto $proxy_x_forwarded_proto { - default {{ if $globals.trust_downstream_proxy }}$http_x_forwarded_proto{{ else }}$scheme{{ end }}; + default {{ if $globals.config.trust_downstream_proxy }}$http_x_forwarded_proto{{ else }}$scheme{{ end }}; '' $scheme; } map $http_x_forwarded_host $proxy_x_forwarded_host { - default {{ if $globals.trust_downstream_proxy }}$http_x_forwarded_host{{ else }}$host{{ end }}; + default {{ if $globals.config.trust_downstream_proxy }}$http_x_forwarded_host{{ else }}$host{{ end }}; '' $host; } # If we receive X-Forwarded-Port, pass it through; otherwise, pass along the # server port the client connected to map $http_x_forwarded_port $proxy_x_forwarded_port { - default {{ if $globals.trust_downstream_proxy }}$http_x_forwarded_port{{ else }}$server_port{{ end }}; + default {{ if $globals.config.trust_downstream_proxy }}$http_x_forwarded_port{{ else }}$server_port{{ end }}; '' $server_port; } @@ -440,7 +444,7 @@ access_log off; * if at least one vhost use a TLSv1 or TLSv1.1 policy * so TLSv1 and TLSv1.1 can be enabled on those vhosts */}} -{{- $httpContextSslPolicy := $globals.ssl_policy }} +{{- $httpContextSslPolicy := $globals.config.ssl_policy }} {{- $inUseSslPolicies := groupByKeys $globals.containers "Env.SSL_POLICY" }} {{- range $tls1Policy := list "AWS-TLS13-1-1-2021-06" "AWS-TLS13-1-0-2021-06" "AWS-FS-1-1-2019-08" "AWS-FS-2018-06" "AWS-TLS-1-1-2017-01" "AWS-2016-08" "AWS-2015-05" "AWS-2015-03" "AWS-2015-02" "Mozilla-Old" }} {{- if has $tls1Policy $inUseSslPolicies }} @@ -518,7 +522,7 @@ proxy_set_header Proxy ""; {{- end }} {{- $_ := set $vhost_data "paths" $paths }} {{- $is_regexp := hasPrefix "~" $hostname }} - {{- $_ := set $vhost_data "upstream_name" (when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname) }} + {{- $_ := set $vhost_data "upstream_name" (when (or $is_regexp $globals.config.sha1_upstream_name) (sha1 $hostname) $hostname) }} {{- $_ := set $globals.vhosts $hostname $vhost_data }} {{- end }} {{- end }} @@ -564,7 +568,7 @@ proxy_set_header Proxy ""; {{- end }} {{- $_ := set $vhost_data "paths" $paths }} {{- $is_regexp := hasPrefix "~" $hostname }} - {{- $_ := set $vhost_data "upstream_name" (when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname) }} + {{- $_ := set $vhost_data "upstream_name" (when (or $is_regexp $globals.config.sha1_upstream_name) (sha1 $hostname) $hostname) }} {{- $_ := set $globals.vhosts $hostname $vhost_data }} {{- end }} @@ -700,30 +704,30 @@ proxy_set_header Proxy ""; server { server_name _; # This is just an invalid value which will never trigger on a real hostname. server_tokens off; - {{ $globals.access_log }} + {{ $globals.config.access_log }} http2 on; {{- if $fallback_http }} - listen {{ $globals.external_http_port }}; {{- /* Do not add `default_server` (see comment above). */}} - {{- if $globals.enable_ipv6 }} - listen [::]:{{ $globals.external_http_port }}; {{- /* Do not add `default_server` (see comment above). */}} + listen {{ $globals.config.external_http_port }}; {{- /* Do not add `default_server` (see comment above). */}} + {{- if $globals.config.enable_ipv6 }} + listen [::]:{{ $globals.config.external_http_port }}; {{- /* Do not add `default_server` (see comment above). */}} {{- end }} {{- end }} {{- if $fallback_https }} - listen {{ $globals.external_https_port }} ssl; {{- /* Do not add `default_server` (see comment above). */}} - {{- if $globals.enable_ipv6 }} - listen [::]:{{ $globals.external_https_port }} ssl; {{- /* Do not add `default_server` (see comment above). */}} + listen {{ $globals.config.external_https_port }} ssl; {{- /* Do not add `default_server` (see comment above). */}} + {{- if $globals.config.enable_ipv6 }} + listen [::]:{{ $globals.config.external_https_port }} ssl; {{- /* Do not add `default_server` (see comment above). */}} {{- end }} {{- if $http3_enabled }} http3 on; - listen {{ $globals.external_https_port }} quic reuseport; {{- /* Do not add `default_server` (see comment above). */}} - {{- if $globals.enable_ipv6 }} - listen [::]:{{ $globals.external_https_port }} quic reuseport; {{- /* Do not add `default_server` (see comment above). */}} + listen {{ $globals.config.external_https_port }} quic reuseport; {{- /* Do not add `default_server` (see comment above). */}} + {{- if $globals.config.enable_ipv6 }} + listen [::]:{{ $globals.config.external_https_port }} quic reuseport; {{- /* Do not add `default_server` (see comment above). */}} {{- end }} {{- end }} ssl_session_cache shared:SSL:50m; ssl_session_tickets off; {{- end }} - {{- if $globals.default_cert_ok }} + {{- if $globals.config.default_cert_ok }} ssl_certificate /etc/nginx/certs/default.crt; ssl_certificate_key /etc/nginx/certs/default.key; {{- else }} @@ -759,10 +763,10 @@ server { {{- if $vhost.server_tokens }} server_tokens {{ $vhost.server_tokens }}; {{- end }} - {{ $globals.access_log }} - listen {{ $globals.external_http_port }} {{ $default_server }}; - {{- if $globals.enable_ipv6 }} - listen [::]:{{ $globals.external_http_port }} {{ $default_server }}; + {{ $globals.config.access_log }} + listen {{ $globals.config.external_http_port }} {{ $default_server }}; + {{- if $globals.config.enable_ipv6 }} + listen [::]:{{ $globals.config.external_http_port }} {{ $default_server }}; {{- end }} {{- if (or $vhost.acme_http_challenge_legacy $vhost.acme_http_challenge_enabled) }} @@ -778,10 +782,10 @@ server { {{- end }} location / { - {{- if eq $globals.external_https_port "443" }} + {{- if eq $globals.config.external_https_port "443" }} return 301 https://$host$request_uri; {{- else }} - return 301 https://$host:{{ $globals.external_https_port }}$request_uri; + return 301 https://$host:{{ $globals.config.external_https_port }}$request_uri; {{- end }} } } @@ -809,14 +813,14 @@ server { {{- if $vhost.server_tokens }} server_tokens {{ $vhost.server_tokens }}; {{- end }} - {{ $globals.access_log }} + {{ $globals.config.access_log }} {{- if $vhost.http2_enabled }} http2 on; {{- end }} {{- if or (eq $vhost.https_method "nohttps") (eq $vhost.https_method "noredirect") }} - listen {{ $globals.external_http_port }} {{ $default_server }}; - {{- if $globals.enable_ipv6 }} - listen [::]:{{ $globals.external_http_port }} {{ $default_server }}; + listen {{ $globals.config.external_http_port }} {{ $default_server }}; + {{- if $globals.config.enable_ipv6 }} + listen [::]:{{ $globals.config.external_http_port }} {{ $default_server }}; {{- end }} {{- if (and (eq $vhost.https_method "noredirect") $vhost.acme_http_challenge_enabled) }} @@ -830,17 +834,17 @@ server { {{- end }} {{- end }} {{- if ne $vhost.https_method "nohttps" }} - listen {{ $globals.external_https_port }} ssl {{ $default_server }}; - {{- if $globals.enable_ipv6 }} - listen [::]:{{ $globals.external_https_port }} ssl {{ $default_server }}; + listen {{ $globals.config.external_https_port }} ssl {{ $default_server }}; + {{- if $globals.config.enable_ipv6 }} + listen [::]:{{ $globals.config.external_https_port }} ssl {{ $default_server }}; {{- end }} {{- if $vhost.http3_enabled }} http3 on; - add_header alt-svc 'h3=":{{ $globals.external_https_port }}"; ma=86400;'; - listen {{ $globals.external_https_port }} quic {{ $default_server }}; - {{- if $globals.enable_ipv6 }} - listen [::]:{{ $globals.external_https_port }} quic {{ $default_server }}; + add_header alt-svc 'h3=":{{ $globals.config.external_https_port }}"; ma=86400;'; + listen {{ $globals.config.external_https_port }} quic {{ $default_server }}; + {{- if $globals.config.enable_ipv6 }} + listen [::]:{{ $globals.config.external_https_port }} quic {{ $default_server }}; {{- end }} {{- end }} @@ -871,7 +875,7 @@ server { } add_header Strict-Transport-Security $sts_header always; {{- end }} - {{- else if $globals.default_cert_ok }} + {{- 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; @@ -903,9 +907,9 @@ server { ) }} {{- end }} - {{- if and (not (contains $vhost.paths "/")) (ne $globals.default_root_response "none")}} + {{- if and (not (contains $vhost.paths "/")) (ne $globals.config.default_root_response "none")}} location / { - return {{ $globals.default_root_response }}; + return {{ $globals.config.default_root_response }}; } {{- end }} } From ebed622fd76dbe3e34681348fde4565c2b87375d Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Wed, 16 Oct 2024 22:06:32 +0200 Subject: [PATCH 2/8] feat: nginx-proxy debug endpoint --- nginx.tmpl | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/nginx.tmpl b/nginx.tmpl index d9b758f..2202a85 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -13,6 +13,7 @@ {{- $_ := set $globals "CurrentContainer" (where $globals.containers "ID" $globals.Docker.CurrentContainerID | first) }} {{- $config := dict }} +{{- $_ := set $config "nginx_proxy_version" $.Env.NGINX_PROXY_VERSION }} {{- $_ := set $config "default_cert_ok" (and (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }} {{- $_ := set $config "external_http_port" (coalesce $globals.Env.HTTP_PORT "80") }} {{- $_ := set $config "external_https_port" (coalesce $globals.Env.HTTPS_PORT "443") }} @@ -22,6 +23,7 @@ {{- $_ := set $config "access_log" (or (and (not $globals.Env.DISABLE_ACCESS_LOGS) "access_log /var/log/nginx/access.log vhost;") "") }} {{- $_ := set $config "enable_ipv6" (parseBool (coalesce $globals.Env.ENABLE_IPV6 "false")) }} {{- $_ := set $config "ssl_policy" (or ($globals.Env.SSL_POLICY) "Mozilla-Intermediate") }} +{{- $_ := set $config "enable_debug_endpoint" ($globals.Env.DEBUG_ENDPOINT | default "false") }} {{- $_ := set $globals "config" $config }} {{- $_ := set $globals "vhosts" (dict) }} @@ -348,6 +350,42 @@ upstream {{ $vpath.upstream }} { } {{- end }} +{{- /* debug "endpoint" response template */}} +{{- define "debug_response" }} + {{- $debug_paths := dict }} + {{- range $path, $vpath := .VHost.paths }} + {{- $tmp_port := dict }} + {{- range $port, $containers := $vpath.ports }} + {{- $tmp_containers := list }} + {{- range $container := $containers }} + {{- $tmp_containers = dict "Name" $container.Name | append $tmp_containers }} + {{- end }} + {{- $_ := dict $port $tmp_containers | set $tmp_port "ports" }} + {{- $tmp_port = deepCopy $vpath | merge $tmp_port }} + {{- end }} + {{- $_ := set $debug_paths $path $tmp_port }} + {{- end }} + + {{- $debug_vhost := deepCopy .VHost }} + {{- $_ := set $debug_vhost "paths" $debug_paths }} + + {{- $debug_response := dict + "global" .GlobalConfig + "hostname" .Hostname + "request" (dict + "host" "$host" + "https" "$https" + "http2" "$http2" + "http3" "$http3" + "ssl_cipher" "$ssl_cipher" + "ssl_protocol" "$ssl_protocol" + ) + "vhost" $debug_vhost + }} + + {{- toJson $debug_response }} +{{- end }} + # If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the # scheme used to connect to this server map $http_x_forwarded_proto $proxy_x_forwarded_proto { @@ -614,6 +652,7 @@ proxy_set_header Proxy ""; {{- $cert := or $certName $vhostCert }} {{- $cert_ok := and (ne $cert "") (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert)) }} + {{- $enable_debug_endpoint := coalesce (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.debug-endpoint" | keys | first) $globals.config.enable_debug_endpoint | parseBool }} {{- $default := eq $globals.Env.DEFAULT_HOST $hostname }} {{- $https_method := or (first (groupByKeys $vhost_containers "Env.HTTPS_METHOD")) $globals.Env.HTTPS_METHOD "redirect" }} {{- $enable_http_on_missing_cert := parseBool (or (first (groupByKeys $vhost_containers "Env.ENABLE_HTTP_ON_MISSING_CERT")) $globals.Env.ENABLE_HTTP_ON_MISSING_CERT "true") }} @@ -645,6 +684,7 @@ proxy_set_header Proxy ""; {{- $vhost_data = merge $vhost_data (dict "cert" $cert "cert_ok" $cert_ok + "enable_debug_endpoint" $enable_debug_endpoint "default" $default "hsts" $hsts "https_method" $https_method @@ -780,6 +820,13 @@ server { break; } {{- end }} + + {{- if $vhost.enable_debug_endpoint }} + location /nginx-proxy-debug { + default_type application/json; + return 200 '{{- template "debug_response" (dict "GlobalConfig" $globals.config "Hostname" $hostname "VHost" $vhost) }}'; + } + {{- end }} location / { {{- if eq $globals.config.external_https_port "443" }} @@ -897,6 +944,13 @@ server { include /etc/nginx/vhost.d/default; {{- end }} + {{- if $vhost.enable_debug_endpoint }} + location /nginx-proxy-debug { + default_type application/json; + return 200 '{{- template "debug_response" (dict "GlobalConfig" $globals.config "Hostname" $hostname "VHost" $vhost) }}'; + } + {{- end }} + {{- range $path, $vpath := $vhost.paths }} {{- template "location" (dict "Path" $path From fe52878940813cebbfaab019e54c7131e8807d29 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Wed, 16 Oct 2024 22:41:32 +0200 Subject: [PATCH 3/8] refactor: expose clearly access log status in debug endpoint --- nginx.tmpl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index 2202a85..c7b5b2d 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -20,7 +20,7 @@ {{- $_ := set $config "sha1_upstream_name" (parseBool (coalesce $globals.Env.SHA1_UPSTREAM_NAME "false")) }} {{- $_ := set $config "default_root_response" (coalesce $globals.Env.DEFAULT_ROOT "404") }} {{- $_ := set $config "trust_downstream_proxy" (parseBool (coalesce $globals.Env.TRUST_DOWNSTREAM_PROXY "true")) }} -{{- $_ := set $config "access_log" (or (and (not $globals.Env.DISABLE_ACCESS_LOGS) "access_log /var/log/nginx/access.log vhost;") "") }} +{{- $_ := set $config "enable_access_log" ($globals.Env.DISABLE_ACCESS_LOGS | default "false" | parseBool | not) }} {{- $_ := set $config "enable_ipv6" (parseBool (coalesce $globals.Env.ENABLE_IPV6 "false")) }} {{- $_ := set $config "ssl_policy" (or ($globals.Env.SSL_POLICY) "Mozilla-Intermediate") }} {{- $_ := set $config "enable_debug_endpoint" ($globals.Env.DEBUG_ENDPOINT | default "false") }} @@ -386,6 +386,10 @@ upstream {{ $vpath.upstream }} { {{- toJson $debug_response }} {{- end }} +{{- define "access_log" }} + {{- when .Enable "access_log /var/log/nginx/access.log vhost;" "" }} +{{- end }} + # If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the # scheme used to connect to this server map $http_x_forwarded_proto $proxy_x_forwarded_proto { @@ -744,7 +748,7 @@ proxy_set_header Proxy ""; server { server_name _; # This is just an invalid value which will never trigger on a real hostname. server_tokens off; - {{ $globals.config.access_log }} + {{ template "access_log" (dict "Enable" $globals.config.enable_access_log) }} http2 on; {{- if $fallback_http }} listen {{ $globals.config.external_http_port }}; {{- /* Do not add `default_server` (see comment above). */}} @@ -803,7 +807,7 @@ server { {{- if $vhost.server_tokens }} server_tokens {{ $vhost.server_tokens }}; {{- end }} - {{ $globals.config.access_log }} + {{ template "access_log" (dict "Enable" $globals.config.enable_access_log) }} listen {{ $globals.config.external_http_port }} {{ $default_server }}; {{- if $globals.config.enable_ipv6 }} listen [::]:{{ $globals.config.external_http_port }} {{ $default_server }}; @@ -860,7 +864,7 @@ server { {{- if $vhost.server_tokens }} server_tokens {{ $vhost.server_tokens }}; {{- end }} - {{ $globals.config.access_log }} + {{ template "access_log" (dict "Enable" $globals.config.enable_access_log) }} {{- if $vhost.http2_enabled }} http2 on; {{- end }} From dce7663b691f88cac3e44da185aaec333377f0c7 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Wed, 16 Oct 2024 23:16:48 +0200 Subject: [PATCH 4/8] refactor: remove duplicate code --- nginx.tmpl | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index c7b5b2d..6b2a99e 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -350,8 +350,8 @@ upstream {{ $vpath.upstream }} { } {{- end }} -{{- /* debug "endpoint" response template */}} -{{- define "debug_response" }} +{{- /* debug "endpoint" location template */}} +{{- define "debug_location" }} {{- $debug_paths := dict }} {{- range $path, $vpath := .VHost.paths }} {{- $tmp_port := dict }} @@ -382,8 +382,11 @@ upstream {{ $vpath.upstream }} { ) "vhost" $debug_vhost }} - - {{- toJson $debug_response }} + + location /nginx-proxy-debug { + default_type application/json; + return 200 '{{ toJson $debug_response }}'; + } {{- end }} {{- define "access_log" }} @@ -826,10 +829,7 @@ server { {{- end }} {{- if $vhost.enable_debug_endpoint }} - location /nginx-proxy-debug { - default_type application/json; - return 200 '{{- template "debug_response" (dict "GlobalConfig" $globals.config "Hostname" $hostname "VHost" $vhost) }}'; - } + {{ template "debug_location" (dict "GlobalConfig" $globals.config "Hostname" $hostname "VHost" $vhost) }} {{- end }} location / { @@ -949,10 +949,7 @@ server { {{- end }} {{- if $vhost.enable_debug_endpoint }} - location /nginx-proxy-debug { - default_type application/json; - return 200 '{{- template "debug_response" (dict "GlobalConfig" $globals.config "Hostname" $hostname "VHost" $vhost) }}'; - } + {{ template "debug_location" (dict "GlobalConfig" $globals.config "Hostname" $hostname "VHost" $vhost) }} {{- end }} {{- range $path, $vpath := $vhost.paths }} From 7dafac8b8737bbb7809ba76ccb1474b0af9816f7 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Thu, 17 Oct 2024 08:13:11 +0200 Subject: [PATCH 5/8] docs: documentation for debug endpoint --- docs/README.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 3990a7a..fbfe1e2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -982,7 +982,7 @@ docker exec nginx -T Pay attention to the `upstream` definition blocks, which should look like this: -```Nginx +```nginx # foo.example.com upstream foo.example.com { ## Can be connected with "my_network" network @@ -1002,6 +1002,101 @@ The effective `Port` is retrieved by order of precedence: 1. From the container's exposed port if there is only one 1. From the default port 80 when none of the above methods apply +### Debug endpoint + +The debug endpoint can be enabled: +- globally by setting the `DEBUG_ENDPOINT` environment variable to `true` on the nginx-proxy container. +- per container by setting the `com.github.nginx-proxy.nginx-proxy.debug-endpoint` label to `true` on a proxied container. + +Enabling it will expose the endpoint at `/nginx-proxy-debug`. + +Querying the debug endpoint will show the global config, along with the virtual host and per path configs in JSON format. + +```yaml +services: + nginx-proxy: + image: nginxproxy/nginx-proxy + ports: + - "80:80" + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + environment: + DEBUG_ENDPOINT: "true" + + test: + image: nginx + environment: + VIRTUAL_HOST: test.nginx-proxy.tld +``` + +(on the CLI, using [`jq`](https://jqlang.github.io/jq/) to format the output of `curl` is recommended) + +```console +curl -s -H "Host: test.nginx-proxy.tld" localhost/nginx-proxy-debug | jq +``` + +```json +{ + "global": { + "default_cert_ok": false, + "default_root_response": "404", + "enable_access_log": true, + "enable_debug_endpoint": "true", + "enable_ipv6": false, + "external_http_port": "80", + "external_https_port": "443", + "nginx_proxy_version": "local", + "sha1_upstream_name": false, + "ssl_policy": "Mozilla-Intermediate", + "trust_downstream_proxy": true + }, + "hostname": "test.nginx-proxy.tld", + "request": { + "host": "test.nginx-proxy.tld", + "http2": "", + "http3": "", + "https": "", + "ssl_cipher": "", + "ssl_protocol": "" + }, + "vhost": { + "acme_http_challenge_enabled": true, + "acme_http_challenge_legacy": false, + "cert": "", + "cert_ok": false, + "default": false, + "enable_debug_endpoint": true, + "hsts": "max-age=31536000", + "http2_enabled": true, + "http3_enabled": false, + "https_method": "noredirect", + "paths": { + "/": { + "dest": "", + "keepalive": "disabled", + "network_tag": "external", + "ports": { + "legacy": [ + { + "Name": "wip-test-1" + } + ] + }, + "proto": "http", + "upstream": "test.nginx-proxy.tld" + } + }, + "server_tokens": "", + "ssl_policy": "", + "upstream_name": "test.nginx-proxy.tld", + "vhost_root": "/var/www/public" + } +} +``` + +:warning: please be aware that the debug endpoint work by rendering the response straight to the nginx configuration, which might result in an unparseable configuration if it exceeds nginx line character limit. Only activate it when needed. + + ⬆️ [back to table of contents](#table-of-contents) ## Contributing From 32ad9b7102cb9a953da7927ec54e56e03494e078 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Thu, 17 Oct 2024 09:08:27 +0200 Subject: [PATCH 6/8] feat: protection against too long debug response --- docs/README.md | 2 +- nginx.tmpl | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index fbfe1e2..06e6b23 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1094,7 +1094,7 @@ curl -s -H "Host: test.nginx-proxy.tld" localhost/nginx-proxy-debug | jq } ``` -:warning: please be aware that the debug endpoint work by rendering the response straight to the nginx configuration, which might result in an unparseable configuration if it exceeds nginx line character limit. Only activate it when needed. +:warning: please be aware that the debug endpoint work by rendering the JSON response straight to the nginx configuration in plaintext. nginx has an upper limit on the size of the configuration files it can parse, so only activate it when needed, and preferably on a per container basis if your setup has a large number of virtual hosts. ⬆️ [back to table of contents](#table-of-contents) diff --git a/nginx.tmpl b/nginx.tmpl index 6b2a99e..2021e7b 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -383,6 +383,16 @@ upstream {{ $vpath.upstream }} { "vhost" $debug_vhost }} + {{- /* + * The maximum line length in an nginx config is 4096 characters. + * If we're nearing this limit (with headroom for the rest + * of the directive), strip vhost.paths from the response. + */}} + {{- if gt (toJson $debug_response | len) 4000 }} + {{- $_ := unset $debug_vhost "paths" }} + {{- $_ := set $debug_response "warning" "Virtual paths configuration for this hostname is too large and has been stripped from response." }} + {{- end }} + location /nginx-proxy-debug { default_type application/json; return 200 '{{ toJson $debug_response }}'; From 190030745c608e6d48a0b937cf05cf04314b61e0 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sat, 2 Nov 2024 16:48:55 +0100 Subject: [PATCH 7/8] test: nginx-proxy debug endpoint --- test/test_debug_endpoint/test_global.py | 24 ++++++++++++++ test/test_debug_endpoint/test_global.yml | 33 +++++++++++++++++++ .../test_debug_endpoint/test_per_container.py | 24 ++++++++++++++ .../test_per_container.yml | 32 ++++++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 test/test_debug_endpoint/test_global.py create mode 100644 test/test_debug_endpoint/test_global.yml create mode 100644 test/test_debug_endpoint/test_per_container.py create mode 100644 test/test_debug_endpoint/test_per_container.yml diff --git a/test/test_debug_endpoint/test_global.py b/test/test_debug_endpoint/test_global.py new file mode 100644 index 0000000..635aaab --- /dev/null +++ b/test/test_debug_endpoint/test_global.py @@ -0,0 +1,24 @@ +import json +import pytest + +def test_debug_endpoint_is_enabled_globally(docker_compose, nginxproxy): + r = nginxproxy.get("http://enabled1.debug.nginx-proxy.example/nginx-proxy-debug") + assert r.status_code == 200 + r = nginxproxy.get("http://enabled2.debug.nginx-proxy.example/nginx-proxy-debug") + assert r.status_code == 200 + + +def test_debug_endpoint_response_contains_expected_values(docker_compose, nginxproxy): + r = nginxproxy.get("http://enabled1.debug.nginx-proxy.example/nginx-proxy-debug") + assert r.status_code == 200 + try: + jsonResponse = json.loads(r.text) + except ValueError as err: + pytest.fail("Failed to parse JSON response: %s" % err, pytrace=False) + assert jsonResponse["global"]["enable_debug_endpoint"] == "true" + assert jsonResponse["vhost"]["enable_debug_endpoint"] == True + + +def test_debug_endpoint_is_disabled_per_container(docker_compose, nginxproxy): + r = nginxproxy.get("http://disabled.debug.nginx-proxy.example/nginx-proxy-debug") + assert r.status_code == 404 diff --git a/test/test_debug_endpoint/test_global.yml b/test/test_debug_endpoint/test_global.yml new file mode 100644 index 0000000..812ffed --- /dev/null +++ b/test/test_debug_endpoint/test_global.yml @@ -0,0 +1,33 @@ +services: + nginx-proxy: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + environment: + DEBUG_ENDPOINT: "true" + + debug_enabled1: + image: web + expose: + - "81" + environment: + WEB_PORTS: 81 + VIRTUAL_HOST: enabled1.debug.nginx-proxy.example + + debug_enabled2: + image: web + expose: + - "82" + environment: + WEB_PORTS: 82 + VIRTUAL_HOST: enabled2.debug.nginx-proxy.example + + debug_disabled: + image: web + expose: + - "83" + environment: + WEB_PORTS: 83 + VIRTUAL_HOST: disabled.debug.nginx-proxy.example + labels: + com.github.nginx-proxy.nginx-proxy.debug-endpoint: "false" diff --git a/test/test_debug_endpoint/test_per_container.py b/test/test_debug_endpoint/test_per_container.py new file mode 100644 index 0000000..8901f8b --- /dev/null +++ b/test/test_debug_endpoint/test_per_container.py @@ -0,0 +1,24 @@ +import json +import pytest + +def test_debug_endpoint_is_disabled_globally(docker_compose, nginxproxy): + r = nginxproxy.get("http://disabled1.debug.nginx-proxy.example/nginx-proxy-debug") + assert r.status_code == 404 + r = nginxproxy.get("http://disabled2.debug.nginx-proxy.example/nginx-proxy-debug") + assert r.status_code == 404 + + +def test_debug_endpoint_is_enabled_per_container(docker_compose, nginxproxy): + r = nginxproxy.get("http://enabled.debug.nginx-proxy.example/nginx-proxy-debug") + assert r.status_code == 200 + + +def test_debug_endpoint_response_contains_expected_values(docker_compose, nginxproxy): + r = nginxproxy.get("http://enabled.debug.nginx-proxy.example/nginx-proxy-debug") + assert r.status_code == 200 + try: + jsonResponse = json.loads(r.text) + except ValueError as err: + pytest.fail("Failed to parse JSON response: %s" % err, pytrace=False) + assert jsonResponse["global"]["enable_debug_endpoint"] == "false" + assert jsonResponse["vhost"]["enable_debug_endpoint"] == True diff --git a/test/test_debug_endpoint/test_per_container.yml b/test/test_debug_endpoint/test_per_container.yml new file mode 100644 index 0000000..56c975c --- /dev/null +++ b/test/test_debug_endpoint/test_per_container.yml @@ -0,0 +1,32 @@ +services: + nginx-proxy: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + + debug_disabled1: + image: web + expose: + - "81" + environment: + WEB_PORTS: 81 + VIRTUAL_HOST: disabled1.debug.nginx-proxy.example + + debug_disabled2: + image: web + expose: + - "82" + environment: + WEB_PORTS: 82 + VIRTUAL_HOST: disabled2.debug.nginx-proxy.example + + + debug_enabled: + image: web + expose: + - "83" + environment: + WEB_PORTS: 83 + VIRTUAL_HOST: enabled.debug.nginx-proxy.example + labels: + com.github.nginx-proxy.nginx-proxy.debug-endpoint: "true" From 9114b8047d643e99a7ec000128a731b2fbe97c24 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sat, 2 Nov 2024 17:14:34 +0100 Subject: [PATCH 8/8] test: paths are stripped from debug endpoint response if too long --- test/test_debug_endpoint/test_global.py | 20 ++++++++++--- test/test_debug_endpoint/test_global.yml | 29 ++++++++++++++++--- .../test_debug_endpoint/test_per_container.py | 2 +- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/test/test_debug_endpoint/test_global.py b/test/test_debug_endpoint/test_global.py index 635aaab..b151b69 100644 --- a/test/test_debug_endpoint/test_global.py +++ b/test/test_debug_endpoint/test_global.py @@ -2,23 +2,35 @@ import json import pytest def test_debug_endpoint_is_enabled_globally(docker_compose, nginxproxy): - r = nginxproxy.get("http://enabled1.debug.nginx-proxy.example/nginx-proxy-debug") + r = nginxproxy.get("http://enabled.debug.nginx-proxy.example/nginx-proxy-debug") assert r.status_code == 200 - r = nginxproxy.get("http://enabled2.debug.nginx-proxy.example/nginx-proxy-debug") + r = nginxproxy.get("http://stripped.debug.nginx-proxy.example/nginx-proxy-debug") assert r.status_code == 200 def test_debug_endpoint_response_contains_expected_values(docker_compose, nginxproxy): - r = nginxproxy.get("http://enabled1.debug.nginx-proxy.example/nginx-proxy-debug") + r = nginxproxy.get("http://enabled.debug.nginx-proxy.example/nginx-proxy-debug") assert r.status_code == 200 try: jsonResponse = json.loads(r.text) except ValueError as err: - pytest.fail("Failed to parse JSON response: %s" % err, pytrace=False) + pytest.fail("Failed to parse debug endpoint response as JSON: %s" % err, pytrace=False) assert jsonResponse["global"]["enable_debug_endpoint"] == "true" assert jsonResponse["vhost"]["enable_debug_endpoint"] == True +def test_debug_endpoint_pahts_stripped_if_response_too_long(docker_compose, nginxproxy): + r = nginxproxy.get("http://stripped.debug.nginx-proxy.example/nginx-proxy-debug") + assert r.status_code == 200 + try: + jsonResponse = json.loads(r.text) + except ValueError as err: + pytest.fail("Failed to parse debug endpoint response as JSON: %s" % err, pytrace=False) + if "paths" in jsonResponse["vhost"]: + pytest.fail("Expected paths to be stripped from debug endpoint response", pytrace=False) + assert jsonResponse["warning"] == "Virtual paths configuration for this hostname is too large and has been stripped from response." + + def test_debug_endpoint_is_disabled_per_container(docker_compose, nginxproxy): r = nginxproxy.get("http://disabled.debug.nginx-proxy.example/nginx-proxy-debug") assert r.status_code == 404 diff --git a/test/test_debug_endpoint/test_global.yml b/test/test_debug_endpoint/test_global.yml index 812ffed..7ec99ef 100644 --- a/test/test_debug_endpoint/test_global.yml +++ b/test/test_debug_endpoint/test_global.yml @@ -6,21 +6,42 @@ services: environment: DEBUG_ENDPOINT: "true" - debug_enabled1: + debug_enabled: image: web expose: - "81" environment: WEB_PORTS: 81 - VIRTUAL_HOST: enabled1.debug.nginx-proxy.example + VIRTUAL_HOST: enabled.debug.nginx-proxy.example - debug_enabled2: + debug_stripped: image: web expose: - "82" environment: WEB_PORTS: 82 - VIRTUAL_HOST: enabled2.debug.nginx-proxy.example + VIRTUAL_HOST_MULTIPORTS: |- + stripped.debug.nginx-proxy.example: + "/1": + "/2": + "/3": + "/4": + "/5": + "/6": + "/7": + "/8": + "/9": + "/10": + "/11": + "/12": + "/13": + "/14": + "/15": + "/16": + "/17": + "/18": + "/19": + "/20": debug_disabled: image: web diff --git a/test/test_debug_endpoint/test_per_container.py b/test/test_debug_endpoint/test_per_container.py index 8901f8b..16c680c 100644 --- a/test/test_debug_endpoint/test_per_container.py +++ b/test/test_debug_endpoint/test_per_container.py @@ -19,6 +19,6 @@ def test_debug_endpoint_response_contains_expected_values(docker_compose, nginxp try: jsonResponse = json.loads(r.text) except ValueError as err: - pytest.fail("Failed to parse JSON response: %s" % err, pytrace=False) + pytest.fail("Failed to parse debug endpoint response as JSON:: %s" % err, pytrace=False) assert jsonResponse["global"]["enable_debug_endpoint"] == "false" assert jsonResponse["vhost"]["enable_debug_endpoint"] == True