diff --git a/.travis.yml b/.travis.yml index e850f08..5386261 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,13 +4,13 @@ services: env: global: - - DOCKER_VERSION=1.12.1-0~trusty + - DOCKER_VERSION=1.12.3-0~trusty before_install: # list docker-engine versions - apt-cache madison docker-engine # upgrade docker-engine to specific version - - sudo apt-get -o Dpkg::Options::="--force-confnew" install -y docker-engine=${DOCKER_VERSION} + - sudo apt-get -o Dpkg::Options::="--force-confnew" install -y --force-yes docker-engine=${DOCKER_VERSION} - docker version - docker info - sudo add-apt-repository ppa:duggan/bats --yes diff --git a/Dockerfile b/Dockerfile index 6d5ce9b..afa564c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM nginx:1.11.3 +FROM nginx:1.11.6 MAINTAINER Jason Wilder mail@jasonwilder.com # Install wget and install/updates certificates diff --git a/README.md b/README.md index e6e5404..1ff2ef2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![nginx 1.11.3](https://img.shields.io/badge/nginx-1.11.3-brightgreen.svg) ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg) [![Build Status](https://travis-ci.org/jwilder/nginx-proxy.svg?branch=master)](https://travis-ci.org/jwilder/nginx-proxy) [![](https://img.shields.io/docker/stars/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub') [![](https://img.shields.io/docker/pulls/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub') +![nginx 1.11.6](https://img.shields.io/badge/nginx-1.11.6-brightgreen.svg) ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg) [![Build Status](https://travis-ci.org/jwilder/nginx-proxy.svg?branch=master)](https://travis-ci.org/jwilder/nginx-proxy) [![](https://img.shields.io/docker/stars/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub') [![](https://img.shields.io/docker/pulls/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub') nginx-proxy sets up a container running nginx and [docker-gen][1]. docker-gen generates reverse proxy configs for nginx and reloads nginx when containers are started and stopped. @@ -42,7 +42,7 @@ services: ```shell $ docker-compose up $ curl -H "Host: whoami.local" localhost -I''m 5b129ab83266 +I'm 5b129ab83266 ``` ### Multiple Ports @@ -76,7 +76,13 @@ In this example, the `my-nginx-proxy` container will be connected to `my-network ### SSL Backends -If you would like to connect to your backend using HTTPS instead of HTTP, set `VIRTUAL_PROTO=https` on the backend container. +If you would like the reverse proxy to connect to your backend using HTTPS instead of HTTP, set `VIRTUAL_PROTO=https` on the backend container. + +### uWSGI Backends + +If you would like to connect to uWSGI backend, set `VIRTUAL_PROTO=uwsgi` on the +backend container. Your backend container should than listen on a port rather +than a socket and expose that port. ### Default Host @@ -92,6 +98,14 @@ image and the official [nginx](https://registry.hub.docker.com/_/nginx/) image. You may want to do this to prevent having the docker socket bound to a publicly exposed container service. +You can demo this pattern with docker-compose: + +```console +$ docker-compose --file docker-compose-separate-containers.yml up +$ curl -H "Host: whoami.local" localhost +I'm 5b129ab83266 +``` + To run nginx proxy as a separate container you'll need to have [nginx.tmpl](https://github.com/jwilder/nginx-proxy/blob/master/nginx.tmpl) on your host system. First start nginx with a volume: @@ -126,6 +140,10 @@ hosts in use. The certificate and keys should be named after the virtual host w `.key` extension. For example, a container with `VIRTUAL_HOST=foo.bar.com` should have a `foo.bar.com.crt` and `foo.bar.com.key` file in the certs directory. +If you are running the container in a virtualized environment (Hyper-V, VirtualBox, etc...), +/path/to/certs must exist in that environment or be made accessible to that environment. +By default, Docker is not able to mount directories on the host machine to containers running in a virtual machine. + #### Diffie-Hellman Groups If you have Diffie-Hellman groups enabled, the files should be named after the virtual host with a @@ -205,6 +223,7 @@ proxy_set_header Connection $proxy_connection; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; +proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; # Mitigate httpoxy attack (see README for details) proxy_set_header Proxy ""; diff --git a/docker-compose-separate-containers.yml b/docker-compose-separate-containers.yml new file mode 100644 index 0000000..a4edb94 --- /dev/null +++ b/docker-compose-separate-containers.yml @@ -0,0 +1,23 @@ +version: '2' +services: + nginx: + image: nginx + container_name: nginx + ports: + - "80:80" + volumes: + - /etc/nginx/conf.d + + dockergen: + image: jwilder/docker-gen + command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf + volumes_from: + - nginx + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl + + whoami: + image: jwilder/whoami + environment: + - VIRTUAL_HOST=whoami.local diff --git a/nginx.tmpl b/nginx.tmpl index 612decb..20688da 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -24,6 +24,13 @@ map $http_x_forwarded_proto $proxy_x_forwarded_proto { '' $scheme; } +# If we receive X-Forwarded-Port, pass it through; otherwise, pass along the +# server port the client connected to +map $http_x_forwarded_port $proxy_x_forwarded_port { + default $http_x_forwarded_port; + '' $server_port; +} + # If we receive Upgrade, set Connection to "upgrade"; otherwise, delete any # Connection header that may have been passed to this server map $http_upgrade $proxy_connection { @@ -51,6 +58,7 @@ proxy_set_header Connection $proxy_connection; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; +proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; # Mitigate httpoxy attack (see README for details) proxy_set_header Proxy ""; @@ -70,6 +78,7 @@ server { access_log /var/log/nginx/access.log vhost; return 503; + ssl_session_tickets off; ssl_certificate /etc/nginx/certs/default.crt; ssl_certificate_key /etc/nginx/certs/default.key; } @@ -118,8 +127,8 @@ upstream {{ $host }} { {{ $vhostCert := (closest (dir "/etc/nginx/certs") (printf "%s.crt" $host))}} {{/* vhostCert is actually a filename so remove any suffixes since they are added later */}} -{{ $vhostCert := replace $vhostCert ".crt" "" -1 }} -{{ $vhostCert := replace $vhostCert ".key" "" -1 }} +{{ $vhostCert := trimSuffix ".crt" $vhostCert }} +{{ $vhostCert := trimSuffix ".key" $vhostCert }} {{/* Use the cert specified on the container or fallback to the best vhost match */}} {{ $cert := (coalesce $certName $vhostCert) }} @@ -158,7 +167,7 @@ server { {{ end }} {{ if (ne $https_method "noredirect") }} - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header Strict-Transport-Security "max-age=31536000"; {{ end }} {{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }} @@ -168,7 +177,12 @@ server { {{ end }} location / { + {{ if eq $proto "uwsgi" }} + include uwsgi_params; + uwsgi_pass {{ trim $proto }}://{{ trim $host }}; + {{ else }} proxy_pass {{ trim $proto }}://{{ trim $host }}; + {{ end }} {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }} auth_basic "Restricted {{ $host }}"; auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }}; @@ -197,7 +211,12 @@ server { {{ end }} location / { + {{ if eq $proto "uwsgi" }} + include uwsgi_params; + uwsgi_pass {{ trim $proto }}://{{ trim $host }}; + {{ else }} proxy_pass {{ trim $proto }}://{{ trim $host }}; + {{ end }} {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }} auth_basic "Restricted {{ $host }}"; auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }}; diff --git a/test/docker.bats b/test/docker.bats index fc10226..0569dcb 100644 --- a/test/docker.bats +++ b/test/docker.bats @@ -111,13 +111,13 @@ function assert_nginxproxy_behaves { assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' # Querying the proxy with Host header → 200 - run curl_container $container /data --header "Host: web1.bats" + run curl_container $container /port --header "Host: web1.bats" assert_output "answer from port 81" - run curl_container $container /data --header "Host: web2.bats" + run curl_container $container /port --header "Host: web2.bats" assert_output "answer from port 82" # Querying the proxy with unknown Host header → 503 - run curl_container $container /data --header "Host: webFOO.bats" --head + run curl_container $container /port --header "Host: webFOO.bats" --head assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' } diff --git a/test/headers.bats b/test/headers.bats new file mode 100644 index 0000000..bc401fd --- /dev/null +++ b/test/headers.bats @@ -0,0 +1,139 @@ +#!/usr/bin/env bats +load test_helpers +SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE} + +function setup { + # make sure to stop any web container before each test so we don't + # have any unexpected container running with VIRTUAL_HOST or VIRUTAL_PORT set + stop_bats_containers web +} + + +@test "[$TEST_FILE] start a nginx-proxy container" { + # GIVEN + run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro + assert_success + docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events" +} + +@test "[$TEST_FILE] nginx-proxy passes arbitrary header" { + # WHEN + prepare_web_container bats-host-1 80 -e VIRTUAL_HOST=web.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-host-1 + sleep 1 + + # THEN + run curl_container $SUT_CONTAINER /headers -H "Foo: Bar" -H "Host: web.bats" + assert_output -l 'Foo: Bar' +} + +##### Testing the handling of X-Forwarded-For ##### + +@test "[$TEST_FILE] nginx-proxy generates X-Forwarded-For" { + # WHEN + prepare_web_container bats-host-2 80 -e VIRTUAL_HOST=web.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-host-2 + sleep 1 + + # THEN + run curl_container $SUT_CONTAINER /headers -H "Host: web.bats" + assert_output -p 'X-Forwarded-For:' +} + +@test "[$TEST_FILE] nginx-proxy passes X-Forwarded-For" { + # WHEN + prepare_web_container bats-host-3 80 -e VIRTUAL_HOST=web.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-host-3 + sleep 1 + + # THEN + run curl_container $SUT_CONTAINER /headers -H "X-Forwarded-For: 1.2.3.4" -H "Host: web.bats" + assert_output -p 'X-Forwarded-For: 1.2.3.4, ' +} + +##### Testing the handling of X-Forwarded-Proto ##### + +@test "[$TEST_FILE] nginx-proxy generates X-Forwarded-Proto" { + # WHEN + prepare_web_container bats-host-4 80 -e VIRTUAL_HOST=web.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-host-4 + sleep 1 + + # THEN + run curl_container $SUT_CONTAINER /headers -H "Host: web.bats" + assert_output -l 'X-Forwarded-Proto: http' +} + +@test "[$TEST_FILE] nginx-proxy passes X-Forwarded-Proto" { + # WHEN + prepare_web_container bats-host-5 80 -e VIRTUAL_HOST=web.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-host-5 + sleep 1 + + # THEN + run curl_container $SUT_CONTAINER /headers -H "X-Forwarded-Proto: https" -H "Host: web.bats" + assert_output -l 'X-Forwarded-Proto: https' +} + +##### Testing the handling of X-Forwarded-Port ##### + +@test "[$TEST_FILE] nginx-proxy generates X-Forwarded-Port" { + # WHEN + prepare_web_container bats-host-6 80 -e VIRTUAL_HOST=web.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-host-6 + sleep 1 + + # THEN + run curl_container $SUT_CONTAINER /headers -H "Host: web.bats" + assert_output -l 'X-Forwarded-Port: 80' +} + +@test "[$TEST_FILE] nginx-proxy passes X-Forwarded-Port" { + # WHEN + prepare_web_container bats-host-7 80 -e VIRTUAL_HOST=web.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-host-7 + sleep 1 + + # THEN + run curl_container $SUT_CONTAINER /headers -H "X-Forwarded-Port: 1234" -H "Host: web.bats" + assert_output -l 'X-Forwarded-Port: 1234' +} + +##### Other headers + +@test "[$TEST_FILE] nginx-proxy generates X-Real-IP" { + # WHEN + prepare_web_container bats-host-8 80 -e VIRTUAL_HOST=web.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-host-8 + sleep 1 + + # THEN + run curl_container $SUT_CONTAINER /headers -H "Host: web.bats" + assert_output -p 'X-Real-IP: ' +} + +@test "[$TEST_FILE] nginx-proxy passes Host" { + # WHEN + prepare_web_container bats-host-9 80 -e VIRTUAL_HOST=web.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-host-9 + sleep 1 + + # THEN + run curl_container $SUT_CONTAINER /headers -H "Host: web.bats" + assert_output -l 'Host: web.bats' +} + +@test "[$TEST_FILE] nginx-proxy supresses Proxy for httpoxy protection" { + # WHEN + prepare_web_container bats-host-10 80 -e VIRTUAL_HOST=web.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-host-10 + sleep 1 + + # THEN + run curl_container $SUT_CONTAINER /headers -H "Proxy: tcp://foo.com" -H "Host: web.bats" + refute_output -l 'Proxy: tcp://foo.com' +} + +@test "[$TEST_FILE] stop all bats containers" { + stop_bats_containers +} diff --git a/test/multiple-hosts.bats b/test/multiple-hosts.bats index 10487ae..8e14c11 100644 --- a/test/multiple-hosts.bats +++ b/test/multiple-hosts.bats @@ -26,15 +26,15 @@ function setup { assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' # THEN querying the proxy with unknown Host header → 503 - run curl_container $SUT_CONTAINER /data --header "Host: webFOO.bats" --head + run curl_container $SUT_CONTAINER /port --header "Host: webFOO.bats" --head assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' # THEN - run curl_container $SUT_CONTAINER /data --header 'Host: multiple-hosts-1-A.bats' + run curl_container $SUT_CONTAINER /port --header 'Host: multiple-hosts-1-A.bats' assert_output "answer from port 80" # THEN - run curl_container $SUT_CONTAINER /data --header 'Host: multiple-hosts-1-B.bats' + run curl_container $SUT_CONTAINER /port --header 'Host: multiple-hosts-1-B.bats' assert_output "answer from port 80" } diff --git a/test/multiple-ports.bats b/test/multiple-ports.bats index a3c6fd0..f3e670b 100644 --- a/test/multiple-ports.bats +++ b/test/multiple-ports.bats @@ -58,7 +58,7 @@ function setup { # $1 port we are expecting an response from function assert_response_is_from_port { local -r port=$1 - run curl_container $SUT_CONTAINER /data --header "Host: web.bats" + run curl_container $SUT_CONTAINER /port --header "Host: web.bats" assert_output "answer from port $port" } diff --git a/test/test_helpers.bash b/test/test_helpers.bash index 9b35b3c..0fd9532 100644 --- a/test/test_helpers.bash +++ b/test/test_helpers.bash @@ -124,6 +124,7 @@ function prepare_web_container { --name $container_name \ $expose_option \ -w /var/www/ \ + -v $DIR/web_helpers:/var/www:ro \ $options \ -e PYTHON_PORTS="$ports" \ python:3 bash -c " @@ -131,10 +132,7 @@ function prepare_web_container { declare -a PIDS for port in \$PYTHON_PORTS; do echo starting a web server listening on port \$port; - mkdir /var/www/\$port - cd /var/www/\$port - echo \"answer from port \$port\" > data - python -m http.server \$port & + ./webserver.py \$port & PIDS+=(\$!) done wait \${PIDS[@]} @@ -146,7 +144,7 @@ function prepare_web_container { # THEN querying directly port works IFS=$' \t\n' # See https://github.com/sstephenson/bats/issues/89 for port in $ports; do - run retry 5 1s docker run --label bats-type="curl" appropriate/curl --silent --fail http://$(docker_ip $container_name):$port/data + run retry 5 1s docker run --label bats-type="curl" appropriate/curl --silent --fail http://$(docker_ip $container_name):$port/port assert_output "answer from port $port" done } diff --git a/test/web_helpers/webserver.py b/test/web_helpers/webserver.py new file mode 100755 index 0000000..d94ed89 --- /dev/null +++ b/test/web_helpers/webserver.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +import os, sys +import http.server +import socketserver + +class BatsHandler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + root = os.getcwd() + + self.send_response(200) + self.send_header("Content-Type", "text/plain") + self.end_headers() + + if self.path == "/headers": + self.wfile.write(self.headers.as_string().encode()) + elif self.path == "/port": + response = "answer from port %s\n" % PORT + self.wfile.write(response.encode()) + else: + self.wfile.write("No route for this path!\n".encode()) + +if __name__ == '__main__': + PORT = int(sys.argv[1]) + socketserver.TCPServer.allow_reuse_address = True + httpd = socketserver.TCPServer(('0.0.0.0', PORT), BatsHandler) + httpd.serve_forever()