From fb7a11212fee18438359e94e6f6d06cfee2980ac Mon Sep 17 00:00:00 2001 From: Laurynas Alekna Date: Mon, 10 May 2021 22:35:53 +0100 Subject: [PATCH] Make server_tokens configurable per virtual-host --- README.md | 4 ++ nginx.tmpl | 17 +++++ test/conftest.py | 25 +++++-- .../web-server-tokens-off.nginx-proxy.tld.crt | 71 +++++++++++++++++++ .../web-server-tokens-off.nginx-proxy.tld.key | 27 +++++++ test/test_headers/test_http.py | 24 ++++++- test/test_headers/test_http.yml | 9 +++ test/test_headers/test_https.py | 25 +++++-- test/test_headers/test_https.yml | 13 ++++ 9 files changed, 204 insertions(+), 11 deletions(-) create mode 100644 test/test_headers/certs/web-server-tokens-off.nginx-proxy.tld.crt create mode 100644 test/test_headers/certs/web-server-tokens-off.nginx-proxy.tld.key diff --git a/README.md b/README.md index af93cdb..9b13463 100644 --- a/README.md +++ b/README.md @@ -421,6 +421,10 @@ If you are using multiple hostnames for a single container (e.g. `VIRTUAL_HOST=e If you want most of your virtual hosts to use a default single `location` block configuration and then override on a few specific ones, add those settings to the `/etc/nginx/vhost.d/default_location` file. This file will be used on any virtual host which does not have a `/etc/nginx/vhost.d/{VIRTUAL_HOST}_location` file associated with it. +#### Per-VIRTUAL_HOST `server_tokens` configuration +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. + + ### Contributing Before submitting pull requests or issues, please check github to make sure an existing issue or pull request is not already open. diff --git a/nginx.tmpl b/nginx.tmpl index d6a1661..9d90241 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -143,6 +143,7 @@ proxy_set_header Proxy ""; {{ $enable_ipv6 := eq (or ($.Env.ENABLE_IPV6) "") "true" }} server { server_name _; # This is just an invalid value which will never trigger on a real hostname. + server_tokens off; listen {{ $external_http_port }}; {{ if $enable_ipv6 }} listen [::]:{{ $external_http_port }}; @@ -154,6 +155,7 @@ server { {{ if (and (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }} server { server_name _; # This is just an invalid value which will never trigger on a real hostname. + server_tokens off; listen {{ $external_https_port }} ssl http2; {{ if $enable_ipv6 }} listen [::]:{{ $external_https_port }} ssl http2; @@ -210,6 +212,9 @@ upstream {{ $upstream_name }} { {{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost, falling back to "http" */}} {{ $proto := trim (or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http") }} +{{/* Get the SERVER_TOKENS defined by containers w/ the same vhost, falling back to "" */}} +{{ $server_tokens := trim (or (first (groupByKeys $containers "Env.SERVER_TOKENS")) "") }} + {{/* 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" }} @@ -246,6 +251,9 @@ upstream {{ $upstream_name }} { {{ if eq $https_method "redirect" }} server { server_name {{ $host }}; + {{ if $server_tokens }} + server_tokens {{ $server_tokens }}; + {{ end }} listen {{ $external_http_port }} {{ $default_server }}; {{ if $enable_ipv6 }} listen [::]:{{ $external_http_port }} {{ $default_server }}; @@ -270,6 +278,9 @@ server { server { server_name {{ $host }}; + {{ if $server_tokens }} + server_tokens {{ $server_tokens }}; + {{ end }} listen {{ $external_https_port }} ssl http2 {{ $default_server }}; {{ if $enable_ipv6 }} listen [::]:{{ $external_https_port }} ssl http2 {{ $default_server }}; @@ -342,6 +353,9 @@ server { server { server_name {{ $host }}; + {{ if $server_tokens }} + server_tokens {{ $server_tokens }}; + {{ end }} listen {{ $external_http_port }} {{ $default_server }}; {{ if $enable_ipv6 }} listen [::]:80 {{ $default_server }}; @@ -387,6 +401,9 @@ server { {{ if (and (not $is_https) (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }} server { server_name {{ $host }}; + {{ if $server_tokens }} + server_tokens {{ $server_tokens }}; + {{ end }} listen {{ $external_https_port }} ssl http2 {{ $default_server }}; {{ if $enable_ipv6 }} listen [::]:{{ $external_https_port }} ssl http2 {{ $default_server }}; diff --git a/test/conftest.py b/test/conftest.py index aa398e6..b738c83 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,17 +1,19 @@ import contextlib import logging import os +import re import shlex import socket import subprocess import time -import re +from typing import List import backoff import docker import pytest import requests from _pytest._code.code import ReprExceptionInfo +from docker.models.containers import Container from requests.packages.urllib3.util.connection import HAS_IPV6 logging.basicConfig(level=logging.INFO) @@ -63,17 +65,32 @@ class requests_for_docker(object): if os.path.isfile(CA_ROOT_CERTIFICATE): self.session.verify = CA_ROOT_CERTIFICATE - def get_conf(self): + @staticmethod + def get_nginx_proxy_containers() -> List[Container]: """ - Return the nginx config file + Return list of containers """ nginx_proxy_containers = docker_client.containers.list(filters={"ancestor": "nginxproxy/nginx-proxy:test"}) if len(nginx_proxy_containers) > 1: pytest.fail("Too many running nginxproxy/nginx-proxy:test containers", pytrace=False) elif len(nginx_proxy_containers) == 0: pytest.fail("No running nginxproxy/nginx-proxy:test container", pytrace=False) + return nginx_proxy_containers + + def get_conf(self): + """ + Return the nginx config file + """ + nginx_proxy_containers = self.get_nginx_proxy_containers() return get_nginx_conf_from_container(nginx_proxy_containers[0]) + def get_ip(self) -> str: + """ + Return the nginx container ip address + """ + nginx_proxy_containers = self.get_nginx_proxy_containers() + return container_ip(nginx_proxy_containers[0]) + def get(self, *args, **kwargs): with ipv6(kwargs.pop('ipv6', False)): @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None) @@ -120,7 +137,7 @@ class requests_for_docker(object): return getattr(requests, name) -def container_ip(container): +def container_ip(container: Container): """ return the IP address of a container. diff --git a/test/test_headers/certs/web-server-tokens-off.nginx-proxy.tld.crt b/test/test_headers/certs/web-server-tokens-off.nginx-proxy.tld.crt new file mode 100644 index 0000000..a96109a --- /dev/null +++ b/test/test_headers/certs/web-server-tokens-off.nginx-proxy.tld.crt @@ -0,0 +1,71 @@ +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: May 11 18:25:49 2021 GMT + Not After : Sep 26 18:25:49 2048 GMT + Subject: CN=web-server-tokens-off.nginx-proxy.tld + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:b4:fa:9d:8a:74:3f:17:ea:99:1c:45:71:18:90: + eb:92:35:38:d7:90:21:81:0a:91:05:41:cf:b5:87: + 34:bd:d8:7b:7f:7d:06:33:f8:94:67:8e:e4:07:54: + 7f:b7:62:c5:76:6c:7f:7c:19:25:19:2c:36:9a:26: + 54:8e:2d:97:02:78:31:c6:13:d3:ad:f3:31:62:e6: + cf:96:ae:63:37:dd:bd:73:cb:4e:fb:3f:9b:65:67: + 97:d8:5a:5d:0e:72:b1:11:ab:0e:d7:23:a9:b7:22: + de:23:74:7e:88:7c:28:98:a9:6e:00:f4:be:8c:69: + ea:3f:33:8b:19:97:da:1b:a6:65:b5:5a:92:01:3c: + 3a:13:6b:00:02:e1:98:78:d3:da:ea:a6:9c:33:b0: + 1d:9f:02:c4:f1:d0:d6:de:7a:f7:42:12:4b:31:fb: + ed:e9:d7:d8:15:e8:4e:18:91:7c:9d:bf:0f:b0:12: + d6:e2:80:8b:7a:ef:17:70:51:f4:3c:b7:43:cb:56: + 61:af:61:7a:4e:9d:6c:5e:d8:27:0c:3b:d7:a4:1d: + 2f:0d:a0:99:8f:b5:71:93:21:b4:87:be:b4:1c:77: + a0:b9:cd:91:bd:9c:d0:b9:81:50:12:63:d2:0a:a9: + 61:05:91:19:27:f7:ea:9d:8e:48:65:2e:1a:e7:fd: + f1:b7 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Alternative Name: + DNS:web-server-tokens-off.nginx-proxy.tld + Signature Algorithm: sha256WithRSAEncryption + 5b:b7:74:ad:07:08:65:3c:8e:02:50:a9:b6:f4:8d:47:95:6f: + e0:ba:5a:8c:ae:5c:32:88:8b:45:04:48:ce:3d:72:45:d7:7e: + 1e:d7:75:17:30:98:90:21:4c:67:e2:57:1d:c9:fa:03:f4:81: + 64:cf:d2:b3:85:71:be:53:b9:2a:fd:89:04:a6:b1:88:0a:0a: + f1:5c:93:9b:fb:4f:86:0e:c5:4d:6a:ff:54:7b:07:f1:7e:d1: + 8a:6b:fa:3b:f3:5c:d2:1b:2c:86:05:4c:e0:b4:04:0d:c7:db: + 0b:89:b4:33:09:b6:1a:f0:cb:d4:ae:2c:05:63:a4:18:19:52: + c7:15:21:ac:ae:9e:15:b9:b0:58:0c:96:df:7b:77:46:ef:59: + a7:96:56:da:f6:f6:81:9f:10:7d:5a:48:68:0c:28:02:5d:7b: + 69:4d:89:41:e2:88:6d:c6:22:45:6a:34:1b:ba:9b:6f:d6:2d: + c2:55:b1:73:b4:bb:f5:06:d6:5f:ed:01:d1:3c:51:8b:e2:6c: + 31:d7:6b:a5:bd:05:e3:9a:97:15:40:bf:bb:8f:81:e5:bf:bc: + 06:66:47:84:fe:f7:06:fb:5d:35:9e:04:26:0d:aa:3d:b5:92: + 6b:90:c2:1c:17:ac:c1:95:d9:6b:f1:5d:0a:09:9f:a7:a6:ca: + 3b:45:a4:59 +-----BEGIN CERTIFICATE----- +MIIDHzCCAgegAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp +bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs +ZDAeFw0yMTA1MTExODI1NDlaFw00ODA5MjYxODI1NDlaMDAxLjAsBgNVBAMMJXdl +Yi1zZXJ2ZXItdG9rZW5zLW9mZi5uZ2lueC1wcm94eS50bGQwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC0+p2KdD8X6pkcRXEYkOuSNTjXkCGBCpEFQc+1 +hzS92Ht/fQYz+JRnjuQHVH+3YsV2bH98GSUZLDaaJlSOLZcCeDHGE9Ot8zFi5s+W +rmM33b1zy077P5tlZ5fYWl0OcrERqw7XI6m3It4jdH6IfCiYqW4A9L6Maeo/M4sZ +l9obpmW1WpIBPDoTawAC4Zh409rqppwzsB2fAsTx0NbeevdCEksx++3p19gV6E4Y +kXydvw+wEtbigIt67xdwUfQ8t0PLVmGvYXpOnWxe2CcMO9ekHS8NoJmPtXGTIbSH +vrQcd6C5zZG9nNC5gVASY9IKqWEFkRkn9+qdjkhlLhrn/fG3AgMBAAGjNDAyMDAG +A1UdEQQpMCeCJXdlYi1zZXJ2ZXItdG9rZW5zLW9mZi5uZ2lueC1wcm94eS50bGQw +DQYJKoZIhvcNAQELBQADggEBAFu3dK0HCGU8jgJQqbb0jUeVb+C6WoyuXDKIi0UE +SM49ckXXfh7XdRcwmJAhTGfiVx3J+gP0gWTP0rOFcb5TuSr9iQSmsYgKCvFck5v7 +T4YOxU1q/1R7B/F+0Ypr+jvzXNIbLIYFTOC0BA3H2wuJtDMJthrwy9SuLAVjpBgZ +UscVIayunhW5sFgMlt97d0bvWaeWVtr29oGfEH1aSGgMKAJde2lNiUHiiG3GIkVq +NBu6m2/WLcJVsXO0u/UG1l/tAdE8UYvibDHXa6W9BeOalxVAv7uPgeW/vAZmR4T+ +9wb7XTWeBCYNqj21kmuQwhwXrMGV2WvxXQoJn6emyjtFpFk= +-----END CERTIFICATE----- diff --git a/test/test_headers/certs/web-server-tokens-off.nginx-proxy.tld.key b/test/test_headers/certs/web-server-tokens-off.nginx-proxy.tld.key new file mode 100644 index 0000000..4e87ba8 --- /dev/null +++ b/test/test_headers/certs/web-server-tokens-off.nginx-proxy.tld.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAtPqdinQ/F+qZHEVxGJDrkjU415AhgQqRBUHPtYc0vdh7f30G +M/iUZ47kB1R/t2LFdmx/fBklGSw2miZUji2XAngxxhPTrfMxYubPlq5jN929c8tO ++z+bZWeX2FpdDnKxEasO1yOptyLeI3R+iHwomKluAPS+jGnqPzOLGZfaG6ZltVqS +ATw6E2sAAuGYeNPa6qacM7AdnwLE8dDW3nr3QhJLMfvt6dfYFehOGJF8nb8PsBLW +4oCLeu8XcFH0PLdDy1Zhr2F6Tp1sXtgnDDvXpB0vDaCZj7VxkyG0h760HHeguc2R +vZzQuYFQEmPSCqlhBZEZJ/fqnY5IZS4a5/3xtwIDAQABAoIBAAaBi/BSRYJimKZ/ +iJVNgGp9J1H4iHvPGW+K8iCgf7Dje20V3Yc4xH0EkgYBb6X0Ew0y0VJwxPimsj/Q +aPHDic446/Em/VEfkQLxMT1Ff6OegRUMlgZKPxfiJX9NoFLIpLzx3VK2oX9H7Zxw +r6vQatUyIhY+tiruE9G51KJS5zBfN388ErfRUI8ByBaDGH0huA6kTBcNffhCfZr5 +9naWSIIcuBe8v7z6nAaeYL00q1q3vuWPmuQduSgsmef7QuN71CIxuOAqXTJl8koS +LYNbj8yvIy3nOF90D+uZD/Pa2Y0kB6aum09hbUP15K0QFKulbKLRQ60IuvRcw3Qv +MM177OECgYEA5Rw3qUcoTDfsx+nu2BxECj62uyNVZfX/QMf7dvzCqjXuOhij+KBB +U9xnNfuLc4HfCXx/rMg5dGExEBbD2iHAo0nvnCSxzLJmF6i66Uves0VWISXcv2Au +L0TWMhhsbDFoqkWuxXr69oNwKyl9yFRFWEY3p3G+aBAEqWZ1lOkU8O0CgYEAyjhC +bN4mJJYhvX+cXhv+89Z+JIDAvtvQ5Vy7kxvhQUTx2By6rWKKrBPdTnzsxBGKqQwv +lXzfgj/MlIr6A6QDReGwU3ZXTJqSGEuT8Ra9SbjczQgaGOrPCrWhnbeZ18iM67pJ +LPfLgdRdkh3XgbOOKcDhpg2KybbbyXx6Q2xb7LMCgYEAzKHKWUh0BreApgIcUSvV +3ayr+zOQ5/Oy24KC6IDTwcFPmNY/RiakkqluCfo1UKKzuj5XrtRa9MaGUs9yeJbi +/zVfbQAdSi4hH4qV/x/Dtiz8w7iUlN3sAk4iXjYQSQZMbKC2fC3ej2VQP0zcypvy +H+j/dnASV9HOyBr6dFlGWfUCgYB3gfYntsXd+2fnQOJdb7glzM5xrjG62dfDpSEp +mGFwHFm8+YWNcF45weeZOhUG7sL+krgQZWMF68RwyQ1mV2ijxPRa7uY63GKYvxmo +cmLdjcXX2gDqVuKTFrJzrgzaTKiTq10RmUQI70N5Ve+FtGLA5D+2zewGt+1+TvVG +oWRWJwKBgAUpJ/NXOB82ie9RtwfAeuiD0yDPM3gNFVe0udAyG/71nXyHiW5aHn/w +H+QSliw7gqir4u6bcrprFQMcwiowtCfeDkcXoQCOBx6TvL2zZTrG7J/68yDHfHGg +w3eFN7ac8FsliRpT+UVKM97zJXcWFkai5Q+R7oKsWXRVXQUZZxg9 +-----END RSA PRIVATE KEY----- diff --git a/test/test_headers/test_http.py b/test/test_headers/test_http.py index 2799262..5983a10 100644 --- a/test/test_headers/test_http.py +++ b/test/test_headers/test_http.py @@ -1,5 +1,3 @@ -import pytest - def test_arbitrary_headers_are_passed_on(docker_compose, nginxproxy): r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'Foo': 'Bar'}) assert r.status_code == 200 @@ -78,4 +76,24 @@ def test_httpoxy_safe(docker_compose, nginxproxy): r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'Proxy': 'tcp://some.hacker.com'}) assert r.status_code == 200 assert "Proxy:" not in r.text - + + +def test_no_host_server_tokens_off(docker_compose, nginxproxy): + ip = nginxproxy.get_ip() + r = nginxproxy.get(f"http://{ip}/headers") + assert r.status_code == 503 + assert r.headers["Server"] == "nginx" + + +def test_server_tokens_on(docker_compose, nginxproxy): + r = nginxproxy.get("http://web.nginx-proxy.tld/headers") + assert r.status_code == 200 + assert "Host: web.nginx-proxy.tld" in r.text + assert r.headers["Server"].startswith("nginx/") + + +def test_server_tokens_off(docker_compose, nginxproxy): + r = nginxproxy.get("http://web-server-tokens-off.nginx-proxy.tld/headers") + assert r.status_code == 200 + assert "Host: web-server-tokens-off.nginx-proxy.tld" in r.text + assert r.headers["Server"] == "nginx" diff --git a/test/test_headers/test_http.yml b/test/test_headers/test_http.yml index f8069c6..0e3880d 100644 --- a/test/test_headers/test_http.yml +++ b/test/test_headers/test_http.yml @@ -6,6 +6,15 @@ web: WEB_PORTS: 80 VIRTUAL_HOST: web.nginx-proxy.tld +web-server-tokens-off: + image: web + expose: + - "80" + environment: + WEB_PORTS: 80 + VIRTUAL_HOST: web-server-tokens-off.nginx-proxy.tld + SERVER_TOKENS: "off" + sut: image: nginxproxy/nginx-proxy:test diff --git a/test/test_headers/test_https.py b/test/test_headers/test_https.py index a1d434a..9aa967a 100644 --- a/test/test_headers/test_https.py +++ b/test/test_headers/test_https.py @@ -1,6 +1,3 @@ -import pytest - - def test_arbitrary_headers_are_passed_on(docker_compose, nginxproxy): r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'Foo': 'Bar'}) assert r.status_code == 200 @@ -79,4 +76,24 @@ def test_httpoxy_safe(docker_compose, nginxproxy): r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'Proxy': 'tcp://some.hacker.com'}) assert r.status_code == 200 assert "Proxy:" not in r.text - + + +def test_no_host_server_tokens_off(docker_compose, nginxproxy): + ip = nginxproxy.get_ip() + r = nginxproxy.get(f"https://{ip}/headers", verify=False) + assert r.status_code == 503 + assert r.headers["Server"] == "nginx" + + +def test_server_tokens_on(docker_compose, nginxproxy): + r = nginxproxy.get("https://web.nginx-proxy.tld/headers", verify=False) + assert r.status_code == 200 + assert "Host: web.nginx-proxy.tld" in r.text + assert r.headers["Server"].startswith("nginx/") + + +def test_server_tokens_off(docker_compose, nginxproxy): + r = nginxproxy.get("https://web-server-tokens-off.nginx-proxy.tld/headers") + assert r.status_code == 200 + assert "Host: web-server-tokens-off.nginx-proxy.tld" in r.text + assert r.headers["Server"] == "nginx" diff --git a/test/test_headers/test_https.yml b/test/test_headers/test_https.yml index 406e433..c0c67b4 100644 --- a/test/test_headers/test_https.yml +++ b/test/test_headers/test_https.yml @@ -6,11 +6,24 @@ web: WEB_PORTS: 80 VIRTUAL_HOST: web.nginx-proxy.tld +web-server-tokens-off: + image: web + expose: + - "80" + environment: + WEB_PORTS: 80 + VIRTUAL_HOST: web-server-tokens-off.nginx-proxy.tld + SERVER_TOKENS: "off" + sut: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro + - ./certs/web.nginx-proxy.tld.crt:/etc/nginx/certs/default.crt:ro + - ./certs/web.nginx-proxy.tld.key:/etc/nginx/certs/default.key:ro - ./certs/web.nginx-proxy.tld.crt:/etc/nginx/certs/web.nginx-proxy.tld.crt:ro - ./certs/web.nginx-proxy.tld.key:/etc/nginx/certs/web.nginx-proxy.tld.key:ro + - ./certs/web-server-tokens-off.nginx-proxy.tld.crt:/etc/nginx/certs/web-server-tokens-off.nginx-proxy.tld.crt:ro + - ./certs/web-server-tokens-off.nginx-proxy.tld.key:/etc/nginx/certs/web-server-tokens-off.nginx-proxy.tld.key:ro - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro