From 340f6b0c085017dc6ef78504773dac1c43263845 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 May 2024 14:02:17 +0000 Subject: [PATCH 01/42] build: bump nginxproxy/docker-gen from 0.12.1-debian to 0.13.0-debian Bumps nginxproxy/docker-gen from 0.12.1-debian to 0.13.0-debian. --- updated-dependencies: - dependency-name: nginxproxy/docker-gen dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Dockerfile.alpine | 2 +- Dockerfile.debian | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.alpine b/Dockerfile.alpine index d4d5c9d2d..e6eccdc9c 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -1,4 +1,4 @@ -FROM nginxproxy/docker-gen:0.12.1 AS docker-gen +FROM nginxproxy/docker-gen:0.13.0 AS docker-gen FROM nginxproxy/forego:0.18.1 AS forego diff --git a/Dockerfile.debian b/Dockerfile.debian index c91f5765f..485542d31 100644 --- a/Dockerfile.debian +++ b/Dockerfile.debian @@ -1,4 +1,4 @@ -FROM nginxproxy/docker-gen:0.12.1-debian AS docker-gen +FROM nginxproxy/docker-gen:0.13.0-debian AS docker-gen FROM nginxproxy/forego:0.18.1-debian AS forego From 87e5b58b779a7aa6344cc56ed5e65d5b5d22bb26 Mon Sep 17 00:00:00 2001 From: Gilles Filippini Date: Sun, 24 Mar 2024 12:31:13 +0100 Subject: [PATCH 02/42] feat: multiports support using yaml syntax (See nginx-proxy/nginx-proxy#1504) Using variable VIRTUAL_HOST_MULTIPORTS as a dictionnary: key: hostname value: dictionnary: key: path value: struct port dest When the dictionnary associated with a hostname is empty, default values apply: path = "/" port = default port dest = "" For each path entry, port and dest are optionnal and are assigned default values when missing. Example: VIRTUAL_HOST_MULTIPORTS: | host1.example.org: "/": port: 8000 "/somewhere": port: 9000 dest: "/elsewhere" host2.example.org: host3.example.org: "/inner/path": --- nginx.tmpl | 149 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 112 insertions(+), 37 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index fefb07f1a..966b9b022 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -128,7 +128,7 @@ # exposed ports:{{ range sortObjectsByKeysAsc $.container.Addresses "Port" }} {{ .Port }}/{{ .Proto }}{{ else }} (none){{ end }} {{- $default_port := when (eq (len $.container.Addresses) 1) (first $.container.Addresses).Port "80" }} # default port: {{ $default_port }} - {{- $port := when (eq $.port "legacy") (or $.container.Env.VIRTUAL_PORT $default_port) $.port }} + {{- $port := when (eq $.port "default") $default_port (when (eq $.port "legacy") (or $.container.Env.VIRTUAL_PORT $default_port) $.port) }} # using port: {{ $port }} {{- $addr_obj := where $.container.Addresses "Port" $port | first }} {{- if and $addr_obj $addr_obj.HostPort }} @@ -347,6 +347,7 @@ upstream {{ $vpath.upstream }} { * - "Containers": List of container's RuntimeContainer struct. * - "Upstream_name" * - "Has_virtual_paths": boolean + * - "Multiport_syntax": boolean * - "Path" * * The return values will be added to the dot dict with keys: @@ -373,6 +374,12 @@ upstream {{ $vpath.upstream }} { {{- $upstream = printf "%s-%s" $upstream $sum }} {{- $dest = or (first (groupByKeys $.Containers "Env.VIRTUAL_DEST")) "" }} {{- end }} + {{- if $.Multiport_syntax }} + {{- if (not (eq $.Path "/")) }} + {{- $sum := sha1 $.Path }} + {{- $upstream = printf "%s-%s" $upstream $sum }} + {{- end }} + {{- end }} {{- $_ := set $ "proto" $proto }} {{- $_ := set $ "network_tag" $network_tag }} {{- $_ := set $ "upstream" $upstream }} @@ -501,14 +508,107 @@ proxy_set_header Proxy ""; {{- end }} {{- /* Precompute some information about each vhost. */}} +{{- range $vhosts_yaml, $containers := groupBy $globals.containers "Env.VIRTUAL_HOST_YAML" }} + {{- range $hostname, $vhost := (fromYaml $vhosts_yaml) }} + {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} + {{- $paths := coalesce $vhost_data.paths (dict) }} + {{- if (empty $vhost) }} + {{ $vhost = dict "/" (dict) }} + {{- end }} + {{- range $path, $vpath := $vhost }} + {{- $dest := coalesce $vpath.dest "" }} + {{- $port := when (hasKey $vpath "port") (toString $vpath.port) "default" }} + {{- $path_data := when (hasKey $paths $path) (get $paths $path) (dict) }} + {{- $path_ports := when (hasKey $path_data "ports") (get $path_data "ports") (dict) }} + {{- $path_port_containers := when (hasKey $path_ports $port) (get $path_ports $port) (list) }} + {{- $path_port_containers = concat $path_port_containers $containers }} + {{- $_ := set $path_ports $port $path_port_containers }} + {{- $_ := set $path_data "ports" $path_ports }} + {{- if (not (hasKey $path_data "dest")) }} + {{- $_ := set $path_data "dest" $dest }} + {{- end }} + {{- $_ := set $paths $path $path_data }} + {{- 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 "has_virtual_paths" false }} + {{- $_ := set $vhost_data "multiport_syntax" true }} + {{- $_ := set $globals.vhosts $hostname $vhost_data }} + {{- end }} +{{- end }} + {{- range $hostname, $containers := groupByMulti $globals.containers "Env.VIRTUAL_HOST" "," }} {{- $hostname = trim $hostname }} {{- if not $hostname }} {{- /* Ignore containers with VIRTUAL_HOST set to the empty string. */}} {{- continue }} {{- end }} + {{- range $_, $containers_to_drop := groupBy $containers "Env.VIRTUAL_HOST_YAML" }} + {{- range $container := $containers_to_drop }} + {{- $containers = without $containers $container }} + {{- end }} + {{- end }} + {{- if (eq (len $containers) 0) }} + {{- continue }} + {{- end }} + {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} + + {{- $is_regexp := hasPrefix "~" $hostname }} + {{- $upstream_name := when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname }} + + {{- $has_virtual_paths := false }} + {{- $paths := coalesce $vhost_data.paths (dict) }} + {{- $tmp_paths := groupBy $containers "Env.VIRTUAL_PATH" }} + {{- $has_virtual_paths = gt (len $tmp_paths) 0}} + {{- if not $has_virtual_paths }} + {{- $tmp_paths = dict "/" $containers }} + {{- end }} + {{- range $path, $containers := $tmp_paths }} + {{- $path_data := when (hasKey $paths $path) (get $paths $path) (dict) }} + {{- $path_ports := when (hasKey $path_data "ports") (get $path_data "ports") (dict) }} + {{- $port := "legacy" }} + {{- $path_port_containers := when (hasKey $path_ports $port) (get $path_ports $port) (list) }} + {{- $path_port_containers = concat $path_port_containers $containers }} + + {{- $_ := set $path_ports $port $path_port_containers }} + {{- $_ := set $path_data "ports" $path_ports }} + {{- if (not (hasKey $path_data "dest")) }} + {{- $_ := set $path_data "dest" (or (first (groupByKeys $containers "Env.VIRTUAL_DEST")) "") }} + {{- end }} + {{- $_ := set $paths $path $path_data }} + {{- end }} + {{- $_ := set $vhost_data "paths" $paths }} + {{- $_ := set $vhost_data "upstream_name" $upstream_name }} + {{- $_ := set $vhost_data "has_virtual_paths" $has_virtual_paths }} + {{- $_ := set $vhost_data "multiport_syntax" false }} + {{- $_ := set $globals.vhosts $hostname $vhost_data }} +{{- end }} + +{{- range $hostname, $vhost_data := $globals.vhosts }} + {{- $vhost_containers := list }} + {{- range $path, $vpath_data := $vhost_data.paths }} + {{- $vpath_containers := list }} + {{- range $port, $vport_containers := $vpath_data.ports }} + {{ $vpath_containers = concat $vpath_containers $vport_containers }} + {{- end }} + + {{- $args := dict "Containers" $vpath_containers "Path" $path "Upstream_name" $vhost_data.upstream_name "Has_virtual_paths" $vhost_data.has_virtual_paths "Multiport_syntax" $vhost_data.multiport_syntax }} + {{- template "get_path_info" $args }} + {{- if $vhost_data.has_virtual_paths }} + {{- $_ := set $vpath_data "dest" $args.dest }} + {{- end }} + {{- $_ := set $vpath_data "proto" $args.proto }} + {{- $_ := set $vpath_data "network_tag" $args.network_tag }} + {{- $_ := set $vpath_data "upstream" $args.upstream }} + {{- $_ := set $vpath_data "loadbalance" $args.loadbalance }} + {{- $_ := set $vpath_data "keepalive" $args.keepalive }} + {{- $_ := set $vhost_data.paths $path $vpath_data }} + + {{ $vhost_containers = concat $vhost_containers $vpath_containers }} + {{- end }} - {{- $certName := first (groupByKeys $containers "Env.CERT_NAME") }} + {{- $certName := first (groupByKeys $vhost_containers "Env.CERT_NAME") }} {{- $vhostCert := closest (dir "/etc/nginx/certs") (printf "%s.crt" $hostname) }} {{- $vhostCert = trimSuffix ".crt" $vhostCert }} {{- $vhostCert = trimSuffix ".key" $vhostCert }} @@ -516,49 +616,23 @@ proxy_set_header Proxy ""; {{- $cert_ok := and (ne $cert "") (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert)) }} {{- $default := eq $globals.Env.DEFAULT_HOST $hostname }} - {{- $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) $globals.Env.HTTPS_METHOD "redirect" }} - {{- $http2_enabled := parseBool (or (first (keys (groupByLabel $containers "com.github.nginx-proxy.nginx-proxy.http2.enable"))) $globals.Env.ENABLE_HTTP2 "true")}} - {{- $http3_enabled := parseBool (or (first (keys (groupByLabel $containers "com.github.nginx-proxy.nginx-proxy.http3.enable"))) $globals.Env.ENABLE_HTTP3 "false")}} - - {{- $is_regexp := hasPrefix "~" $hostname }} - {{- $upstream_name := when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname }} + {{- $https_method := or (first (groupByKeys $vhost_containers "Env.HTTPS_METHOD")) $globals.Env.HTTPS_METHOD "redirect" }} + {{- $http2_enabled := parseBool (or (first (keys (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http2.enable"))) $globals.Env.ENABLE_HTTP2 "true")}} + {{- $http3_enabled := parseBool (or (first (keys (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http3.enable"))) $globals.Env.ENABLE_HTTP3 "false")}} {{- /* Get the SERVER_TOKENS defined by containers w/ the same vhost, falling back to "". */}} - {{- $server_tokens := trim (or (first (groupByKeys $containers "Env.SERVER_TOKENS")) "") }} + {{- $server_tokens := trim (or (first (groupByKeys $vhost_containers "Env.SERVER_TOKENS")) "") }} {{- /* Get the SSL_POLICY defined by containers w/ the same vhost, falling back to empty string (use default). */}} - {{- $ssl_policy := or (first (groupByKeys $containers "Env.SSL_POLICY")) "" }} + {{- $ssl_policy := or (first (groupByKeys $vhost_containers "Env.SSL_POLICY")) "" }} {{- /* Get the HSTS defined by containers w/ the same vhost, falling back to "max-age=31536000". */}} - {{- $hsts := or (first (groupByKeys $containers "Env.HSTS")) (or $globals.Env.HSTS "max-age=31536000") }} + {{- $hsts := or (first (groupByKeys $vhost_containers "Env.HSTS")) (or $globals.Env.HSTS "max-age=31536000") }} {{- /* Get the VIRTUAL_ROOT By containers w/ use fastcgi root */}} - {{- $vhost_root := or (first (groupByKeys $containers "Env.VIRTUAL_ROOT")) "/var/www/public" }} - + {{- $vhost_root := or (first (groupByKeys $vhost_containers "Env.VIRTUAL_ROOT")) "/var/www/public" }} - {{- $tmp_paths := groupBy $containers "Env.VIRTUAL_PATH" }} - {{- $has_virtual_paths := gt (len $tmp_paths) 0}} - {{- if not $has_virtual_paths }} - {{- $tmp_paths = dict "/" $containers }} - {{- end }} - - {{- $paths := dict }} - - {{- range $path, $containers := $tmp_paths }} - {{- $args := dict "Containers" $containers "Path" $path "Upstream_name" $upstream_name "Has_virtual_paths" $has_virtual_paths }} - {{- template "get_path_info" $args }} - {{- $_ := set $paths $path (dict - "ports" (dict "legacy" $containers) - "dest" $args.dest - "proto" $args.proto - "network_tag" $args.network_tag - "upstream" $args.upstream - "loadbalance" $args.loadbalance - "keepalive" $args.keepalive - ) }} - {{- end }} - - {{- $_ := set $globals.vhosts $hostname (dict + {{- $vhost_data = merge $vhost_data (dict "cert" $cert "cert_ok" $cert_ok "default" $default @@ -566,13 +640,14 @@ proxy_set_header Proxy ""; "https_method" $https_method "http2_enabled" $http2_enabled "http3_enabled" $http3_enabled - "paths" $paths "server_tokens" $server_tokens "ssl_policy" $ssl_policy "vhost_root" $vhost_root ) }} + {{- $_ := set $globals.vhosts $hostname $vhost_data }} {{- end }} + {{- /* * If needed, create a catch-all fallback server to send an error code to * clients that request something from an unknown vhost. From fc98f4c953015c021f269829aabb76e02b3a801c Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Thu, 2 May 2024 11:37:20 +0200 Subject: [PATCH 03/42] refactor: cleanup template --- nginx.tmpl | 59 ++++++++++++++++++++++-------------------------------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index 966b9b022..f554d0b5b 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -346,12 +346,9 @@ upstream {{ $vpath.upstream }} { * The provided dot dict is expected to have the following entries: * - "Containers": List of container's RuntimeContainer struct. * - "Upstream_name" - * - "Has_virtual_paths": boolean - * - "Multiport_syntax": boolean * - "Path" * * The return values will be added to the dot dict with keys: - * - "dest" * - "proto" * - "network_tag" * - "upstream" @@ -368,22 +365,13 @@ upstream {{ $vpath.upstream }} { {{- $keepalive := coalesce (first (keys (groupByLabel $.Containers "com.github.nginx-proxy.nginx-proxy.keepalive"))) "disabled" }} {{- $upstream := $.Upstream_name }} - {{- $dest := "" }} - {{- if $.Has_virtual_paths }} + {{- if (not (eq $.Path "/")) }} {{- $sum := sha1 $.Path }} {{- $upstream = printf "%s-%s" $upstream $sum }} - {{- $dest = or (first (groupByKeys $.Containers "Env.VIRTUAL_DEST")) "" }} - {{- end }} - {{- if $.Multiport_syntax }} - {{- if (not (eq $.Path "/")) }} - {{- $sum := sha1 $.Path }} - {{- $upstream = printf "%s-%s" $upstream $sum }} - {{- end }} {{- end }} {{- $_ := set $ "proto" $proto }} {{- $_ := set $ "network_tag" $network_tag }} {{- $_ := set $ "upstream" $upstream }} - {{- $_ := set $ "dest" $dest }} {{- $_ := set $ "loadbalance" $loadbalance }} {{- $_ := set $ "keepalive" $keepalive }} {{- end }} @@ -507,14 +495,16 @@ proxy_set_header X-Original-URI $request_uri; proxy_set_header Proxy ""; {{- end }} -{{- /* Precompute some information about each vhost. */}} +{{- /* Precompute some information about vhost that use VIRTUAL_HOST_YAML. */}} {{- range $vhosts_yaml, $containers := groupBy $globals.containers "Env.VIRTUAL_HOST_YAML" }} {{- range $hostname, $vhost := (fromYaml $vhosts_yaml) }} {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} {{- $paths := coalesce $vhost_data.paths (dict) }} + {{- if (empty $vhost) }} {{ $vhost = dict "/" (dict) }} {{- end }} + {{- range $path, $vpath := $vhost }} {{- $dest := coalesce $vpath.dest "" }} {{- $port := when (hasKey $vpath "port") (toString $vpath.port) "default" }} @@ -532,18 +522,19 @@ proxy_set_header Proxy ""; {{- $_ := 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 "has_virtual_paths" false }} - {{- $_ := set $vhost_data "multiport_syntax" true }} {{- $_ := set $globals.vhosts $hostname $vhost_data }} {{- end }} {{- end }} +{{- /* Precompute some information about vhost that use VIRTUAL_HOST. */}} {{- range $hostname, $containers := groupByMulti $globals.containers "Env.VIRTUAL_HOST" "," }} + {{- /* Ignore containers with VIRTUAL_HOST set to the empty string. */}} {{- $hostname = trim $hostname }} {{- if not $hostname }} - {{- /* Ignore containers with VIRTUAL_HOST set to the empty string. */}} {{- continue }} {{- end }} + + {{/* Drop containers with VIRTUAL_HOST_YAML set (VIRTUAL_HOST_YAML takes precedence). */}} {{- range $_, $containers_to_drop := groupBy $containers "Env.VIRTUAL_HOST_YAML" }} {{- range $container := $containers_to_drop }} {{- $containers = without $containers $container }} @@ -552,36 +543,32 @@ proxy_set_header Proxy ""; {{- if (eq (len $containers) 0) }} {{- continue }} {{- end }} - {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} - {{- $is_regexp := hasPrefix "~" $hostname }} - {{- $upstream_name := when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname }} - - {{- $has_virtual_paths := false }} + {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} {{- $paths := coalesce $vhost_data.paths (dict) }} + {{- $tmp_paths := groupBy $containers "Env.VIRTUAL_PATH" }} - {{- $has_virtual_paths = gt (len $tmp_paths) 0}} - {{- if not $has_virtual_paths }} + {{- if not (gt (len $tmp_paths) 0) }} {{- $tmp_paths = dict "/" $containers }} {{- end }} + {{- range $path, $containers := $tmp_paths }} + {{- $dest := or (first (groupByKeys $containers "Env.VIRTUAL_DEST")) "" }} + {{- $port := "legacy" }} {{- $path_data := when (hasKey $paths $path) (get $paths $path) (dict) }} {{- $path_ports := when (hasKey $path_data "ports") (get $path_data "ports") (dict) }} - {{- $port := "legacy" }} {{- $path_port_containers := when (hasKey $path_ports $port) (get $path_ports $port) (list) }} {{- $path_port_containers = concat $path_port_containers $containers }} - {{- $_ := set $path_ports $port $path_port_containers }} {{- $_ := set $path_data "ports" $path_ports }} {{- if (not (hasKey $path_data "dest")) }} - {{- $_ := set $path_data "dest" (or (first (groupByKeys $containers "Env.VIRTUAL_DEST")) "") }} + {{- $_ := set $path_data "dest" $dest }} {{- end }} {{- $_ := set $paths $path $path_data }} {{- end }} {{- $_ := set $vhost_data "paths" $paths }} - {{- $_ := set $vhost_data "upstream_name" $upstream_name }} - {{- $_ := set $vhost_data "has_virtual_paths" $has_virtual_paths }} - {{- $_ := set $vhost_data "multiport_syntax" false }} + {{- $is_regexp := hasPrefix "~" $hostname }} + {{- $_ := set $vhost_data "upstream_name" (when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname) }} {{- $_ := set $globals.vhosts $hostname $vhost_data }} {{- end }} @@ -593,11 +580,13 @@ proxy_set_header Proxy ""; {{ $vpath_containers = concat $vpath_containers $vport_containers }} {{- end }} - {{- $args := dict "Containers" $vpath_containers "Path" $path "Upstream_name" $vhost_data.upstream_name "Has_virtual_paths" $vhost_data.has_virtual_paths "Multiport_syntax" $vhost_data.multiport_syntax }} + {{- $args := (dict + "Containers" $vpath_containers + "Path" $path + "Upstream_name" $vhost_data.upstream_name + )}} + {{- template "get_path_info" $args }} - {{- if $vhost_data.has_virtual_paths }} - {{- $_ := set $vpath_data "dest" $args.dest }} - {{- end }} {{- $_ := set $vpath_data "proto" $args.proto }} {{- $_ := set $vpath_data "network_tag" $args.network_tag }} {{- $_ := set $vpath_data "upstream" $args.upstream }} @@ -883,4 +872,4 @@ server { } {{- end }} } -{{- end }} +{{- end }} \ No newline at end of file From 62d9c08474ba0490ed8664605be4eb6527543321 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Thu, 2 May 2024 13:00:55 +0200 Subject: [PATCH 04/42] fix: default values if port and dest are missing --- nginx.tmpl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nginx.tmpl b/nginx.tmpl index f554d0b5b..8c67ae99e 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -506,6 +506,9 @@ proxy_set_header Proxy ""; {{- end }} {{- range $path, $vpath := $vhost }} + {{- if (empty $vpath) }} + {{- $vpath = dict "dest" "" "port" "default" }} + {{- end }} {{- $dest := coalesce $vpath.dest "" }} {{- $port := when (hasKey $vpath "port") (toString $vpath.port) "default" }} {{- $path_data := when (hasKey $paths $path) (get $paths $path) (dict) }} From 62212186ebaf2f39a3a1deaef5b5c88af0633c5e Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Thu, 2 May 2024 09:51:48 +0200 Subject: [PATCH 05/42] test: re-organize test files --- test/test_composev2.py | 10 ---------- test/test_composev2.yml | 15 --------------- test/{ => test_logs}/test_log_format.py | 0 test/{ => test_logs}/test_log_format.yml | 0 test/{ => test_logs}/test_log_json.py | 0 test/{ => test_logs}/test_log_json.yml | 0 test/{ => test_logs}/test_log_json_format.py | 0 test/{ => test_logs}/test_log_json_format.yml | 0 ...TUAL_PORT-single-different-from-single-port.py | 0 ...UAL_PORT-single-different-from-single-port.yml | 0 .../test_VIRTUAL_PORT.py | 0 .../test_VIRTUAL_PORT.yml | 0 .../test_default-80.py | 0 .../test_default-80.yml | 0 .../test_single-port-not-80.py | 0 .../test_single-port-not-80.yml | 0 .../test_default-root-none.py | 0 .../test_default-root-none.yml | 0 18 files changed, 25 deletions(-) delete mode 100644 test/test_composev2.py delete mode 100644 test/test_composev2.yml rename test/{ => test_logs}/test_log_format.py (100%) rename test/{ => test_logs}/test_log_format.yml (100%) rename test/{ => test_logs}/test_log_json.py (100%) rename test/{ => test_logs}/test_log_json.yml (100%) rename test/{ => test_logs}/test_log_json_format.py (100%) rename test/{ => test_logs}/test_log_json_format.yml (100%) rename test/{test_multiple-ports => test_ports}/test_VIRTUAL_PORT-single-different-from-single-port.py (100%) rename test/{test_multiple-ports => test_ports}/test_VIRTUAL_PORT-single-different-from-single-port.yml (100%) rename test/{test_multiple-ports => test_ports}/test_VIRTUAL_PORT.py (100%) rename test/{test_multiple-ports => test_ports}/test_VIRTUAL_PORT.yml (100%) rename test/{test_multiple-ports => test_ports}/test_default-80.py (100%) rename test/{test_multiple-ports => test_ports}/test_default-80.yml (100%) rename test/{test_multiple-ports => test_ports}/test_single-port-not-80.py (100%) rename test/{test_multiple-ports => test_ports}/test_single-port-not-80.yml (100%) rename test/{ => test_virtual-path}/test_default-root-none.py (100%) rename test/{ => test_virtual-path}/test_default-root-none.yml (100%) diff --git a/test/test_composev2.py b/test/test_composev2.py deleted file mode 100644 index 695857efe..000000000 --- a/test/test_composev2.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest - -def test_unknown_virtual_host(docker_compose, nginxproxy): - r = nginxproxy.get("http://nginx-proxy/") - assert r.status_code == 503 - -def test_forwards_to_whoami(docker_compose, nginxproxy): - r = nginxproxy.get("http://web.nginx-proxy.example/port") - assert r.status_code == 200 - assert r.text == "answer from port 81\n" diff --git a/test/test_composev2.yml b/test/test_composev2.yml deleted file mode 100644 index 3c36022b5..000000000 --- a/test/test_composev2.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: "2" - -services: - nginx-proxy: - image: nginxproxy/nginx-proxy:test - volumes: - - /var/run/docker.sock:/tmp/docker.sock:ro - - web: - image: web - expose: - - "81" - environment: - WEB_PORTS: 81 - VIRTUAL_HOST: web.nginx-proxy.example diff --git a/test/test_log_format.py b/test/test_logs/test_log_format.py similarity index 100% rename from test/test_log_format.py rename to test/test_logs/test_log_format.py diff --git a/test/test_log_format.yml b/test/test_logs/test_log_format.yml similarity index 100% rename from test/test_log_format.yml rename to test/test_logs/test_log_format.yml diff --git a/test/test_log_json.py b/test/test_logs/test_log_json.py similarity index 100% rename from test/test_log_json.py rename to test/test_logs/test_log_json.py diff --git a/test/test_log_json.yml b/test/test_logs/test_log_json.yml similarity index 100% rename from test/test_log_json.yml rename to test/test_logs/test_log_json.yml diff --git a/test/test_log_json_format.py b/test/test_logs/test_log_json_format.py similarity index 100% rename from test/test_log_json_format.py rename to test/test_logs/test_log_json_format.py diff --git a/test/test_log_json_format.yml b/test/test_logs/test_log_json_format.yml similarity index 100% rename from test/test_log_json_format.yml rename to test/test_logs/test_log_json_format.yml diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.py b/test/test_ports/test_VIRTUAL_PORT-single-different-from-single-port.py similarity index 100% rename from test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.py rename to test/test_ports/test_VIRTUAL_PORT-single-different-from-single-port.py diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.yml b/test/test_ports/test_VIRTUAL_PORT-single-different-from-single-port.yml similarity index 100% rename from test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.yml rename to test/test_ports/test_VIRTUAL_PORT-single-different-from-single-port.yml diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT.py b/test/test_ports/test_VIRTUAL_PORT.py similarity index 100% rename from test/test_multiple-ports/test_VIRTUAL_PORT.py rename to test/test_ports/test_VIRTUAL_PORT.py diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT.yml b/test/test_ports/test_VIRTUAL_PORT.yml similarity index 100% rename from test/test_multiple-ports/test_VIRTUAL_PORT.yml rename to test/test_ports/test_VIRTUAL_PORT.yml diff --git a/test/test_multiple-ports/test_default-80.py b/test/test_ports/test_default-80.py similarity index 100% rename from test/test_multiple-ports/test_default-80.py rename to test/test_ports/test_default-80.py diff --git a/test/test_multiple-ports/test_default-80.yml b/test/test_ports/test_default-80.yml similarity index 100% rename from test/test_multiple-ports/test_default-80.yml rename to test/test_ports/test_default-80.yml diff --git a/test/test_multiple-ports/test_single-port-not-80.py b/test/test_ports/test_single-port-not-80.py similarity index 100% rename from test/test_multiple-ports/test_single-port-not-80.py rename to test/test_ports/test_single-port-not-80.py diff --git a/test/test_multiple-ports/test_single-port-not-80.yml b/test/test_ports/test_single-port-not-80.yml similarity index 100% rename from test/test_multiple-ports/test_single-port-not-80.yml rename to test/test_ports/test_single-port-not-80.yml diff --git a/test/test_default-root-none.py b/test/test_virtual-path/test_default-root-none.py similarity index 100% rename from test/test_default-root-none.py rename to test/test_virtual-path/test_default-root-none.py diff --git a/test/test_default-root-none.yml b/test/test_virtual-path/test_default-root-none.yml similarity index 100% rename from test/test_default-root-none.yml rename to test/test_virtual-path/test_default-root-none.yml From 216eae9f70559637b392d3b223a25f9039fb3929 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Thu, 2 May 2024 10:08:51 +0200 Subject: [PATCH 06/42] test: multiports base test --- test/test_multiports/test_multiports-base.py | 39 ++++++++++++ test/test_multiports/test_multiports-base.yml | 61 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 test/test_multiports/test_multiports-base.py create mode 100644 test/test_multiports/test_multiports-base.yml diff --git a/test/test_multiports/test_multiports-base.py b/test/test_multiports/test_multiports-base.py new file mode 100644 index 000000000..7f1ef0fb0 --- /dev/null +++ b/test/test_multiports/test_multiports-base.py @@ -0,0 +1,39 @@ +import pytest + + +def test_virtual_host_is_dropped_when_using_multiports(docker_compose, nginxproxy): + r = nginxproxy.get("http://notskipped.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 81\n" in r.text + r = nginxproxy.get("http://skipped.nginx-proxy.tld/") + assert r.status_code == 503 + + +def test_answer_is_served_from_port_80_by_default(docker_compose, nginxproxy): + r = nginxproxy.get("http://port80.a.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + r = nginxproxy.get("http://port80.b.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + r = nginxproxy.get("http://port80.c.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + + +def test_answer_is_served_from_chosen_ports(docker_compose, nginxproxy): + r = nginxproxy.get("http://port8080.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 8080\n" in r.text + r = nginxproxy.get("http://port9000.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 9000\n" in r.text + + +def test_answer_is_served_from_chosen_ports_and_dest(docker_compose, nginxproxy): + r = nginxproxy.get("http://virtualpaths.nginx-proxy.tld/rootdest/port") + assert r.status_code == 200 + assert "answer from port 10001\n" in r.text + r = nginxproxy.get("http://virtualpaths.nginx-proxy.tld/customdest") + assert r.status_code == 200 + assert "answer from port 10002\n" in r.text diff --git a/test/test_multiports/test_multiports-base.yml b/test/test_multiports/test_multiports-base.yml new file mode 100644 index 000000000..bfe4a01ac --- /dev/null +++ b/test/test_multiports/test_multiports-base.yml @@ -0,0 +1,61 @@ +version: "2" + +services: + skipvirtualhost: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: skipped.nginx-proxy.tld + VIRTUAL_HOST_YAML: |- + notskipped.nginx-proxy.tld: + + defaultport: + image: web + expose: + - "80" + - "8080" + environment: + WEB_PORTS: "80 8080" + VIRTUAL_HOST_YAML: |- + port80.a.nginx-proxy.tld: + port80.b.nginx-proxy.tld: + port80.c.nginx-proxy.tld: + "/": + + multiports: + image: web + expose: + - "8080" + - "9000" + environment: + WEB_PORTS: "8080 9000" + VIRTUAL_HOST_YAML: |- + port8080.nginx-proxy.tld: + "/": + port: 8080 + port9000.nginx-proxy.tld: + "/": + port: 9000 + + virtualpath: + image: web + expose: + - "10001" + - "10002" + environment: + WEB_PORTS: "10001 10002" + VIRTUAL_HOST_YAML: |- + virtualpaths.nginx-proxy.tld: + "/rootdest": + port: 10001 + dest: "/" + "/customdest": + port: 10002 + dest: "/port" + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro From 47e2838e615544adda3313ef576db4bacd94d0ac Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Fri, 3 May 2024 00:12:14 +0200 Subject: [PATCH 07/42] refactor: rename VIRTUAL_HOST_YAML -> VIRTUAL_HOST_MULTIPORTS --- nginx.tmpl | 13 ++++++++----- test/test_multiports/test_multiports-base.yml | 8 ++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index 8c67ae99e..615526cf3 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -495,8 +495,8 @@ proxy_set_header X-Original-URI $request_uri; proxy_set_header Proxy ""; {{- end }} -{{- /* Precompute some information about vhost that use VIRTUAL_HOST_YAML. */}} -{{- range $vhosts_yaml, $containers := groupBy $globals.containers "Env.VIRTUAL_HOST_YAML" }} +{{- /* Precompute and store some information about vhost that use VIRTUAL_HOST_MULTIPORTS. */}} +{{- range $vhosts_yaml, $containers := groupBy $globals.containers "Env.VIRTUAL_HOST_MULTIPORTS" }} {{- range $hostname, $vhost := (fromYaml $vhosts_yaml) }} {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} {{- $paths := coalesce $vhost_data.paths (dict) }} @@ -529,7 +529,7 @@ proxy_set_header Proxy ""; {{- end }} {{- end }} -{{- /* Precompute some information about vhost that use VIRTUAL_HOST. */}} +{{- /* Precompute and store some information about vhost that use VIRTUAL_HOST. */}} {{- range $hostname, $containers := groupByMulti $globals.containers "Env.VIRTUAL_HOST" "," }} {{- /* Ignore containers with VIRTUAL_HOST set to the empty string. */}} {{- $hostname = trim $hostname }} @@ -537,8 +537,10 @@ proxy_set_header Proxy ""; {{- continue }} {{- end }} - {{/* Drop containers with VIRTUAL_HOST_YAML set (VIRTUAL_HOST_YAML takes precedence). */}} - {{- range $_, $containers_to_drop := groupBy $containers "Env.VIRTUAL_HOST_YAML" }} + {{- /* Drop containers with both VIRTUAL_HOST and VIRTUAL_HOST_MULTIPORTS set + * (VIRTUAL_HOST_MULTIPORTS takes precedence thanks to the previous loop). + */}} + {{- range $_, $containers_to_drop := groupBy $containers "Env.VIRTUAL_HOST_MULTIPORTS" }} {{- range $container := $containers_to_drop }} {{- $containers = without $containers $container }} {{- end }} @@ -575,6 +577,7 @@ proxy_set_header Proxy ""; {{- $_ := set $globals.vhosts $hostname $vhost_data }} {{- end }} +{{- /* Loop over $globals.vhosts and update it with the remaining informations about each vhost. */}} {{- range $hostname, $vhost_data := $globals.vhosts }} {{- $vhost_containers := list }} {{- range $path, $vpath_data := $vhost_data.paths }} diff --git a/test/test_multiports/test_multiports-base.yml b/test/test_multiports/test_multiports-base.yml index bfe4a01ac..8bd58030c 100644 --- a/test/test_multiports/test_multiports-base.yml +++ b/test/test_multiports/test_multiports-base.yml @@ -8,7 +8,7 @@ services: environment: WEB_PORTS: "81" VIRTUAL_HOST: skipped.nginx-proxy.tld - VIRTUAL_HOST_YAML: |- + VIRTUAL_HOST_MULTIPORTS: |- notskipped.nginx-proxy.tld: defaultport: @@ -18,7 +18,7 @@ services: - "8080" environment: WEB_PORTS: "80 8080" - VIRTUAL_HOST_YAML: |- + VIRTUAL_HOST_MULTIPORTS: |- port80.a.nginx-proxy.tld: port80.b.nginx-proxy.tld: port80.c.nginx-proxy.tld: @@ -31,7 +31,7 @@ services: - "9000" environment: WEB_PORTS: "8080 9000" - VIRTUAL_HOST_YAML: |- + VIRTUAL_HOST_MULTIPORTS: |- port8080.nginx-proxy.tld: "/": port: 8080 @@ -46,7 +46,7 @@ services: - "10002" environment: WEB_PORTS: "10001 10002" - VIRTUAL_HOST_YAML: |- + VIRTUAL_HOST_MULTIPORTS: |- virtualpaths.nginx-proxy.tld: "/rootdest": port: 10001 From 8359aa2089eb3a0f1c5a94d24f904d815a77484b Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Fri, 3 May 2024 00:52:33 +0200 Subject: [PATCH 08/42] docs: documentation for multiports support --- docs/README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/README.md b/docs/README.md index 6e29e03ff..77b48745f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -53,6 +53,62 @@ For each host defined into `VIRTUAL_HOST`, the associated virtual port is retrie 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 +### Multiple ports + +If your container expose more than one service on different ports and those services need to be proxied, you'll need to use the `VIRTUAL_HOST_MULTIPORTS` environment variable. This variable takes virtual host, path, port and dest definition in YAML (or JSON) form, and completely override the `VIRTUAL_HOST`, `VIRTUAL_PORT`, `VIRTUAL_PATH` and `VIRTUAL_DEST` environment variables on this container. + +The expected format is the following: + +```yaml +hostname: + path: + port: int + dest: string +``` + +For each hostname entry, `path`, `port` and `dest` are optionnal and are assigned default values when missing: + +- `path` = "/" +- `port` = default port +- `dest` = "" + +Docker compose example with an hypotetical container running services on port 80, 8000 and 9000: + +```yaml +services: + multiport-container: + image: somerepo/somecontainer + container_name: multiport-container + environment: + VIRTUAL_HOST_MULTIPORTS: |- + service1.example.org: + service2.example.org: + "/": + port: 8000 + "/foo": + port: 9000 + dest: "/" + service3.example.org: + "/bar": + dest: "/somewhere" +``` + +Command line equivalent using JSON formatting: + +```console +docker run --detach \ + --name multiport-container \ + --env 'VIRTUAL_HOST_MULTIPORTS={"service1.example.org": {}, "service2.example.org": {"/": {"port": 8000}, "/somewhere": {"port": 9000, "dest": "/elsewhere"}}, "service3.example.org": {"/foo": {"dest": "/bar"}}}' + somerepo/somecontainer +``` + +This would result in the following proxy config: + +- `host1.example.org` -> `multiport-container:80` +- `host2.example.org` -> `multiport-container:8000` +- `host2.example.org/foo` -> `multiport-container:9000` +- `host3.example.org/bar` -> `multiport-container:80/somewhere` + ⬆️ [back to table of contents](#table-of-contents) ## Path-based Routing From 0baff189bcdffbb4bf82b9d0ed940f2eda7642d2 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Fri, 3 May 2024 11:21:45 +0200 Subject: [PATCH 09/42] refactor: get rid of get_path_info template --- nginx.tmpl | 69 +++++++++++++++--------------------------------------- 1 file changed, 19 insertions(+), 50 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index 615526cf3..63bc67eaa 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -338,44 +338,6 @@ upstream {{ $vpath.upstream }} { } {{- end }} -{{- /* - * Template used as a function to collect virtual path properties from - * the given containers. These properties are "returned" by storing their - * values into the provided dot dict. - * - * The provided dot dict is expected to have the following entries: - * - "Containers": List of container's RuntimeContainer struct. - * - "Upstream_name" - * - "Path" - * - * The return values will be added to the dot dict with keys: - * - "proto" - * - "network_tag" - * - "upstream" - * - "loadbalance" - * - "keepalive" - */}} -{{- define "get_path_info" }} - {{- /* Get the VIRTUAL_PROTO defined by containers w/ the same vhost-vpath, falling back to "http". */}} - {{- $proto := trim (or (first (groupByKeys $.Containers "Env.VIRTUAL_PROTO")) "http") }} - {{- /* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external". */}} - {{- $network_tag := or (first (groupByKeys $.Containers "Env.NETWORK_ACCESS")) "external" }} - - {{- $loadbalance := first (keys (groupByLabel $.Containers "com.github.nginx-proxy.nginx-proxy.loadbalance")) }} - {{- $keepalive := coalesce (first (keys (groupByLabel $.Containers "com.github.nginx-proxy.nginx-proxy.keepalive"))) "disabled" }} - - {{- $upstream := $.Upstream_name }} - {{- if (not (eq $.Path "/")) }} - {{- $sum := sha1 $.Path }} - {{- $upstream = printf "%s-%s" $upstream $sum }} - {{- end }} - {{- $_ := set $ "proto" $proto }} - {{- $_ := set $ "network_tag" $network_tag }} - {{- $_ := set $ "upstream" $upstream }} - {{- $_ := set $ "loadbalance" $loadbalance }} - {{- $_ := set $ "keepalive" $keepalive }} -{{- 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 { @@ -586,18 +548,25 @@ proxy_set_header Proxy ""; {{ $vpath_containers = concat $vpath_containers $vport_containers }} {{- end }} - {{- $args := (dict - "Containers" $vpath_containers - "Path" $path - "Upstream_name" $vhost_data.upstream_name - )}} - - {{- template "get_path_info" $args }} - {{- $_ := set $vpath_data "proto" $args.proto }} - {{- $_ := set $vpath_data "network_tag" $args.network_tag }} - {{- $_ := set $vpath_data "upstream" $args.upstream }} - {{- $_ := set $vpath_data "loadbalance" $args.loadbalance }} - {{- $_ := set $vpath_data "keepalive" $args.keepalive }} + {{- /* Get the VIRTUAL_PROTO defined by containers w/ the same vhost-vpath, falling back to "http". */}} + {{- $proto := trim (or (first (groupByKeys $vpath_containers "Env.VIRTUAL_PROTO")) "http") }} + {{- /* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external". */}} + {{- $network_tag := or (first (groupByKeys $vpath_containers "Env.NETWORK_ACCESS")) "external" }} + + {{- $loadbalance := first (keys (groupByLabel $vpath_containers "com.github.nginx-proxy.nginx-proxy.loadbalance")) }} + {{- $keepalive := coalesce (first (keys (groupByLabel $vpath_containers "com.github.nginx-proxy.nginx-proxy.keepalive"))) "disabled" }} + + {{- $upstream := $vhost_data.upstream_name }} + {{- if (not (eq $path "/")) }} + {{- $sum := sha1 $path }} + {{- $upstream = printf "%s-%s" $upstream $sum }} + {{- end }} + + {{- $_ := set $vpath_data "proto" $proto }} + {{- $_ := set $vpath_data "network_tag" $network_tag }} + {{- $_ := set $vpath_data "upstream" $upstream }} + {{- $_ := set $vpath_data "loadbalance" $loadbalance }} + {{- $_ := set $vpath_data "keepalive" $keepalive }} {{- $_ := set $vhost_data.paths $path $vpath_data }} {{ $vhost_containers = concat $vhost_containers $vpath_containers }} From 53e9a03ac9d822d9664f14575a3ff69b625978f0 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Fri, 3 May 2024 22:21:21 +0200 Subject: [PATCH 10/42] feat: print warning on unparsable VIRTUAL_HOST_MULTIPORTS --- nginx.tmpl | 14 +++++- .../test_multiports-invalid-syntax.py | 18 ++++++++ .../test_multiports-invalid-syntax.yml | 44 +++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 test/test_multiports/test_multiports-invalid-syntax.py create mode 100644 test/test_multiports/test_multiports-invalid-syntax.yml diff --git a/nginx.tmpl b/nginx.tmpl index 63bc67eaa..a5be0b0bf 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -459,7 +459,19 @@ proxy_set_header Proxy ""; {{- /* Precompute and store some information about vhost that use VIRTUAL_HOST_MULTIPORTS. */}} {{- range $vhosts_yaml, $containers := groupBy $globals.containers "Env.VIRTUAL_HOST_MULTIPORTS" }} - {{- range $hostname, $vhost := (fromYaml $vhosts_yaml) }} + {{- /* Print a warning in the config if VIRTUAL_HOST_MULTIPORTS can't be parsed. */}} + {{- $parsedVhosts := fromYaml $vhosts_yaml }} + {{- if (empty $parsedVhosts) }} + {{- $containerNames := list }} + {{- range $container := $containers }} + {{- $containerNames = append $containerNames $container.Name }} + {{- end }} +# /!\ WARNING: the VIRTUAL_HOST_MULTIPORTS environment variable used for {{ len $containerNames | plural "this container" "those containers" }} is not a valid YAML string: +# {{ $containerNames | join ", " }} + {{- continue }} + {{- end }} + + {{- range $hostname, $vhost := $parsedVhosts }} {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} {{- $paths := coalesce $vhost_data.paths (dict) }} diff --git a/test/test_multiports/test_multiports-invalid-syntax.py b/test/test_multiports/test_multiports-invalid-syntax.py new file mode 100644 index 000000000..ed1c77338 --- /dev/null +++ b/test/test_multiports/test_multiports-invalid-syntax.py @@ -0,0 +1,18 @@ +import pytest +import re + + +def test_virtual_hosts_with_syntax_error_should_not_be_reachable(docker_compose, nginxproxy): + r = nginxproxy.get("http://test1.nginx-proxy.tld") + assert r.status_code == 503 + r = nginxproxy.get("http://test2.nginx-proxy.tld") + assert r.status_code == 503 + + +def test_config_should_have_multiports_warning_comments(docker_compose, nginxproxy): + conf = nginxproxy.get_conf().decode('ASCII') + matches = re.findall(r"the VIRTUAL_HOST_MULTIPORTS environment variable used for this container is not a valid YAML string", conf) + assert len(matches) == 3 + assert "# invalidsyntax" in conf + assert "# hostnamerepeat" in conf + assert "# pathrepeat" in conf diff --git a/test/test_multiports/test_multiports-invalid-syntax.yml b/test/test_multiports/test_multiports-invalid-syntax.yml new file mode 100644 index 000000000..9f4022061 --- /dev/null +++ b/test/test_multiports/test_multiports-invalid-syntax.yml @@ -0,0 +1,44 @@ +version: "2" + +services: + invalidsyntax: + image: web + container_name: invalidsyntax + expose: + - "80" + environment: + WEB_PORTS: "80" + VIRTUAL_HOST_MULTIPORTS: |- + test1.nginx-proxy.tld + test2.nginx-proxy.tld: + + hostnamerepeat: + image: web + container_name: hostnamerepeat + expose: + - "80" + environment: + WEB_PORTS: "80" + VIRTUAL_HOST_MULTIPORTS: |- + test1.nginx-proxy.tld: + test1.nginx-proxy.tld: + + pathrepeat: + image: web + container_name: pathrepeat + expose: + - "8080" + - "9000" + environment: + WEB_PORTS: "8080 9000" + VIRTUAL_HOST_MULTIPORTS: |- + test1.nginx-proxy.tld: + "/": + port: 8080 + "/": + port: 9000 + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro From d80ca7ec36b26b6d3059601d2611ba2d9361074e Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sat, 4 May 2024 13:30:21 +0200 Subject: [PATCH 11/42] test: json syntax for multiports variable --- ...s-base.py => test_multiports-base-json.py} | 0 .../test_multiports-base-json.yml | 77 +++++++++++++++++++ .../test_multiports-base-yaml.py | 39 ++++++++++ ...base.yml => test_multiports-base-yaml.yml} | 0 4 files changed, 116 insertions(+) rename test/test_multiports/{test_multiports-base.py => test_multiports-base-json.py} (100%) create mode 100644 test/test_multiports/test_multiports-base-json.yml create mode 100644 test/test_multiports/test_multiports-base-yaml.py rename test/test_multiports/{test_multiports-base.yml => test_multiports-base-yaml.yml} (100%) diff --git a/test/test_multiports/test_multiports-base.py b/test/test_multiports/test_multiports-base-json.py similarity index 100% rename from test/test_multiports/test_multiports-base.py rename to test/test_multiports/test_multiports-base-json.py diff --git a/test/test_multiports/test_multiports-base-json.yml b/test/test_multiports/test_multiports-base-json.yml new file mode 100644 index 000000000..fc0d0fa81 --- /dev/null +++ b/test/test_multiports/test_multiports-base-json.yml @@ -0,0 +1,77 @@ +version: "2" + +services: + skipvirtualhost: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: skipped.nginx-proxy.tld + VIRTUAL_HOST_MULTIPORTS: |- + { + "notskipped.nginx-proxy.tld": {} + } + + defaultport: + image: web + expose: + - "80" + - "8080" + environment: + WEB_PORTS: "80 8080" + VIRTUAL_HOST_MULTIPORTS: |- + { + "port80.a.nginx-proxy.tld": {}, + "port80.b.nginx-proxy.tld": {}, + "port80.c.nginx-proxy.tld": { + "/": {} + } + } + + multiports: + image: web + expose: + - "8080" + - "9000" + environment: + WEB_PORTS: "8080 9000" + VIRTUAL_HOST_MULTIPORTS: |- + { + "port8080.nginx-proxy.tld": { + "/": { + "port": 8080 + } + }, + "port9000.nginx-proxy.tld": { + "/": { + "port": 9000 + } + } + } + + virtualpath: + image: web + expose: + - "10001" + - "10002" + environment: + WEB_PORTS: "10001 10002" + VIRTUAL_HOST_MULTIPORTS: |- + { + "virtualpaths.nginx-proxy.tld": { + "/rootdest": { + "port": 10001, + "dest": "/" + }, + "/customdest": { + "port": 10002, + "dest": "/port" + } + } + } + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro diff --git a/test/test_multiports/test_multiports-base-yaml.py b/test/test_multiports/test_multiports-base-yaml.py new file mode 100644 index 000000000..7f1ef0fb0 --- /dev/null +++ b/test/test_multiports/test_multiports-base-yaml.py @@ -0,0 +1,39 @@ +import pytest + + +def test_virtual_host_is_dropped_when_using_multiports(docker_compose, nginxproxy): + r = nginxproxy.get("http://notskipped.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 81\n" in r.text + r = nginxproxy.get("http://skipped.nginx-proxy.tld/") + assert r.status_code == 503 + + +def test_answer_is_served_from_port_80_by_default(docker_compose, nginxproxy): + r = nginxproxy.get("http://port80.a.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + r = nginxproxy.get("http://port80.b.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + r = nginxproxy.get("http://port80.c.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + + +def test_answer_is_served_from_chosen_ports(docker_compose, nginxproxy): + r = nginxproxy.get("http://port8080.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 8080\n" in r.text + r = nginxproxy.get("http://port9000.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 9000\n" in r.text + + +def test_answer_is_served_from_chosen_ports_and_dest(docker_compose, nginxproxy): + r = nginxproxy.get("http://virtualpaths.nginx-proxy.tld/rootdest/port") + assert r.status_code == 200 + assert "answer from port 10001\n" in r.text + r = nginxproxy.get("http://virtualpaths.nginx-proxy.tld/customdest") + assert r.status_code == 200 + assert "answer from port 10002\n" in r.text diff --git a/test/test_multiports/test_multiports-base.yml b/test/test_multiports/test_multiports-base-yaml.yml similarity index 100% rename from test/test_multiports/test_multiports-base.yml rename to test/test_multiports/test_multiports-base-yaml.yml From 8b91f09a9b4dba8143f6483cfea28f4ac6e95fbb Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sat, 4 May 2024 13:40:05 +0200 Subject: [PATCH 12/42] docs: use examples closer to real life scenarios --- docs/README.md | 75 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/docs/README.md b/docs/README.md index 77b48745f..b6ad130e5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -57,6 +57,8 @@ For each host defined into `VIRTUAL_HOST`, the associated virtual port is retrie If your container expose more than one service on different ports and those services need to be proxied, you'll need to use the `VIRTUAL_HOST_MULTIPORTS` environment variable. This variable takes virtual host, path, port and dest definition in YAML (or JSON) form, and completely override the `VIRTUAL_HOST`, `VIRTUAL_PORT`, `VIRTUAL_PATH` and `VIRTUAL_DEST` environment variables on this container. +The YAML syntax should be easier to write on Docker compose files, while the JSON syntax can be used for CLI invocation. + The expected format is the following: ```yaml @@ -72,7 +74,9 @@ For each hostname entry, `path`, `port` and `dest` are optionnal and are assigne - `port` = default port - `dest` = "" -Docker compose example with an hypotetical container running services on port 80, 8000 and 9000: +The following examples use an hypotetical container running services on port 80, 8000 and 9000: + +#### Multiple ports routed to different hostnames ```yaml services: @@ -81,33 +85,72 @@ services: container_name: multiport-container environment: VIRTUAL_HOST_MULTIPORTS: |- + www.example.org: service1.example.org: + "/": + port: 8000 service2.example.org: "/": + port: 9000 + +# There is no path dict specified for www.example.org, so it get the default values: +# www.example.org: +# "/": +# port: 80 (default port) +# dest: "" + +# JSON equivalent: +# VIRTUAL_HOST_MULTIPORTS: |- +# { +# "www.example.org": {}, +# "service1.example.org": { "/": { "port": 8000, "dest": "" } }, +# "service2.example.org": { "/": { "port": 9000, "dest": "" } } +# } +``` + +This would result in the following proxy config: + +- `www.example.org` -> `multiport-container:80` +- `service1.example.org` -> `multiport-container:8000` +- `service2.example.org` -> `multiport-container:9000` + +#### Multiple ports routed to same hostname and different paths + +```yaml +services: + multiport-container: + image: somerepo/somecontainer + container_name: multiport-container + environment: + VIRTUAL_HOST_MULTIPORTS: |- + www.example.org: + "/": + "/service1": port: 8000 - "/foo": + dest: "/" + "/service2": port: 9000 dest: "/" - service3.example.org: - "/bar": - dest: "/somewhere" -``` -Command line equivalent using JSON formatting: +# port and dest are not specified on the / path, so this path is routed +# to the default port with the default dest value (empty string) -```console -docker run --detach \ - --name multiport-container \ - --env 'VIRTUAL_HOST_MULTIPORTS={"service1.example.org": {}, "service2.example.org": {"/": {"port": 8000}, "/somewhere": {"port": 9000, "dest": "/elsewhere"}}, "service3.example.org": {"/foo": {"dest": "/bar"}}}' - somerepo/somecontainer +# JSON equivalent: +# VIRTUAL_HOST_MULTIPORTS: |- +# { +# "www.example.org": { +# "/": {}, +# "/service1": { "port": 8000, "dest": "/" }, +# "/service2": { "port": 9000, "dest": "/" } +# } +# } ``` This would result in the following proxy config: -- `host1.example.org` -> `multiport-container:80` -- `host2.example.org` -> `multiport-container:8000` -- `host2.example.org/foo` -> `multiport-container:9000` -- `host3.example.org/bar` -> `multiport-container:80/somewhere` +- `www.example.org` -> `multiport-container:80` +- `www.example.org/service1` -> `multiport-container:8000` +- `www.example.org/service2` -> `multiport-container:9000` ⬆️ [back to table of contents](#table-of-contents) From 1b97b111734c019a0688ead0b4475c9f9aaf8c7d Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sun, 5 May 2024 11:19:56 +0200 Subject: [PATCH 13/42] test: use python:3-alpine as base for the web image --- test/requirements/web/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/requirements/web/Dockerfile b/test/requirements/web/Dockerfile index 923ed79c4..da755534c 100644 --- a/test/requirements/web/Dockerfile +++ b/test/requirements/web/Dockerfile @@ -1,6 +1,7 @@ # Docker Image running one (or multiple) webservers listening on all given ports from WEB_PORTS environment variable -FROM python:3 +FROM python:3-alpine +RUN apk add --no-cache bash COPY ./webserver.py / COPY ./entrypoint.sh / WORKDIR /opt From 9a76577ebc301e8b9b1a2f300ee8cb29dcd42fc5 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sun, 5 May 2024 11:41:04 +0200 Subject: [PATCH 14/42] style: shellcheck linting --- test/requirements/web/entrypoint.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/requirements/web/entrypoint.sh b/test/requirements/web/entrypoint.sh index 3015c115d..be9f9c365 100644 --- a/test/requirements/web/entrypoint.sh +++ b/test/requirements/web/entrypoint.sh @@ -5,11 +5,11 @@ trap '[ ${#PIDS[@]} -gt 0 ] && kill -TERM ${PIDS[@]}' TERM declare -a PIDS for port in $WEB_PORTS; do - echo starting a web server listening on port $port; - /webserver.py $port & + echo starting a web server listening on port "$port"; + /webserver.py "$port" & PIDS+=($!) done -wait ${PIDS[@]} +wait "${PIDS[@]}" trap - TERM -wait ${PIDS[@]} +wait "${PIDS[@]}" From be7c4c8c850fe0e9e3eb863b0d2bf99c819881f4 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sun, 5 May 2024 16:31:03 +0200 Subject: [PATCH 15/42] fix: do not discard containers without VIRTUAL_PATH For containers grouped by identical VIRTUAL_HOST, those with no VIRTUAL_PATH variable were silently discarded when at least one container with VIRTUAL_PATH existed. --- nginx.tmpl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index a5be0b0bf..2b73158b0 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -526,10 +526,7 @@ proxy_set_header Proxy ""; {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} {{- $paths := coalesce $vhost_data.paths (dict) }} - {{- $tmp_paths := groupBy $containers "Env.VIRTUAL_PATH" }} - {{- if not (gt (len $tmp_paths) 0) }} - {{- $tmp_paths = dict "/" $containers }} - {{- end }} + {{- $tmp_paths := groupByWithDefault $containers "Env.VIRTUAL_PATH" "/" }} {{- range $path, $containers := $tmp_paths }} {{- $dest := or (first (groupByKeys $containers "Env.VIRTUAL_DEST")) "" }} From 1bf7eff04f03db19d378d94120c45612af403001 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sat, 4 May 2024 14:48:08 +0200 Subject: [PATCH 16/42] test: multiport merge with legacy variable --- test/test_multiports/test_multiports-merge.py | 14 +++++++ .../test_multiports/test_multiports-merge.yml | 41 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 test/test_multiports/test_multiports-merge.py create mode 100644 test/test_multiports/test_multiports-merge.yml diff --git a/test/test_multiports/test_multiports-merge.py b/test/test_multiports/test_multiports-merge.py new file mode 100644 index 000000000..f5aa69782 --- /dev/null +++ b/test/test_multiports/test_multiports-merge.py @@ -0,0 +1,14 @@ +import backoff +import pytest + + +def test_multiports_and_legacy_configs_should_be_merged(docker_compose, nginxproxy): + @backoff.on_predicate(backoff.constant, lambda r: r == False, interval=.5, max_tries=20, jitter=None) + def answer_contains(answer, url): + return answer in nginxproxy.get(url).text + + assert answer_contains("80", "http://merged.nginx-proxy.tld/port") + assert answer_contains("81", "http://merged.nginx-proxy.tld/port") + + assert answer_contains("9090", "http://merged.nginx-proxy.tld/foo/port") + assert answer_contains("9191", "http://merged.nginx-proxy.tld/foo/port") diff --git a/test/test_multiports/test_multiports-merge.yml b/test/test_multiports/test_multiports-merge.yml new file mode 100644 index 000000000..5c5cd8bd0 --- /dev/null +++ b/test/test_multiports/test_multiports-merge.yml @@ -0,0 +1,41 @@ +version: "2" + +services: + merged-singleport: + image: web + expose: + - "80" + environment: + WEB_PORTS: "80" + VIRTUAL_HOST: merged.nginx-proxy.tld + + merged-singleport-virtual-path: + image: web + expose: + - "9090" + environment: + WEB_PORTS: "9090" + VIRTUAL_HOST: merged.nginx-proxy.tld + VIRTUAL_PORT: "9090" + VIRTUAL_PATH: "/foo" + VIRTUAL_DEST: "/" + + merged-multiports: + image: web + expose: + - "81" + - "9191" + environment: + WEB_PORTS: "81 9191" + VIRTUAL_HOST_MULTIPORTS: |- + merged.nginx-proxy.tld: + "/": + port: 81 + "/foo": + port: 9191 + dest: "/" + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro From d6c38a0bab062b0a5f2597e12b83b8e175be0b2a Mon Sep 17 00:00:00 2001 From: Povilas Kanapickas Date: Sat, 4 May 2024 23:52:57 +0300 Subject: [PATCH 17/42] tests: Add tests for how Let's Encrypt ACME challenge is handled At the moment no changes to functionality are done, only the current behavior is captured. --- test/conftest.py | 7 ++++++ .../.well-known/acme-challenge/test-filename | 1 + test/test_ssl/test_https_port.py | 9 ++++++++ test/test_ssl/test_https_port.yml | 1 + test/test_ssl/test_nohttp.py | 7 ++++++ test/test_ssl/test_nohttp.yml | 1 + test/test_ssl/test_nohttps.py | 8 +++++++ test/test_ssl/test_nohttps.yml | 1 + test/test_ssl/test_noredirect.py | 10 +++++++- test/test_ssl/test_noredirect.yml | 1 + test/test_ssl/test_virtual_path.py | 9 ++++++++ test/test_ssl/test_virtual_path.yml | 1 + test/test_ssl/test_wildcard.py | 10 ++++++++ test/test_ssl/test_wildcard.yml | 1 + .../.well-known/acme-challenge/test-filename | 1 + .../docker-compose.yml | 1 + .../test_wildcard_cert_nohttps.py | 23 +++++++++++++++++++ 17 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 test/test_ssl/acme_root/.well-known/acme-challenge/test-filename create mode 100644 test/test_ssl/wildcard_cert_and_nohttps/acme_root/.well-known/acme-challenge/test-filename diff --git a/test/conftest.py b/test/conftest.py index 7fa269a61..dda20f60b 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -510,6 +510,13 @@ def nginxproxy(): yield requests_for_docker() +@pytest.fixture() +def acme_challenge_path(): + """ + Provides fake Let's Encrypt ACME challenge path used in certain tests + """ + return ".well-known/acme-challenge/test-filename" + ############################################################################### # # Py.test hooks diff --git a/test/test_ssl/acme_root/.well-known/acme-challenge/test-filename b/test/test_ssl/acme_root/.well-known/acme-challenge/test-filename new file mode 100644 index 000000000..5b45dff28 --- /dev/null +++ b/test/test_ssl/acme_root/.well-known/acme-challenge/test-filename @@ -0,0 +1 @@ +challenge-teststring diff --git a/test/test_ssl/test_https_port.py b/test/test_ssl/test_https_port.py index 27a42eff6..ebe305f70 100644 --- a/test/test_ssl/test_https_port.py +++ b/test/test_ssl/test_https_port.py @@ -17,3 +17,12 @@ def test_nonstandardport_Host_header(docker_compose, nginxproxy): r = nginxproxy.get("https://web.nginx-proxy.tld:8443/headers") assert r.status_code == 200 assert "Host: web.nginx-proxy.tld:8443" in r.text + +@pytest.mark.parametrize("subdomain", ["foo", "bar"]) +def test_web1_acme_challenge_works(docker_compose, nginxproxy, acme_challenge_path, subdomain): + r = nginxproxy.get( + f"http://{subdomain}.nginx-proxy.tld:8080/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 200 + assert "challenge-teststring\n" in r.text diff --git a/test/test_ssl/test_https_port.yml b/test/test_ssl/test_https_port.yml index 047054a39..b6541acb0 100644 --- a/test/test_ssl/test_https_port.yml +++ b/test/test_ssl/test_https_port.yml @@ -14,6 +14,7 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro environment: HTTP_PORT: 8080 HTTPS_PORT: 8443 diff --git a/test/test_ssl/test_nohttp.py b/test/test_ssl/test_nohttp.py index 5b650db4f..aecbb544a 100644 --- a/test/test_ssl/test_nohttp.py +++ b/test/test_ssl/test_nohttp.py @@ -7,6 +7,13 @@ def test_web2_http_is_connection_refused(docker_compose, nginxproxy): nginxproxy.get("http://web2.nginx-proxy.tld/") +def test_web2_http_is_connection_refused_for_acme_challenge( + docker_compose, nginxproxy, acme_challenge_path +): + with pytest.raises(requests.exceptions.RequestException, match="Connection refused"): + nginxproxy.get(f"http://web2.nginx-proxy.tld/{acme_challenge_path}") + + def test_web2_https_is_forwarded(docker_compose, nginxproxy): r = nginxproxy.get("https://web2.nginx-proxy.tld/port", allow_redirects=False) assert r.status_code == 200 diff --git a/test/test_ssl/test_nohttp.yml b/test/test_ssl/test_nohttp.yml index 40b393ad6..0c21bf9ca 100644 --- a/test/test_ssl/test_nohttp.yml +++ b/test/test_ssl/test_nohttp.yml @@ -15,3 +15,4 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_ssl/test_nohttps.py b/test/test_ssl/test_nohttps.py index 1cedf8218..23e822477 100644 --- a/test/test_ssl/test_nohttps.py +++ b/test/test_ssl/test_nohttps.py @@ -10,3 +10,11 @@ def test_http_is_forwarded(docker_compose, nginxproxy): def test_https_is_disabled(docker_compose, nginxproxy): with pytest.raises(ConnectionError): nginxproxy.get("https://web.nginx-proxy.tld/", allow_redirects=False) + + +def test_http_acme_challenge_does_not_work(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 404 diff --git a/test/test_ssl/test_nohttps.yml b/test/test_ssl/test_nohttps.yml index f2b07574b..209f57a8c 100644 --- a/test/test_ssl/test_nohttps.yml +++ b/test/test_ssl/test_nohttps.yml @@ -14,3 +14,4 @@ services: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_ssl/test_noredirect.py b/test/test_ssl/test_noredirect.py index 62df28b13..0f50063df 100644 --- a/test/test_ssl/test_noredirect.py +++ b/test/test_ssl/test_noredirect.py @@ -16,4 +16,12 @@ def test_web3_https_is_forwarded(docker_compose, nginxproxy): def test_web2_HSTS_policy_is_inactive(docker_compose, nginxproxy): r = nginxproxy.get("https://web3.nginx-proxy.tld/port", allow_redirects=False) assert "answer from port 83\n" in r.text - assert "Strict-Transport-Security" not in r.headers \ No newline at end of file + assert "Strict-Transport-Security" not in r.headers + + +def test_web3_acme_challenge_does_not_work(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 == 404 diff --git a/test/test_ssl/test_noredirect.yml b/test/test_ssl/test_noredirect.yml index 8ee845525..7610ae212 100644 --- a/test/test_ssl/test_noredirect.yml +++ b/test/test_ssl/test_noredirect.yml @@ -15,3 +15,4 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_ssl/test_virtual_path.py b/test/test_ssl/test_virtual_path.py index 508653f24..72ac433b9 100644 --- a/test/test_ssl/test_virtual_path.py +++ b/test/test_ssl/test_virtual_path.py @@ -1,4 +1,5 @@ import pytest +from requests import ConnectionError @pytest.mark.parametrize("path", ["web1", "web2"]) def test_web1_http_redirects_to_https(docker_compose, nginxproxy, path): @@ -13,3 +14,11 @@ def test_web1_https_is_forwarded(docker_compose, nginxproxy, path, port): assert r.status_code == 200 assert "answer from port %d\n" % port in r.text + +@pytest.mark.parametrize("port", [81, 82]) +def test_acme_challenge_does_not_work(docker_compose, nginxproxy, acme_challenge_path, port): + with pytest.raises(ConnectionError): + nginxproxy.get( + f"http://www.nginx-proxy.tld:{port}/{acme_challenge_path}", + allow_redirects=False + ) diff --git a/test/test_ssl/test_virtual_path.yml b/test/test_ssl/test_virtual_path.yml index 2494d35d8..eb09ef0f6 100644 --- a/test/test_ssl/test_virtual_path.yml +++ b/test/test_ssl/test_virtual_path.yml @@ -26,3 +26,4 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_ssl/test_wildcard.py b/test/test_ssl/test_wildcard.py index 202ba247e..f019f68ef 100644 --- a/test/test_ssl/test_wildcard.py +++ b/test/test_ssl/test_wildcard.py @@ -21,3 +21,13 @@ def test_web1_HSTS_policy_is_active(docker_compose, nginxproxy, subdomain): r = nginxproxy.get(f"https://{subdomain}.nginx-proxy.tld/port", allow_redirects=False) assert "answer from port 81\n" in r.text assert "Strict-Transport-Security" in r.headers + + +@pytest.mark.parametrize("subdomain", ["foo", "bar"]) +def test_web1_acme_challenge_works(docker_compose, nginxproxy, acme_challenge_path, subdomain): + r = nginxproxy.get( + f"http://web3.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 200 + assert "challenge-teststring\n" in r.text diff --git a/test/test_ssl/test_wildcard.yml b/test/test_ssl/test_wildcard.yml index ea8c596cc..b101e9f11 100644 --- a/test/test_ssl/test_wildcard.yml +++ b/test/test_ssl/test_wildcard.yml @@ -14,3 +14,4 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_ssl/wildcard_cert_and_nohttps/acme_root/.well-known/acme-challenge/test-filename b/test/test_ssl/wildcard_cert_and_nohttps/acme_root/.well-known/acme-challenge/test-filename new file mode 100644 index 000000000..5b45dff28 --- /dev/null +++ b/test/test_ssl/wildcard_cert_and_nohttps/acme_root/.well-known/acme-challenge/test-filename @@ -0,0 +1 @@ +challenge-teststring diff --git a/test/test_ssl/wildcard_cert_and_nohttps/docker-compose.yml b/test/test_ssl/wildcard_cert_and_nohttps/docker-compose.yml index 98f41a0a2..7cc64e76d 100644 --- a/test/test_ssl/wildcard_cert_and_nohttps/docker-compose.yml +++ b/test/test_ssl/wildcard_cert_and_nohttps/docker-compose.yml @@ -7,6 +7,7 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro web1: image: web diff --git a/test/test_ssl/wildcard_cert_and_nohttps/test_wildcard_cert_nohttps.py b/test/test_ssl/wildcard_cert_and_nohttps/test_wildcard_cert_nohttps.py index 445377912..d07437d76 100644 --- a/test/test_ssl/wildcard_cert_and_nohttps/test_wildcard_cert_nohttps.py +++ b/test/test_ssl/wildcard_cert_and_nohttps/test_wildcard_cert_nohttps.py @@ -1,5 +1,6 @@ import pytest from ssl import CertificateError +from requests import ConnectionError from requests.exceptions import SSLError @@ -32,3 +33,25 @@ def test_https_request_to_nohttps_vhost_goes_to_fallback_server(docker_compose, r = nginxproxy.get("https://3.web.nginx-proxy.tld/port", verify=False) assert r.status_code == 503 + + +@pytest.mark.parametrize("subdomain,acme_should_work", [ + (1, True), + (2, True), + (3, False), +]) +def test_acme_challenge_works( + docker_compose, nginxproxy, acme_challenge_path, subdomain, acme_should_work +): + if acme_should_work: + r = nginxproxy.get( + f"https://{subdomain}.web.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 404 + else: + with pytest.raises(ConnectionError): + nginxproxy.get( + f"https://{subdomain}.web.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) From 99645f104da77aece73a79e04ef37bdf81527809 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Mon, 6 May 2024 14:40:32 +0200 Subject: [PATCH 18/42] docs: typo --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index b6ad130e5..3f3b17d1c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -74,7 +74,7 @@ For each hostname entry, `path`, `port` and `dest` are optionnal and are assigne - `port` = default port - `dest` = "" -The following examples use an hypotetical container running services on port 80, 8000 and 9000: +The following examples use an hypothetical container running services on port 80, 8000 and 9000: #### Multiple ports routed to different hostnames From be319e662977e13fc157960cb9b39adea663309d Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Mon, 6 May 2024 15:34:51 +0200 Subject: [PATCH 19/42] docs: typo Co-authored-by: Niek <100143256+SchoNie@users.noreply.github.com> --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 3f3b17d1c..aea35564b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -68,7 +68,7 @@ hostname: dest: string ``` -For each hostname entry, `path`, `port` and `dest` are optionnal and are assigned default values when missing: +For each hostname entry, `path`, `port` and `dest` are optional and are assigned default values when missing: - `path` = "/" - `port` = default port From 12c4f0c7c2f664eb7f7ed04659fe35e329a56b51 Mon Sep 17 00:00:00 2001 From: KagurazakaNyaa Date: Fri, 28 Oct 2022 10:33:55 +0800 Subject: [PATCH 20/42] Support TCP and UDP proxy --- Dockerfile.alpine | 4 +++- Dockerfile.debian | 4 +++- docs/README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/Dockerfile.alpine b/Dockerfile.alpine index e6eccdc9c..34a059505 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -17,8 +17,10 @@ ENV NGINX_PROXY_VERSION=${NGINX_PROXY_VERSION} \ RUN apk add --no-cache --virtual .run-deps bash openssl # Configure Nginx -RUN sed -i 's/worker_connections.*;$/worker_connections 10240;/' /etc/nginx/nginx.conf \ +RUN echo -e "\ninclude /etc/nginx/toplevel.conf.d/*.conf;" >> /etc/nginx/nginx.conf \ + && sed -i 's/worker_connections.*;$/worker_connections 10240;/' /etc/nginx/nginx.conf \ && sed -i -e '/^\}$/{s//\}\nworker_rlimit_nofile 20480;/;:a' -e '$!N;$!ba' -e '}' /etc/nginx/nginx.conf \ + && mkdir -p '/etc/nginx/toplevel.conf.d' \ && mkdir -p '/etc/nginx/dhparam' \ && mkdir -p '/etc/nginx/certs' diff --git a/Dockerfile.debian b/Dockerfile.debian index 485542d31..840673db4 100644 --- a/Dockerfile.debian +++ b/Dockerfile.debian @@ -14,8 +14,10 @@ ENV NGINX_PROXY_VERSION=${NGINX_PROXY_VERSION} \ DOCKER_HOST=unix:///tmp/docker.sock # Configure Nginx -RUN sed -i 's/worker_connections.*;$/worker_connections 10240;/' /etc/nginx/nginx.conf \ +RUN echo "\ninclude /etc/nginx/toplevel.conf.d/*.conf;" >> /etc/nginx/nginx.conf \ + && sed -i 's/worker_connections.*;$/worker_connections 10240;/' /etc/nginx/nginx.conf \ && sed -i -e '/^\}$/{s//\}\nworker_rlimit_nofile 20480;/;:a' -e '$!N;$!ba' -e '}' /etc/nginx/nginx.conf \ + && mkdir -p '/etc/nginx/toplevel.conf.d' \ && mkdir -p '/etc/nginx/dhparam' \ && mkdir -p '/etc/nginx/certs' diff --git a/docs/README.md b/docs/README.md index 6e29e03ff..23e91bec5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,7 @@ - [HTTP/2 and HTTP/3](#http2-and-http3) - [Headers](#headers) - [Custom Nginx Configuration](#custom-nginx-configuration) +- [TCP and UDP stream](#tcp-and-udp-stream) - [Unhashed vs SHA1 upstream names](#unhashed-vs-sha1-upstream-names) - [Separate Containers](#separate-containers) - [Docker Compose](#docker-compose) @@ -699,6 +700,61 @@ Per virtual-host `servers_tokens` directive can be configured by passing appropr ⬆️ [back to table of contents](#table-of-contents) +## TCP and UDP stream + +If you want to proxy non-HTTP traffic, you can use nginx's stream module. Write a configuration file and mount it inside `/etc/nginx/toplevel.conf.d`. + +```nginx +# stream.conf +stream { + upstream stream_backend { + server backend1.example.com:12345; + server backend2.example.com:12345; + server backend3.example.com:12346; + # ... + } + server { + listen 12345; + #TCP traffic will be forwarded to the "stream_backend" upstream group + proxy_pass stream_backend; + } + + server { + listen 12346; + #TCP traffic will be forwarded to the specified server + proxy_pass backend.example.com:12346; + } + + upstream dns_servers { + server 192.168.136.130:53; + server 192.168.136.131:53; + # ... + } + server { + listen 53 udp; + #UDP traffic will be forwarded to the "dns_servers" upstream group + proxy_pass dns_servers; + } + # ... +} +``` + +```console +docker run --detach \ + --name nginx-proxy \ + --publish 80:80 \ + --publish 12345:12345 \ + --publish 12346:12346 \ + --publish 53:53:udp \ + --volume /var/run/docker.sock:/tmp/docker.sock:ro \ + --volume ./stream.conf:/etc/nginx/toplevel.conf.d/stream.conf:ro \ + nginxproxy/nginx-proxy:1.5 +``` + +Please note that TCP and UDP stream are not core features of nginx-proxy, so the above is provided as an example only, without any guarantee. + +⬆️ [back to table of contents](#table-of-contents) + ## Unhashed vs SHA1 upstream names By default the nginx configuration `upstream` blocks will use this block's corresponding hostname as a predictable name. However, this can cause issues in some setups (see [this issue](https://github.com/nginx-proxy/nginx-proxy/issues/1162)). In those cases you might want to switch to SHA1 names for the `upstream` blocks by setting the `SHA1_UPSTREAM_NAME` environment variable to `true` on the nginx-proxy container. From 59d5293480c36e92cc1942d045658d374217bae4 Mon Sep 17 00:00:00 2001 From: Gilles Filippini Date: Wed, 8 May 2024 18:30:11 +0200 Subject: [PATCH 21/42] fix 'requirements' path in test/test_build.py Without this patch the test fails when run from project base directory. --- test/test_build.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_build.py b/test/test_build.py index 9c798082e..4052d1ee0 100644 --- a/test/test_build.py +++ b/test/test_build.py @@ -4,13 +4,14 @@ import pytest import docker import re +import os client = docker.from_env() @pytest.fixture(scope = "session") def docker_build(request): # Define Dockerfile path - dockerfile_path = "requirements/" + dockerfile_path = os.path.join(os.path.dirname(__file__), "requirements/") dockerfile_name = "Dockerfile-nginx-proxy-tester" # Build the Docker image From 5b1491f46473ee6c2a601f26aa5c687b4919a39e Mon Sep 17 00:00:00 2001 From: Gilles Filippini Date: Sat, 29 Apr 2023 18:11:21 +0200 Subject: [PATCH 22/42] build: disambiguate base image names to build with podman / buildah. --- Dockerfile.alpine | 6 +++--- Dockerfile.debian | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 34a059505..588033e49 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -1,9 +1,9 @@ -FROM nginxproxy/docker-gen:0.13.0 AS docker-gen +FROM docker.io/nginxproxy/docker-gen:0.13.0 AS docker-gen -FROM nginxproxy/forego:0.18.1 AS forego +FROM docker.io/nginxproxy/forego:0.18.1 AS forego # Build the final image -FROM nginx:1.26.0-alpine +FROM docker.io/library/nginx:1.26.0-alpine ARG NGINX_PROXY_VERSION # Add DOCKER_GEN_VERSION environment variable because diff --git a/Dockerfile.debian b/Dockerfile.debian index 840673db4..7ab29e041 100644 --- a/Dockerfile.debian +++ b/Dockerfile.debian @@ -1,9 +1,9 @@ -FROM nginxproxy/docker-gen:0.13.0-debian AS docker-gen +FROM docker.io/nginxproxy/docker-gen:0.13.0-debian AS docker-gen -FROM nginxproxy/forego:0.18.1-debian AS forego +FROM docker.io/nginxproxy/forego:0.18.1-debian AS forego # Build the final image -FROM nginx:1.26.0 +FROM docker.io/library/nginx:1.26.0 ARG NGINX_PROXY_VERSION # Add DOCKER_GEN_VERSION environment variable because From 6c1b532ffb4cd732d99a59f753e4f77deac4a157 Mon Sep 17 00:00:00 2001 From: Gilles Filippini Date: Thu, 9 May 2024 23:28:34 +0200 Subject: [PATCH 23/42] Improve acme-challenge handling So that there is no need anymore for the Let's Encrypt companion to fiddle with vhosts nginx configuration. When `HTTPS_METHOD=nohttp` and the certificate is missing, enforce nohttp instead of switching to `HTTPS_METHOD=redirect`. --- nginx.tmpl | 12 +++++++++++- test/test_ssl/test_noredirect.py | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index 2b73158b0..b50997c67 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -762,6 +762,16 @@ server { {{- if $globals.enable_ipv6 }} listen [::]:{{ $globals.external_http_port }} {{ $default_server }}; {{- end }} + + {{- if (eq $vhost.https_method "noredirect") }} + location /.well-known/acme-challenge/ { + auth_basic off; + allow all; + root /usr/share/nginx/html; + try_files $uri =404; + break; + } + {{- end }} {{- end }} {{- if ne $vhost.https_method "nohttps" }} listen {{ $globals.external_https_port }} ssl {{ $default_server }}; @@ -856,4 +866,4 @@ server { } {{- end }} } -{{- end }} \ No newline at end of file +{{- end }} diff --git a/test/test_ssl/test_noredirect.py b/test/test_ssl/test_noredirect.py index 0f50063df..1d956d198 100644 --- a/test/test_ssl/test_noredirect.py +++ b/test/test_ssl/test_noredirect.py @@ -19,9 +19,9 @@ def test_web2_HSTS_policy_is_inactive(docker_compose, nginxproxy): assert "Strict-Transport-Security" not in r.headers -def test_web3_acme_challenge_does_not_work(docker_compose, nginxproxy, acme_challenge_path): +def test_web3_acme_challenge_does_work(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 == 404 + assert r.status_code == 200 From 91652aac48c60b4053d54c051b8a88b1ea2720f0 Mon Sep 17 00:00:00 2001 From: Gilles Filippini Date: Tue, 14 May 2024 22:30:06 +0200 Subject: [PATCH 24/42] fix: constistent behavior for `HTTPS_METHOD=nohttp` Without this fix the response of nohttp sites to HTTP requests changes depending on the existence of at least one HTTP enabled site: * no HTTP enabled sites -> connection refused * at least one HTTP enabled site -> 503 This fix ensures the response is always 503. --- nginx.tmpl | 3 +-- test/test_fallback.py | 12 +++++------- test/test_ssl/test_nohttp.py | 8 ++++---- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index b50997c67..72181d433 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -650,7 +650,7 @@ proxy_set_header Proxy ""; {{- $default_https_exists = or $default_https_exists (and $https $vhost.default) }} {{- $http3_enabled = or $http3_enabled $vhost.http3_enabled }} {{- end }} - {{- $fallback_http := and $http_exists (not $default_http_exists) }} + {{- $fallback_http := not $default_http_exists }} {{- $fallback_https := and $https_exists (not $default_https_exists) }} {{- /* * If there are no vhosts at all, create fallbacks for both plain http @@ -658,7 +658,6 @@ proxy_set_header Proxy ""; * refused error. */}} {{- if and (not $http_exists) (not $https_exists) }} - {{- $fallback_http = true }} {{- $fallback_https = true }} {{- end }} {{- if or $fallback_http $fallback_https }} diff --git a/test/test_fallback.py b/test/test_fallback.py index 1ee923ac1..16da3d7d6 100644 --- a/test/test_fallback.py +++ b/test/test_fallback.py @@ -60,19 +60,17 @@ def _get(url): ("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. - ("nohttp.yml", "http://https-only.nginx-proxy.test/", None, CONNECTION_REFUSED_RE), + ("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/", None, CONNECTION_REFUSED_RE), + ("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-on-app.yml", "http://https-only.nginx-proxy.test/", None, CONNECTION_REFUSED_RE), + ("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/", None, CONNECTION_REFUSED_RE), + ("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 - # HTTPS_METHOD=nohttp setting to effectively become HTTPS_METHOD=noredirect. This means that - # there will be a plain http server solely to support that vhost, so http requests to other - # vhosts get a 503, not a connection refused error. + # 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), diff --git a/test/test_ssl/test_nohttp.py b/test/test_ssl/test_nohttp.py index aecbb544a..032f60fd4 100644 --- a/test/test_ssl/test_nohttp.py +++ b/test/test_ssl/test_nohttp.py @@ -3,15 +3,15 @@ def test_web2_http_is_connection_refused(docker_compose, nginxproxy): - with pytest.raises(requests.exceptions.RequestException, match="Connection refused"): - nginxproxy.get("http://web2.nginx-proxy.tld/") + r = nginxproxy.get("http://web2.nginx-proxy.tld/", allow_redirects=False) + assert r.status_code == 503 def test_web2_http_is_connection_refused_for_acme_challenge( docker_compose, nginxproxy, acme_challenge_path ): - with pytest.raises(requests.exceptions.RequestException, match="Connection refused"): - nginxproxy.get(f"http://web2.nginx-proxy.tld/{acme_challenge_path}") + r = nginxproxy.get(f"http://web2.nginx-proxy.tld/{acme_challenge_path}", allow_redirects=False) + assert r.status_code == 503 def test_web2_https_is_forwarded(docker_compose, nginxproxy): From 8e372c39c27ef51241310ef2f89ceed1afede2fb Mon Sep 17 00:00:00 2001 From: Rodrigo Aguilera Date: Sun, 10 Jul 2022 23:05:24 +0200 Subject: [PATCH 25/42] fix: include a complete fastcgi_params config --- nginx.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx.tmpl b/nginx.tmpl index 72181d433..2ad433925 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -265,7 +265,7 @@ uwsgi_pass {{ trim $proto }}://{{ trim $upstream }}; {{- else if eq $proto "fastcgi" }} root {{ trim .VhostRoot }}; - include fastcgi_params; + include fastcgi.conf; fastcgi_pass {{ trim $upstream }}; {{- if ne $keepalive "disabled" }} fastcgi_keep_conn on; From f22b64df795f5fa46f3efa46a3ffb34af8d8711b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 16:23:18 +0000 Subject: [PATCH 26/42] build: bump nginxproxy/docker-gen from 0.13.0-debian to 0.13.1-debian Bumps nginxproxy/docker-gen from 0.13.0-debian to 0.13.1-debian. --- updated-dependencies: - dependency-name: nginxproxy/docker-gen dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Dockerfile.alpine | 2 +- Dockerfile.debian | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 588033e49..3137a175c 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -1,4 +1,4 @@ -FROM docker.io/nginxproxy/docker-gen:0.13.0 AS docker-gen +FROM docker.io/nginxproxy/docker-gen:0.13.1 AS docker-gen FROM docker.io/nginxproxy/forego:0.18.1 AS forego diff --git a/Dockerfile.debian b/Dockerfile.debian index 7ab29e041..cc9545b4c 100644 --- a/Dockerfile.debian +++ b/Dockerfile.debian @@ -1,4 +1,4 @@ -FROM docker.io/nginxproxy/docker-gen:0.13.0-debian AS docker-gen +FROM docker.io/nginxproxy/docker-gen:0.13.1-debian AS docker-gen FROM docker.io/nginxproxy/forego:0.18.1-debian AS forego From 49f0b89fb0a97e3f62568838bc37cdb5ba06f287 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 04:47:19 +0000 Subject: [PATCH 27/42] ci: bump pytest from 8.2.0 to 8.2.1 in /test/requirements Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.0 to 8.2.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.0...8.2.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- test/requirements/python-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/requirements/python-requirements.txt b/test/requirements/python-requirements.txt index f22c000dd..ebabe2eca 100644 --- a/test/requirements/python-requirements.txt +++ b/test/requirements/python-requirements.txt @@ -1,4 +1,4 @@ backoff==2.2.1 docker==7.0.0 -pytest==8.2.0 +pytest==8.2.1 requests==2.31.0 From 1c1f8e8700eaba62af96e63ef885b6efbe90f069 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 09:09:45 +0000 Subject: [PATCH 28/42] --- updated-dependencies: - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- test/requirements/python-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/requirements/python-requirements.txt b/test/requirements/python-requirements.txt index ebabe2eca..66d719d86 100644 --- a/test/requirements/python-requirements.txt +++ b/test/requirements/python-requirements.txt @@ -1,4 +1,4 @@ backoff==2.2.1 docker==7.0.0 pytest==8.2.1 -requests==2.31.0 +requests==2.32.1 From 2a793b2d5bff560bceeaa5cf6949698334662123 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Tue, 21 May 2024 13:30:34 +0200 Subject: [PATCH 29/42] fix: hardcode requests package version to 2.31.0 This reverts commit 1c1f8e8700eaba62af96e63ef885b6efbe90f069. --- test/requirements/python-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/requirements/python-requirements.txt b/test/requirements/python-requirements.txt index 66d719d86..ebabe2eca 100644 --- a/test/requirements/python-requirements.txt +++ b/test/requirements/python-requirements.txt @@ -1,4 +1,4 @@ backoff==2.2.1 docker==7.0.0 pytest==8.2.1 -requests==2.32.1 +requests==2.31.0 From fb9c3a646a4184bd9e33a946148a0e85c6cacd05 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Wed, 22 May 2024 08:23:48 +0200 Subject: [PATCH 30/42] feat: custom default error page (#2430) * feat: customizable error page * fix: use regex on catchall root location to fix DEFAULT_ROOT=none test * docs: custom error pages * fix: don't use default nginx image error page * docs: small fix --- Dockerfile.alpine | 3 ++- Dockerfile.debian | 3 ++- docs/README.md | 15 ++++++++++++ nginx.tmpl | 12 +++++++++- test/test_custom-error-page/50x.html | 23 +++++++++++++++++++ .../test_custom-error-page.py | 8 +++++++ .../test_custom-error-page.yml | 8 +++++++ 7 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 test/test_custom-error-page/50x.html create mode 100644 test/test_custom-error-page/test_custom-error-page.py create mode 100644 test/test_custom-error-page/test_custom-error-page.yml diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 3137a175c..c8ca701cd 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -22,7 +22,8 @@ RUN echo -e "\ninclude /etc/nginx/toplevel.conf.d/*.conf;" >> /etc/nginx/nginx.c && sed -i -e '/^\}$/{s//\}\nworker_rlimit_nofile 20480;/;:a' -e '$!N;$!ba' -e '}' /etc/nginx/nginx.conf \ && mkdir -p '/etc/nginx/toplevel.conf.d' \ && mkdir -p '/etc/nginx/dhparam' \ - && mkdir -p '/etc/nginx/certs' + && mkdir -p '/etc/nginx/certs' \ + && mkdir -p '/usr/share/nginx/html/errors' # Install Forego + docker-gen COPY --from=forego /usr/local/bin/forego /usr/local/bin/forego diff --git a/Dockerfile.debian b/Dockerfile.debian index cc9545b4c..08c014ede 100644 --- a/Dockerfile.debian +++ b/Dockerfile.debian @@ -19,7 +19,8 @@ RUN echo "\ninclude /etc/nginx/toplevel.conf.d/*.conf;" >> /etc/nginx/nginx.conf && sed -i -e '/^\}$/{s//\}\nworker_rlimit_nofile 20480;/;:a' -e '$!N;$!ba' -e '}' /etc/nginx/nginx.conf \ && mkdir -p '/etc/nginx/toplevel.conf.d' \ && mkdir -p '/etc/nginx/dhparam' \ - && mkdir -p '/etc/nginx/certs' + && mkdir -p '/etc/nginx/certs' \ + && mkdir -p '/usr/share/nginx/html/errors' # Install Forego + docker-gen COPY --from=forego /usr/local/bin/forego /usr/local/bin/forego diff --git a/docs/README.md b/docs/README.md index 949c0e887..5e79a022f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -797,6 +797,21 @@ location / { Per virtual-host `servers_tokens` directive can be configured by passing appropriate value to the `SERVER_TOKENS` environment variable. Please see the [nginx http_core module configuration](https://nginx.org/en/docs/http/ngx_http_core_module.html#server_tokens) for more details. +### Custom error page + +To override the default error page displayed on 50x errors, mount your custom HTML error page inside the container at `/usr/share/nginx/html/errors/50x.html`: + +```console +docker run --detach \ + --name nginx-proxy \ + --publish 80:80 \ + --volume /var/run/docker.sock:/tmp/docker.sock:ro \ + --volume /path/to/error.html:/usr/share/nginx/html/errors/50x.html:ro \ + nginxproxy/nginx-proxy:1.5 +``` + +Note that this will not replace your own services error pages. + ⬆️ [back to table of contents](#table-of-contents) ## TCP and UDP stream diff --git a/nginx.tmpl b/nginx.tmpl index 2ad433925..86f001f86 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -702,7 +702,17 @@ server { return 444; } {{- end }} - return 503; + + {{- if (exists "/usr/share/nginx/html/errors/50x.html") }} + error_page 500 502 503 504 /50x.html; + location /50x.html { + root /usr/share/nginx/html/errors; + internal; + } + {{- end }} + location ^~ / { + return 503; + } } {{- end }} {{- end }} diff --git a/test/test_custom-error-page/50x.html b/test/test_custom-error-page/50x.html new file mode 100644 index 000000000..63a299ec0 --- /dev/null +++ b/test/test_custom-error-page/50x.html @@ -0,0 +1,23 @@ + + + + Maintenance + + + +

Damn, there's some maintenance in progress.

+

+ Our apologies for this temporary inconvenience. Regular service + performance will be re-established shortly. +

+ + diff --git a/test/test_custom-error-page/test_custom-error-page.py b/test/test_custom-error-page/test_custom-error-page.py new file mode 100644 index 000000000..32cb0b542 --- /dev/null +++ b/test/test_custom-error-page/test_custom-error-page.py @@ -0,0 +1,8 @@ +import pytest +import re + + +def test_custom_error_page(docker_compose, nginxproxy): + r = nginxproxy.get("http://unknown.nginx-proxy.tld") + assert r.status_code == 503 + assert re.search(r"Damn, there's some maintenance in progress.", r.text) diff --git a/test/test_custom-error-page/test_custom-error-page.yml b/test/test_custom-error-page/test_custom-error-page.yml new file mode 100644 index 000000000..419b7eb8d --- /dev/null +++ b/test/test_custom-error-page/test_custom-error-page.yml @@ -0,0 +1,8 @@ +version: "2" + +services: + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./50x.html:/usr/share/nginx/html/errors/50x.html:ro From 7a761e10731a645a6b7ab20f440bee9b9624ae75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 May 2024 13:54:05 +0200 Subject: [PATCH 31/42] ci: bump docker from 7.0.0 to 7.1.0 in /test/requirements (#2461) Bumps [docker](https://github.com/docker/docker-py) from 7.0.0 to 7.1.0. - [Release notes](https://github.com/docker/docker-py/releases) - [Commits](https://github.com/docker/docker-py/compare/7.0.0...7.1.0) --- updated-dependencies: - dependency-name: docker dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/requirements/python-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/requirements/python-requirements.txt b/test/requirements/python-requirements.txt index ebabe2eca..74d0348fe 100644 --- a/test/requirements/python-requirements.txt +++ b/test/requirements/python-requirements.txt @@ -1,4 +1,4 @@ backoff==2.2.1 -docker==7.0.0 +docker==7.1.0 pytest==8.2.1 requests==2.31.0 From 0dfc8b7a5038c062ea5b23301cc0baf92ebf2bc6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 May 2024 14:04:28 +0200 Subject: [PATCH 32/42] ci: bump requests from 2.31.0 to 2.32.2 in /test/requirements (#2462) Bumps [requests](https://github.com/psf/requests) from 2.31.0 to 2.32.2. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.31.0...v2.32.2) --- updated-dependencies: - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/requirements/python-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/requirements/python-requirements.txt b/test/requirements/python-requirements.txt index 74d0348fe..4618d1301 100644 --- a/test/requirements/python-requirements.txt +++ b/test/requirements/python-requirements.txt @@ -1,4 +1,4 @@ backoff==2.2.1 docker==7.1.0 pytest==8.2.1 -requests==2.31.0 +requests==2.32.2 From 5f3ec18b282c4f71f5aa9bc1f1ba54d03fc1d61a Mon Sep 17 00:00:00 2001 From: pini-gh Date: Mon, 27 May 2024 20:50:13 +0200 Subject: [PATCH 33/42] docs: explicit policy on missing certificate (#2465) * chore/doc: explicit policy on missing certificate This doesn't change the current nginx-proxy behavior, but makes explicit the current HTTPS_METHOD policy on missing certificate. * fix: bad wording about missing certificate Co-authored-by: Nicolas Duchon * docs: typo in suggestion --------- Co-authored-by: Nicolas Duchon --- docs/README.md | 4 +++- nginx.tmpl | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index 5e79a022f..3cd6f4f84 100644 --- a/docs/README.md +++ b/docs/README.md @@ -572,7 +572,9 @@ _WARNING_: HSTS will force your users to visit the HTTPS version of your site fo ### Missing Certificate -If HTTPS is enabled for a virtual host but its certificate is missing, nginx-proxy will configure nginx to use the default certificate (`default.crt` with `default.key`) and return a 500 error. +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, +* force enable HTTP; i.e. `HTTPS_METHOD` will switch to `noredirect` if it was set to `nohttp` or `redirect`. If the default certificate is also missing, nginx-proxy will configure nginx to accept HTTPS connections but fail the TLS negotiation. Client browsers will render a TLS error page. As of March 2023, web browsers display the following error messages: diff --git a/nginx.tmpl b/nginx.tmpl index 86f001f86..b3a601126 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -590,6 +590,10 @@ proxy_set_header Proxy ""; {{- $default := eq $globals.Env.DEFAULT_HOST $hostname }} {{- $https_method := or (first (groupByKeys $vhost_containers "Env.HTTPS_METHOD")) $globals.Env.HTTPS_METHOD "redirect" }} + {{- /* When the certificate is missing we want to ensure that HTTP is enabled; hence switching from 'nohttp' or 'redirect' to 'noredirect' */}} + {{- if (and (not $cert_ok) (or (eq $https_method "nohttp") (eq $https_method "redirect"))) }} + {{- $https_method = "noredirect" }} + {{- end }} {{- $http2_enabled := parseBool (or (first (keys (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http2.enable"))) $globals.Env.ENABLE_HTTP2 "true")}} {{- $http3_enabled := parseBool (or (first (keys (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http3.enable"))) $globals.Env.ENABLE_HTTP3 "false")}} @@ -642,7 +646,7 @@ proxy_set_header Proxy ""; {{- $default_https_exists := false }} {{- $http3_enabled := false }} {{- range $vhost := $globals.vhosts }} - {{- $http := or (ne $vhost.https_method "nohttp") (not $vhost.cert_ok) }} + {{- $http := ne $vhost.https_method "nohttp" }} {{- $https := ne $vhost.https_method "nohttps" }} {{- $http_exists = or $http_exists $http }} {{- $https_exists = or $https_exists $https }} @@ -725,7 +729,7 @@ server { {{ template "upstream" (dict "globals" $globals "Path" $path "VPath" $vpath) }} {{- end }} - {{- if and $vhost.cert_ok (eq $vhost.https_method "redirect") }} + {{- if (eq $vhost.https_method "redirect") }} server { server_name {{ $hostname }}; {{- if $vhost.server_tokens }} @@ -766,7 +770,7 @@ server { {{- if $vhost.http2_enabled }} http2 on; {{- end }} - {{- if or (eq $vhost.https_method "nohttps") (not $vhost.cert_ok) (eq $vhost.https_method "noredirect") }} + {{- 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 }}; From e904471cd304643fdbcdd37aef7e67a86c309787 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 07:45:00 +0200 Subject: [PATCH 34/42] build: bump nginxproxy/docker-gen from 0.13.1-debian to 0.14.0-debian (#2467) Bumps nginxproxy/docker-gen from 0.13.1-debian to 0.14.0-debian. --- updated-dependencies: - dependency-name: nginxproxy/docker-gen dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile.alpine | 2 +- Dockerfile.debian | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.alpine b/Dockerfile.alpine index c8ca701cd..cca9e3c35 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -1,4 +1,4 @@ -FROM docker.io/nginxproxy/docker-gen:0.13.1 AS docker-gen +FROM docker.io/nginxproxy/docker-gen:0.14.0 AS docker-gen FROM docker.io/nginxproxy/forego:0.18.1 AS forego diff --git a/Dockerfile.debian b/Dockerfile.debian index 08c014ede..5bad37aa3 100644 --- a/Dockerfile.debian +++ b/Dockerfile.debian @@ -1,4 +1,4 @@ -FROM docker.io/nginxproxy/docker-gen:0.13.1-debian AS docker-gen +FROM docker.io/nginxproxy/docker-gen:0.14.0-debian AS docker-gen FROM docker.io/nginxproxy/forego:0.18.1-debian AS forego From 9cf736f1f8526be426eaa159afc37c8ee3ed42f0 Mon Sep 17 00:00:00 2001 From: pini-gh Date: Fri, 31 May 2024 00:10:44 +0200 Subject: [PATCH 35/42] feat: variable ACME_HTTP_CHALLENGE_LOCATION (#2468) Values: * `legacy` (default): generate location blocks for ACME HTP Challenge excepted when `HTTPS_METHOD=noredirect` or there is no certificate for the domain * `true`: generate location blocks for ACME HTP Challenge in all cases * `false`: do not generate location blocks for ACME HTP Challenge This feature is currently needed because acme-companion may generate the HTTP Challenge configuration while it was done already by nginx-proxy (see #2465#issuecomment-2136361373). Also sometimes a hardcoded ACME challenge location is not wanted because the challenge validation is not done with acme-companion / Let's Encrypt, and with a challenge location setup differently. --- docs/README.md | 5 ++ nginx.tmpl | 12 +++- .../.well-known/acme-challenge/test-filename | 1 + .../certs/nginx-proxy.tld.crt | 70 +++++++++++++++++++ .../certs/nginx-proxy.tld.key | 27 +++++++ .../test_acme_challenge_location_disabled.py | 30 ++++++++ .../test_acme_challenge_location_disabled.yml | 47 +++++++++++++ .../test_acme_challenge_location_enabled.py | 30 ++++++++ .../test_acme_challenge_location_enabled.yml | 47 +++++++++++++ ...me_challenge_location_legacy_is_default.py | 16 +++++ ...e_challenge_location_legacy_is_default.yml | 26 +++++++ test/test_ssl/test_noredirect.py | 4 +- 12 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 test/test_acme_http_challenge_location/acme_root/.well-known/acme-challenge/test-filename create mode 100644 test/test_acme_http_challenge_location/certs/nginx-proxy.tld.crt create mode 100644 test/test_acme_http_challenge_location/certs/nginx-proxy.tld.key create mode 100644 test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.py create mode 100644 test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.yml create mode 100644 test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.py create mode 100644 test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.yml create mode 100644 test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.py create mode 100644 test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.yml diff --git a/docs/README.md b/docs/README.md index 3cd6f4f84..8d7d2f0fe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -421,6 +421,11 @@ If you are running the container in a virtualized environment (Hyper-V, VirtualB [acme-companion](https://github.com/nginx-proxy/acme-companion) is a lightweight companion container for the nginx-proxy. It allows the automated creation/renewal of SSL certificates using the ACME protocol. +By default nginx-proxy generates location blocks to handle ACME HTTP Challenge, excepted when `HTTPS_METHOD=noredirect` or there is no certificate for the domain. Ths behavior can be changed with environment variable `ACME_HTTP_CHALLENGE_LOCATION`. It accepts these values: +* `legacy`: default value; current default behavior +* `true`: handle ACME HTTP Challenge in all cases +* `false`: do not handle ACME HTTP Chalenge at all. + ### 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 b3a601126..4fd611071 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -596,6 +596,12 @@ proxy_set_header Proxy ""; {{- end }} {{- $http2_enabled := parseBool (or (first (keys (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http2.enable"))) $globals.Env.ENABLE_HTTP2 "true")}} {{- $http3_enabled := parseBool (or (first (keys (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http3.enable"))) $globals.Env.ENABLE_HTTP3 "false")}} + {{- $acme_http_challenge := or (first (groupByKeys $vhost_containers "Env.ACME_HTTP_CHALLENGE_LOCATION")) $globals.Env.ACME_HTTP_CHALLENGE_LOCATION "legacy" }} + {{- $acme_http_challenge_legacy := eq $acme_http_challenge "legacy" }} + {{- $acme_http_challenge_enabled := false }} + {{- if (not $acme_http_challenge_legacy) }} + {{- $acme_http_challenge_enabled = parseBool $acme_http_challenge }} + {{- end }} {{- /* Get the SERVER_TOKENS defined by containers w/ the same vhost, falling back to "". */}} {{- $server_tokens := trim (or (first (groupByKeys $vhost_containers "Env.SERVER_TOKENS")) "") }} @@ -617,6 +623,8 @@ proxy_set_header Proxy ""; "https_method" $https_method "http2_enabled" $http2_enabled "http3_enabled" $http3_enabled + "acme_http_challenge_legacy" $acme_http_challenge_legacy + "acme_http_challenge_enabled" $acme_http_challenge_enabled "server_tokens" $server_tokens "ssl_policy" $ssl_policy "vhost_root" $vhost_root @@ -741,6 +749,7 @@ server { listen [::]:{{ $globals.external_http_port }} {{ $default_server }}; {{- end }} + {{- if (or $vhost.acme_http_challenge_legacy $vhost.acme_http_challenge_enabled) }} # Do not HTTPS redirect Let's Encrypt ACME challenge location ^~ /.well-known/acme-challenge/ { auth_basic off; @@ -750,6 +759,7 @@ server { try_files $uri =404; break; } + {{- end }} location / { {{- if eq $globals.external_https_port "443" }} @@ -776,7 +786,7 @@ server { listen [::]:{{ $globals.external_http_port }} {{ $default_server }}; {{- end }} - {{- if (eq $vhost.https_method "noredirect") }} + {{- if (and (eq $vhost.https_method "noredirect") $vhost.acme_http_challenge_enabled) }} location /.well-known/acme-challenge/ { auth_basic off; allow all; diff --git a/test/test_acme_http_challenge_location/acme_root/.well-known/acme-challenge/test-filename b/test/test_acme_http_challenge_location/acme_root/.well-known/acme-challenge/test-filename new file mode 100644 index 000000000..5b45dff28 --- /dev/null +++ b/test/test_acme_http_challenge_location/acme_root/.well-known/acme-challenge/test-filename @@ -0,0 +1 @@ +challenge-teststring diff --git a/test/test_acme_http_challenge_location/certs/nginx-proxy.tld.crt b/test/test_acme_http_challenge_location/certs/nginx-proxy.tld.crt new file mode 100644 index 000000000..cd7284b06 --- /dev/null +++ b/test/test_acme_http_challenge_location/certs/nginx-proxy.tld.crt @@ -0,0 +1,70 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 4096 (0x1000) + Signature Algorithm: sha256WithRSAEncryption + Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld + Validity + Not Before: Jan 10 00:08:52 2017 GMT + Not After : May 28 00:08:52 2044 GMT + Subject: CN=*.nginx-proxy.tld + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:cb:45:f4:14:9b:fe:64:85:79:4a:36:8d:3d:d1: + 27:d0:7c:36:28:30:e6:73:80:6f:7c:49:23:d0:6c: + 17:e4:44:c0:77:4d:9a:c2:bc:24:84:e3:a5:4d:ba: + d2:da:51:7b:a1:2a:12:d4:c0:19:55:69:2c:22:27: + 2d:1a:f6:fc:4b:7f:e9:cb:a8:3c:e8:69:b8:d2:4f: + de:4e:50:e2:d0:74:30:7c:42:5a:ae:aa:85:a5:b1: + 71:4d:c9:7e:86:8b:62:8c:3e:0d:e3:3b:c3:f5:81: + 0b:8c:68:79:fe:bf:10:fb:ae:ec:11:49:6d:64:5e: + 1a:7d:b3:92:93:4e:96:19:3a:98:04:a7:66:b2:74: + 61:2d:41:13:0c:a4:54:0d:2c:78:fd:b4:a3:e8:37: + 78:9a:de:fa:bc:2e:a8:0f:67:14:58:ce:c3:87:d5: + 14:0e:8b:29:7d:48:19:b2:a9:f5:b4:e8:af:32:21: + 67:15:7e:43:52:8b:20:cf:9f:38:43:bf:fd:c8:24: + 7f:52:a3:88:f2:f1:4a:14:91:2a:6e:91:6f:fb:7d: + 6a:78:c6:6d:2e:dd:1e:4c:2b:63:bb:3a:43:9c:91: + f9:df:d3:08:13:63:86:7d:ce:e8:46:cf:f1:6c:1f: + ca:f7:4c:de:d8:4b:e0:da:bc:06:d9:87:0f:ff:96: + 45:85 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Alternative Name: + DNS:*.nginx-proxy.tld + Signature Algorithm: sha256WithRSAEncryption + 6e:a5:0e:e4:d3:cc:d5:b7:fc:34:75:89:4e:98:8c:e7:08:06: + a8:5b:ec:13:7d:83:99:a2:61:b8:d5:12:6e:c5:b4:53:4e:9a: + 22:cd:ad:14:30:6a:7d:58:d7:23:d9:a4:2a:96:a0:40:9e:50: + 9f:ce:f2:fe:8c:dd:9a:ac:99:39:5b:89:2d:ca:e5:3e:c3:bc: + 03:04:1c:12:d9:6e:b8:9f:f0:3a:be:12:44:7e:a4:21:86:73: + af:d5:00:51:3f:2c:56:70:34:8f:26:b0:7f:b0:cf:cf:7f:f9: + 40:6f:00:29:c4:cf:c3:b7:c2:49:3d:3f:b0:26:78:87:b9:c7: + 6c:1b:aa:6a:1a:dd:c5:eb:f2:69:ba:6d:46:0b:92:49:b5:11: + 3c:eb:48:c7:2f:fb:33:a6:6a:82:a2:ab:f8:1e:5f:7d:e3:b7: + f2:fd:f5:88:a5:09:4d:a0:bc:f4:3b:cd:d2:8b:d7:57:1f:86: + 3b:d2:3e:a4:92:21:b0:02:0b:e9:e0:c4:1c:f1:78:e2:58:a7: + 26:5f:4c:29:c8:23:f0:6e:12:3f:bd:ad:44:7b:0b:bd:db:ba: + 63:8d:07:c6:9d:dc:46:cc:63:40:ba:5e:45:82:dd:9a:e5:50: + e8:e7:d7:27:88:fc:6f:1d:8a:e7:5c:49:28:aa:10:29:75:28: + c7:52:de:f9 +-----BEGIN CERTIFICATE----- +MIIC9zCCAd+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp +bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs +ZDAeFw0xNzAxMTAwMDA4NTJaFw00NDA1MjgwMDA4NTJaMBwxGjAYBgNVBAMMESou +bmdpbngtcHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +y0X0FJv+ZIV5SjaNPdEn0Hw2KDDmc4BvfEkj0GwX5ETAd02awrwkhOOlTbrS2lF7 +oSoS1MAZVWksIictGvb8S3/py6g86Gm40k/eTlDi0HQwfEJarqqFpbFxTcl+hoti +jD4N4zvD9YELjGh5/r8Q+67sEUltZF4afbOSk06WGTqYBKdmsnRhLUETDKRUDSx4 +/bSj6Dd4mt76vC6oD2cUWM7Dh9UUDospfUgZsqn1tOivMiFnFX5DUosgz584Q7/9 +yCR/UqOI8vFKFJEqbpFv+31qeMZtLt0eTCtjuzpDnJH539MIE2OGfc7oRs/xbB/K +90ze2Evg2rwG2YcP/5ZFhQIDAQABoyAwHjAcBgNVHREEFTATghEqLm5naW54LXBy +b3h5LnRsZDANBgkqhkiG9w0BAQsFAAOCAQEAbqUO5NPM1bf8NHWJTpiM5wgGqFvs +E32DmaJhuNUSbsW0U06aIs2tFDBqfVjXI9mkKpagQJ5Qn87y/ozdmqyZOVuJLcrl +PsO8AwQcEtluuJ/wOr4SRH6kIYZzr9UAUT8sVnA0jyawf7DPz3/5QG8AKcTPw7fC +ST0/sCZ4h7nHbBuqahrdxevyabptRguSSbURPOtIxy/7M6ZqgqKr+B5ffeO38v31 +iKUJTaC89DvN0ovXVx+GO9I+pJIhsAIL6eDEHPF44linJl9MKcgj8G4SP72tRHsL +vdu6Y40Hxp3cRsxjQLpeRYLdmuVQ6OfXJ4j8bx2K51xJKKoQKXUox1Le+Q== +-----END CERTIFICATE----- diff --git a/test/test_acme_http_challenge_location/certs/nginx-proxy.tld.key b/test/test_acme_http_challenge_location/certs/nginx-proxy.tld.key new file mode 100644 index 000000000..91adb14e1 --- /dev/null +++ b/test/test_acme_http_challenge_location/certs/nginx-proxy.tld.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAy0X0FJv+ZIV5SjaNPdEn0Hw2KDDmc4BvfEkj0GwX5ETAd02a +wrwkhOOlTbrS2lF7oSoS1MAZVWksIictGvb8S3/py6g86Gm40k/eTlDi0HQwfEJa +rqqFpbFxTcl+hotijD4N4zvD9YELjGh5/r8Q+67sEUltZF4afbOSk06WGTqYBKdm +snRhLUETDKRUDSx4/bSj6Dd4mt76vC6oD2cUWM7Dh9UUDospfUgZsqn1tOivMiFn +FX5DUosgz584Q7/9yCR/UqOI8vFKFJEqbpFv+31qeMZtLt0eTCtjuzpDnJH539MI +E2OGfc7oRs/xbB/K90ze2Evg2rwG2YcP/5ZFhQIDAQABAoIBAQCjAro2PNLJMfCO +fyjNRgmzu6iCmpR0U68T8GN0JPsT576g7e8J828l0pkhuIyW33lRSThIvLSUNf9a +dChL032H3lBTLduKVh4NKleQXnVFzaeEPoISSFVdButiAhAhPW4OIUVp0OfY3V+x +fac3j2nDLAfL5SKAtqZv363Py9m66EBYm5BmGTQqT/frQWeCEBvlErQef5RIaU8p +e2zMWgSNNojVai8U3nKNRvYHWeWXM6Ck7lCvkHhMF+RpbmCZuqhbEARVnehU/Jdn +QHJ3nxeA2OWpoWKXvAHtSnno49yxq1UIstiQvY+ng5C5i56UlB60UiU2NJ6doZkB +uQ7/1MaBAoGBAORdcFtgdgRALjXngFWhpCp0CseyUehn1KhxDCG+D1pJ142/ymcf +oJOzKJPMRNDdDUBMnR1GBfy7rmwvYevI/SMNy2Qs7ofcXPbdtwwvTCToZ1V9/54k +VfuPBFT+3QzWRvG1tjTV3E4L2VV3nrl2qNPhE5DlfIaU3nQq5Fl0HprJAoGBAOPf +MWOTGev61CdODO5KN3pLAoamiPs5lEUlz3kM3L1Q52YLITxNDjRj9hWBUATJZOS2 +pLOoYRwmhD7vrnimMc41+NuuFX+4T7hWPc8uSuOxX0VijYtULyNRK57mncG1Fq9M +RMLbOJ7FD+8jdXNsSMqpQ+pxLJRX/A10O2fOQnbdAoGAL5hV4YWSM0KZHvz332EI +ER0MXiCJN7HkPZMKH0I4eu3m8hEmAyYxVndBnsQ1F37q0xrkqAQ/HTSUntGlS/og +4Bxw5pkCwegoq/77tpto+ExDtSrEitYx4XMmSPyxX4qNULU5m3tzJgUML+b1etwD +Rd2kMU/TC02dq4KBAy/TbRkCgYAl1xN5iJz+XenLGR/2liZ+TWR+/bqzlU006mF4 +pZUmbv/uJxz+yYD5XDwqOA4UrWjuvhG9r9FoflDprp2XdWnB556KxG7XhcDfSJr9 +A5/2DadXe1Ur9O/a+oi2228JEsxQkea9QPA3FVxfBtFjOHEiDlez39VaUP4PMeUH +iO3qlQKBgFQhdTb7HeYnApYIDHLmd1PvjRvp8XKR1CpEN0nkw8HpHcT1q1MUjQCr +iT6FQupULEvGmO3frQsgVeRIQDbEdZK3C5xCtn6qOw70sYATVf361BbTtidmU9yV +THFxwDSVLiVZgFryoY/NtAc27sVdJnGsPRjjaeVgALAsLbmZ1K/H +-----END RSA PRIVATE KEY----- diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.py b/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.py new file mode 100644 index 000000000..d64186987 --- /dev/null +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.py @@ -0,0 +1,30 @@ +import pytest + + +def test_redirect_acme_challenge_location_disabled(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 == 301 + +def test_redirect_acme_challenge_location_enabled(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 == 200 + +def test_noderirect_acme_challenge_location_disabled(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 == 404 + +def test_noderirect_acme_challenge_location_enabled(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 == 200 diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.yml b/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.yml new file mode 100644 index 000000000..3cd4f2d62 --- /dev/null +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.yml @@ -0,0 +1,47 @@ +version: "2" + +services: + 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: "true" + + 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: "true" + + sut: + image: nginxproxy/nginx-proxy:test + environment: + ACME_HTTP_CHALLENGE_LOCATION: "false" + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.py b/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.py new file mode 100644 index 000000000..3b2950fc6 --- /dev/null +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.py @@ -0,0 +1,30 @@ +import pytest + + +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_noderirect_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_noderirect_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 diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.yml b/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.yml new file mode 100644 index 000000000..4d211fc9f --- /dev/null +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.yml @@ -0,0 +1,47 @@ +version: "2" + +services: + 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" + + sut: + image: nginxproxy/nginx-proxy:test + environment: + ACME_HTTP_CHALLENGE_LOCATION: "true" + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.py b/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.py new file mode 100644 index 000000000..a0c73ee76 --- /dev/null +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.py @@ -0,0 +1,16 @@ +import pytest + + +def test_redirect_acme_challenge_location_legacy(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_noderirect_acme_challenge_location_legacy(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 == 404 diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.yml b/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.yml new file mode 100644 index 000000000..d29efbda2 --- /dev/null +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.yml @@ -0,0 +1,26 @@ +version: "2" + +services: + 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" + HTTPS_METHOD: noredirect + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_ssl/test_noredirect.py b/test/test_ssl/test_noredirect.py index 1d956d198..0f50063df 100644 --- a/test/test_ssl/test_noredirect.py +++ b/test/test_ssl/test_noredirect.py @@ -19,9 +19,9 @@ def test_web2_HSTS_policy_is_inactive(docker_compose, nginxproxy): assert "Strict-Transport-Security" not in r.headers -def test_web3_acme_challenge_does_work(docker_compose, nginxproxy, acme_challenge_path): +def test_web3_acme_challenge_does_not_work(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 + assert r.status_code == 404 From 57e86561eb5c76e90f756e8fbf167ccebe872540 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 May 2024 10:28:46 +0200 Subject: [PATCH 36/42] build: bump library/nginx from 1.26.0 to 1.27.0 (#2470) Bumps library/nginx from 1.26.0 to 1.27.0. --- updated-dependencies: - dependency-name: library/nginx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile.alpine | 2 +- Dockerfile.debian | 2 +- README.md | 2 +- test/certs/create_server_certificate.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile.alpine b/Dockerfile.alpine index cca9e3c35..244ecb76c 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -3,7 +3,7 @@ FROM docker.io/nginxproxy/docker-gen:0.14.0 AS docker-gen FROM docker.io/nginxproxy/forego:0.18.1 AS forego # Build the final image -FROM docker.io/library/nginx:1.26.0-alpine +FROM docker.io/library/nginx:1.27.0-alpine ARG NGINX_PROXY_VERSION # Add DOCKER_GEN_VERSION environment variable because diff --git a/Dockerfile.debian b/Dockerfile.debian index 5bad37aa3..d3f5945ce 100644 --- a/Dockerfile.debian +++ b/Dockerfile.debian @@ -3,7 +3,7 @@ FROM docker.io/nginxproxy/docker-gen:0.14.0-debian AS docker-gen FROM docker.io/nginxproxy/forego:0.18.1-debian AS forego # Build the final image -FROM docker.io/library/nginx:1.26.0 +FROM docker.io/library/nginx:1.27.0 ARG NGINX_PROXY_VERSION # Add DOCKER_GEN_VERSION environment variable because diff --git a/README.md b/README.md index 09e81d09f..d29c15d38 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Test](https://github.com/nginx-proxy/nginx-proxy/actions/workflows/test.yml/badge.svg)](https://github.com/nginx-proxy/nginx-proxy/actions/workflows/test.yml) [![GitHub release](https://img.shields.io/github/v/release/nginx-proxy/nginx-proxy)](https://github.com/nginx-proxy/nginx-proxy/releases) -![nginx 1.26.0](https://img.shields.io/badge/nginx-1.26.0-brightgreen.svg) +![nginx 1.27.0](https://img.shields.io/badge/nginx-1.27.0-brightgreen.svg) [![Docker Image Size](https://img.shields.io/docker/image-size/nginxproxy/nginx-proxy?sort=semver)](https://hub.docker.com/r/nginxproxy/nginx-proxy "Click to view the image on Docker Hub") [![Docker stars](https://img.shields.io/docker/stars/nginxproxy/nginx-proxy.svg)](https://hub.docker.com/r/nginxproxy/nginx-proxy "DockerHub") [![Docker pulls](https://img.shields.io/docker/pulls/nginxproxy/nginx-proxy.svg)](https://hub.docker.com/r/nginxproxy/nginx-proxy "DockerHub") diff --git a/test/certs/create_server_certificate.sh b/test/certs/create_server_certificate.sh index 0789a22e7..f9d6b9764 100755 --- a/test/certs/create_server_certificate.sh +++ b/test/certs/create_server_certificate.sh @@ -24,7 +24,7 @@ fi # Create a nginx container (which conveniently provides the `openssl` command) ############################################################################### -CONTAINER=$(docker run -d -v $DIR:/work -w /work -e SAN="$ALTERNATE_DOMAINS" nginx:1.25.3) +CONTAINER=$(docker run -d -v $DIR:/work -w /work -e SAN="$ALTERNATE_DOMAINS" nginx:1.27.0) # Configure openssl docker exec $CONTAINER bash -c ' mkdir -p /ca/{certs,crl,private,newcerts} 2>/dev/null From 7922c925affeedbecb5414d87358719cdeb0f2ff Mon Sep 17 00:00:00 2001 From: Niek <100143256+SchoNie@users.noreply.github.com> Date: Fri, 31 May 2024 17:07:07 +0200 Subject: [PATCH 37/42] chore: spelling fixes for #2468 (#2471) --- docs/README.md | 2 +- .../test_acme_challenge_location_disabled.py | 4 ++-- .../test_acme_challenge_location_enabled.py | 4 ++-- .../test_acme_challenge_location_legacy_is_default.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/README.md b/docs/README.md index 8d7d2f0fe..d1aa75b6e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -424,7 +424,7 @@ If you are running the container in a virtualized environment (Hyper-V, VirtualB By default nginx-proxy generates location blocks to handle ACME HTTP Challenge, excepted when `HTTPS_METHOD=noredirect` or there is no certificate for the domain. Ths behavior can be changed with environment variable `ACME_HTTP_CHALLENGE_LOCATION`. It accepts these values: * `legacy`: default value; current default behavior * `true`: handle ACME HTTP Challenge in all cases -* `false`: do not handle ACME HTTP Chalenge at all. +* `false`: do not handle ACME HTTP Challenge at all. ### Diffie-Hellman Groups diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.py b/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.py index d64186987..acbc8feb9 100644 --- a/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.py +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.py @@ -15,14 +15,14 @@ def test_redirect_acme_challenge_location_enabled(docker_compose, nginxproxy, ac ) assert r.status_code == 200 -def test_noderirect_acme_challenge_location_disabled(docker_compose, nginxproxy, acme_challenge_path): +def test_noredirect_acme_challenge_location_disabled(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 == 404 -def test_noderirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path): +def test_noredirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path): r = nginxproxy.get( f"http://web4.nginx-proxy.tld/{acme_challenge_path}", allow_redirects=False diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.py b/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.py index 3b2950fc6..fd06e847d 100644 --- a/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.py +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.py @@ -15,14 +15,14 @@ def test_redirect_acme_challenge_location_disabled(docker_compose, nginxproxy, a ) assert r.status_code == 301 -def test_noderirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path): +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_noderirect_acme_challenge_location_disabled(docker_compose, nginxproxy, acme_challenge_path): +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 diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.py b/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.py index a0c73ee76..d2051d0aa 100644 --- a/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.py +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.py @@ -8,7 +8,7 @@ def test_redirect_acme_challenge_location_legacy(docker_compose, nginxproxy, acm ) assert r.status_code == 200 -def test_noderirect_acme_challenge_location_legacy(docker_compose, nginxproxy, acme_challenge_path): +def test_noredirect_acme_challenge_location_legacy(docker_compose, nginxproxy, acme_challenge_path): r = nginxproxy.get( f"http://web2.nginx-proxy.tld/{acme_challenge_path}", allow_redirects=False From 8de923fd33479648ad041bf9df215cf0c496e513 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 09:36:25 +0200 Subject: [PATCH 38/42] ci: bump requests from 2.32.2 to 2.32.3 in /test/requirements (#2472) Bumps [requests](https://github.com/psf/requests) from 2.32.2 to 2.32.3. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.2...v2.32.3) --- updated-dependencies: - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/requirements/python-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/requirements/python-requirements.txt b/test/requirements/python-requirements.txt index 4618d1301..c3e2218a1 100644 --- a/test/requirements/python-requirements.txt +++ b/test/requirements/python-requirements.txt @@ -1,4 +1,4 @@ backoff==2.2.1 docker==7.1.0 pytest==8.2.1 -requests==2.32.2 +requests==2.32.3 From 4bd542de99e05de3195b652be06f3854ea19262f Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Wed, 5 Jun 2024 08:23:55 +0200 Subject: [PATCH 39/42] feat: handle acme challenge location by default --- docs/README.md | 9 +++++---- nginx.tmpl | 2 +- ...> test_acme_challenge_location_enabled_is_default.py} | 0 ... test_acme_challenge_location_enabled_is_default.yml} | 2 -- ...default.py => test_acme_challenge_location_legacy.py} | 0 ...fault.yml => test_acme_challenge_location_legacy.yml} | 2 ++ test/test_ssl/test_noredirect.py | 4 ++-- 7 files changed, 10 insertions(+), 9 deletions(-) rename test/test_acme_http_challenge_location/{test_acme_challenge_location_enabled.py => test_acme_challenge_location_enabled_is_default.py} (100%) rename test/test_acme_http_challenge_location/{test_acme_challenge_location_enabled.yml => test_acme_challenge_location_enabled_is_default.yml} (93%) rename test/test_acme_http_challenge_location/{test_acme_challenge_location_legacy_is_default.py => test_acme_challenge_location_legacy.py} (100%) rename test/test_acme_http_challenge_location/{test_acme_challenge_location_legacy_is_default.yml => test_acme_challenge_location_legacy.yml} (89%) diff --git a/docs/README.md b/docs/README.md index d1aa75b6e..33a33ae6a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -421,10 +421,11 @@ If you are running the container in a virtualized environment (Hyper-V, VirtualB [acme-companion](https://github.com/nginx-proxy/acme-companion) is a lightweight companion container for the nginx-proxy. It allows the automated creation/renewal of SSL certificates using the ACME protocol. -By default nginx-proxy generates location blocks to handle ACME HTTP Challenge, excepted when `HTTPS_METHOD=noredirect` or there is no certificate for the domain. Ths behavior can be changed with environment variable `ACME_HTTP_CHALLENGE_LOCATION`. It accepts these values: -* `legacy`: default value; current default behavior -* `true`: handle ACME HTTP Challenge in all cases -* `false`: do not handle ACME HTTP Challenge at all. +By default nginx-proxy generates location blocks to handle ACME HTTP Challenge. Ths behavior can be changed with environment variable `ACME_HTTP_CHALLENGE_LOCATION`. It accepts these values: + +- `true`: default behavior, handle ACME HTTP Challenge in all cases. +- `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`. ### Diffie-Hellman Groups diff --git a/nginx.tmpl b/nginx.tmpl index 4fd611071..85123795a 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -596,7 +596,7 @@ proxy_set_header Proxy ""; {{- end }} {{- $http2_enabled := parseBool (or (first (keys (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http2.enable"))) $globals.Env.ENABLE_HTTP2 "true")}} {{- $http3_enabled := parseBool (or (first (keys (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http3.enable"))) $globals.Env.ENABLE_HTTP3 "false")}} - {{- $acme_http_challenge := or (first (groupByKeys $vhost_containers "Env.ACME_HTTP_CHALLENGE_LOCATION")) $globals.Env.ACME_HTTP_CHALLENGE_LOCATION "legacy" }} + {{- $acme_http_challenge := or (first (groupByKeys $vhost_containers "Env.ACME_HTTP_CHALLENGE_LOCATION")) $globals.Env.ACME_HTTP_CHALLENGE_LOCATION "true" }} {{- $acme_http_challenge_legacy := eq $acme_http_challenge "legacy" }} {{- $acme_http_challenge_enabled := false }} {{- if (not $acme_http_challenge_legacy) }} diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.py b/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled_is_default.py similarity index 100% rename from test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.py rename to test/test_acme_http_challenge_location/test_acme_challenge_location_enabled_is_default.py diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.yml b/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled_is_default.yml similarity index 93% rename from test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.yml rename to test/test_acme_http_challenge_location/test_acme_challenge_location_enabled_is_default.yml index 4d211fc9f..41439e30c 100644 --- a/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled.yml +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled_is_default.yml @@ -39,8 +39,6 @@ services: sut: image: nginxproxy/nginx-proxy:test - environment: - ACME_HTTP_CHALLENGE_LOCATION: "true" volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.py b/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy.py similarity index 100% rename from test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.py rename to test/test_acme_http_challenge_location/test_acme_challenge_location_legacy.py diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.yml b/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy.yml similarity index 89% rename from test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.yml rename to test/test_acme_http_challenge_location/test_acme_challenge_location_legacy.yml index d29efbda2..693f9e013 100644 --- a/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy_is_default.yml +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy.yml @@ -20,6 +20,8 @@ services: sut: image: nginxproxy/nginx-proxy:test + environment: + ACME_HTTP_CHALLENGE_LOCATION: "legacy" volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro diff --git a/test/test_ssl/test_noredirect.py b/test/test_ssl/test_noredirect.py index 0f50063df..1d956d198 100644 --- a/test/test_ssl/test_noredirect.py +++ b/test/test_ssl/test_noredirect.py @@ -19,9 +19,9 @@ def test_web2_HSTS_policy_is_inactive(docker_compose, nginxproxy): assert "Strict-Transport-Security" not in r.headers -def test_web3_acme_challenge_does_not_work(docker_compose, nginxproxy, acme_challenge_path): +def test_web3_acme_challenge_does_work(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 == 404 + assert r.status_code == 200 From 714fa25704a680e9e6aabb54d867a47f9f717a40 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Wed, 5 Jun 2024 08:47:39 +0200 Subject: [PATCH 40/42] style: docs linting --- docs/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/README.md b/docs/README.md index 33a33ae6a..eb27d7709 100644 --- a/docs/README.md +++ b/docs/README.md @@ -579,8 +579,9 @@ _WARNING_: HSTS will force your users to visit the HTTPS version of your site fo ### 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`) and return a 500 error for HTTPS, -* force enable HTTP; i.e. `HTTPS_METHOD` will switch to `noredirect` if it was set to `nohttp` or `redirect`. + +- configure nginx to use the default certificate (`default.crt` with `default.key`) and return a 500 error for HTTPS, +- force enable HTTP; i.e. `HTTPS_METHOD` will switch to `noredirect` if it was set to `nohttp` or `redirect`. If the default certificate is also missing, nginx-proxy will configure nginx to accept HTTPS connections but fail the TLS negotiation. Client browsers will render a TLS error page. As of March 2023, web browsers display the following error messages: From cea905ff884d5b51294cc4fce48ee120c7f6d129 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Wed, 5 Jun 2024 15:55:49 +0200 Subject: [PATCH 41/42] docs: typo Co-authored-by: Niek <100143256+SchoNie@users.noreply.github.com> --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index eb27d7709..218f650c0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -421,7 +421,7 @@ If you are running the container in a virtualized environment (Hyper-V, VirtualB [acme-companion](https://github.com/nginx-proxy/acme-companion) is a lightweight companion container for the nginx-proxy. It allows the automated creation/renewal of SSL certificates using the ACME protocol. -By default nginx-proxy generates location blocks to handle ACME HTTP Challenge. Ths behavior can be changed with environment variable `ACME_HTTP_CHALLENGE_LOCATION`. It accepts these values: +By default nginx-proxy generates location blocks to handle ACME HTTP Challenge. This behavior can be changed with environment variable `ACME_HTTP_CHALLENGE_LOCATION`. It accepts these values: - `true`: default behavior, handle ACME HTTP Challenge in all cases. - `false`: do not handle ACME HTTP Challenge at all. From 60b123d2495c037dc863f8d0c634ae9fa995667a Mon Sep 17 00:00:00 2001 From: Gilles Filippini Date: Mon, 27 May 2024 23:28:57 +0200 Subject: [PATCH 42/42] feat: ENABLE_HTTP_ON_MISSING_CERT variable Default: true --- docs/README.md | 1 + nginx.tmpl | 3 +- test/test_enable_http_on_missing_cert.py | 18 +++++++++ test/test_enable_http_on_missing_cert.yml | 46 +++++++++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 test/test_enable_http_on_missing_cert.py create mode 100644 test/test_enable_http_on_missing_cert.yml diff --git a/docs/README.md b/docs/README.md index 218f650c0..6dea7c8e6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -582,6 +582,7 @@ 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, - 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 accept HTTPS connections but fail the TLS negotiation. Client browsers will render a TLS error page. As of March 2023, web browsers display the following error messages: diff --git a/nginx.tmpl b/nginx.tmpl index 85123795a..a7f4a0c18 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -590,8 +590,9 @@ proxy_set_header Proxy ""; {{- $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") }} {{- /* When the certificate is missing we want to ensure that HTTP is enabled; hence switching from 'nohttp' or 'redirect' to 'noredirect' */}} - {{- if (and (not $cert_ok) (or (eq $https_method "nohttp") (eq $https_method "redirect"))) }} + {{- if (and $enable_http_on_missing_cert (not $cert_ok) (or (eq $https_method "nohttp") (eq $https_method "redirect"))) }} {{- $https_method = "noredirect" }} {{- end }} {{- $http2_enabled := parseBool (or (first (keys (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http2.enable"))) $globals.Env.ENABLE_HTTP2 "true")}} diff --git a/test/test_enable_http_on_missing_cert.py b/test/test_enable_http_on_missing_cert.py new file mode 100644 index 000000000..cdedc8a2c --- /dev/null +++ b/test/test_enable_http_on_missing_cert.py @@ -0,0 +1,18 @@ +import pytest + + +def test_nohttp_missing_cert_disabled(docker_compose, nginxproxy): + r = nginxproxy.get("http://nohttp-missing-cert-disabled.nginx-proxy.tld/", allow_redirects=False) + assert r.status_code == 503 + +def test_nohttp_missing_cert_enabled(docker_compose, nginxproxy): + r = nginxproxy.get("http://nohttp-missing-cert-enabled.nginx-proxy.tld/", allow_redirects=False) + assert r.status_code == 200 + +def test_redirect_missing_cert_disabled(docker_compose, nginxproxy): + r = nginxproxy.get("http://redirect-missing-cert-disabled.nginx-proxy.tld/", allow_redirects=False) + assert r.status_code == 301 + +def test_redirect_missing_cert_enabled(docker_compose, nginxproxy): + r = nginxproxy.get("http://redirect-missing-cert-enabled.nginx-proxy.tld/", allow_redirects=False) + assert r.status_code == 200 diff --git a/test/test_enable_http_on_missing_cert.yml b/test/test_enable_http_on_missing_cert.yml new file mode 100644 index 000000000..1149ef720 --- /dev/null +++ b/test/test_enable_http_on_missing_cert.yml @@ -0,0 +1,46 @@ +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: + ENABLE_HTTP_ON_MISSING_CERT: "false" + + nohttp-missing-cert-disabled: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: nohttp-missing-cert-disabled.nginx-proxy.tld + HTTPS_METHOD: nohttp + + nohttp-missing-cert-enabled: + image: web + expose: + - "82" + environment: + WEB_PORTS: "82" + VIRTUAL_HOST: nohttp-missing-cert-enabled.nginx-proxy.tld + HTTPS_METHOD: nohttp + ENABLE_HTTP_ON_MISSING_CERT: "true" + + redirect-missing-cert-disabled: + image: web + expose: + - "83" + environment: + WEB_PORTS: "83" + VIRTUAL_HOST: redirect-missing-cert-disabled.nginx-proxy.tld + + redirect-missing-cert-enabled: + image: web + expose: + - "84" + environment: + WEB_PORTS: "84" + VIRTUAL_HOST: redirect-missing-cert-enabled.nginx-proxy.tld + ENABLE_HTTP_ON_MISSING_CERT: "true"