Parcourir la source

Make server_tokens configurable per virtual-host

Laurynas Alekna il y a 4 ans
Parent
commit
fb7a11212f

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

+ 17 - 0
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 }};

+ 21 - 4
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.
 

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

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

+ 21 - 3
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"

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

+ 21 - 4
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"

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