DEV Community

Florian Pitance
Florian Pitance

Posted on

bunkerized-nginx - make your web apps and APIs secured by default

Why bunkerized-nginx ?

Avoid the hassle of following security best practices each time you need a web server or reverse proxy. Bunkerized-nginx provides generic security configs, settings and tools so you don't need to do it yourself.

Non-exhaustive list of features :

  • HTTPS support with transparent Let's Encrypt automation
  • State-of-the-art web security : HTTP security headers, prevent leaks, TLS hardening, ...
  • Integrated ModSecurity WAF with the OWASP Core Rule Set
  • Automatic ban of strange behaviors with fail2ban
  • Antibot challenge through cookie, javascript, captcha or recaptcha v3
  • Block TOR, proxies, bad user-agents, countries, ...
  • Block known bad IP with DNSBL and CrowdSec
  • Prevent bruteforce attacks with rate limiting
  • Detect bad files with ClamAV
  • Easy to configure with environment variables or web UI
  • Automatic configuration with container labels
  • Docker Swarm support

Fooling automated tools/scanners :

Quickstart guide

Run HTTP server with default settings

docker run -p 80:8080 -v /path/to/web/files:/www:ro bunkerity/bunkerized-nginx
Enter fullscreen mode Exit fullscreen mode

Web files are stored in the /www directory, the container will serve files from there. Please note that bunkerized-nginx doesn't run as root but with an unprivileged user with UID/GID 101 therefore you should set the rights of /path/to/web/files accordingly.

In combination with PHP

docker network create mynet
docker run --network mynet \
           -p 80:8080 \
           -v /path/to/web/files:/www:ro \
           -e REMOTE_PHP=myphp \
           -e REMOTE_PHP_PATH=/app \
           bunkerity/bunkerized-nginx
docker run --network mynet \
           --name myphp \
           -v /path/to/web/files:/app \
           php:fpm
Enter fullscreen mode Exit fullscreen mode

The REMOTE_PHP environment variable lets you define the address of a remote PHP-FPM instance that will execute the .php files. REMOTE_PHP_PATH must be set to the directory where the PHP container will find the files.

Run HTTPS server with automated Let's Encrypt

docker run -p 80:8080 \
           -p 443:8443 \
           -v /path/to/web/files:/www:ro \
           -v /where/to/save/certificates:/etc/letsencrypt \
           -e SERVER_NAME=www.yourdomain.com \
           -e AUTO_LETS_ENCRYPT=yes \
           -e REDIRECT_HTTP_TO_HTTPS=yes \
           bunkerity/bunkerized-nginx
Enter fullscreen mode Exit fullscreen mode

Certificates are stored in the /etc/letsencrypt directory, you should save it on your local drive. Please note that bunkerized-nginx doesn't run as root but with an unprivileged user with UID/GID 101 therefore you should set the rights of /where/to/save/certificates accordingly.

If you don't want your webserver to listen on HTTP add the environment variable LISTEN_HTTP with a no value (e.g. HTTPS only). But Let's Encrypt needs the port 80 to be opened so redirecting the port is mandatory.

Here you have three environment variables :

  • SERVER_NAME : define the FQDN of your webserver, this is mandatory for Let's Encrypt (www.yourdomain.com should point to your IP address)
  • AUTO_LETS_ENCRYPT : enable automatic Let's Encrypt creation and renewal of certificates
  • REDIRECT_HTTP_TO_HTTPS : enable HTTP to HTTPS redirection

As a reverse proxy

docker run -p 80:8080 \
           -e USE_REVERSE_PROXY=yes \
           -e REVERSE_PROXY_URL=/ \
           -e REVERSE_PROXY_HOST=http://myserver:8080 \
           bunkerity/bunkerized-nginx
Enter fullscreen mode Exit fullscreen mode

This is a simple reverse proxy to a unique application. If you have more than one application you can add more REVERSE_PROXY_URL/REVERSE_PROXY_HOST by appending a suffix number like this :

docker run -p 80:8080 \
           -e USE_REVERSE_PROXY=yes \
           -e REVERSE_PROXY_URL_1=/app1/ \
           -e REVERSE_PROXY_HOST_1=http://myapp1:3000/ \
           -e REVERSE_PROXY_URL_2=/app2/ \
           -e REVERSE_PROXY_HOST_2=http://myapp2:3000/ \
           bunkerity/bunkerized-nginx
Enter fullscreen mode Exit fullscreen mode

Behind a reverse proxy

docker run -p 80:8080 \
           -v /path/to/web/files:/www \
           -e PROXY_REAL_IP=yes \
           bunkerity/bunkerized-nginx
Enter fullscreen mode Exit fullscreen mode

The PROXY_REAL_IP environment variable, when set to yes, activates the ngx_http_realip_module to get the real client IP from the reverse proxy.

See this section if you need to tweak some values (trusted ip/network, header, ...).

Multisite

By default, bunkerized-nginx will only create one server block. When setting the MULTISITE environment variable to yes, one server block will be created for each host defined in the SERVER_NAME environment variable.

You can set/override values for a specific server by prefixing the environment variable with one of the server name previously defined.

docker run -p 80:8080 \
           -p 443:8443 \
           -v /where/to/save/certificates:/etc/letsencrypt \
           -e SERVER_NAME=app1.domain.com app2.domain.com \
           -e MULTISITE=yes \
           -e AUTO_LETS_ENCRYPT=yes \
           -e REDIRECT_HTTP_TO_HTTPS=yes \
           -e USE_REVERSE_PROXY=yes \
           -e app1.domain.com_REVERSE_PROXY_URL=/ \
           -e app1.domain.com_REVERSE_PROXY_HOST=http://myapp1:8000 \
           -e app2.domain.com_REVERSE_PROXY_URL=/ \
           -e app2.domain.com_REVERSE_PROXY_HOST=http://myapp2:8000 \
           bunkerity/bunkerized-nginx
Enter fullscreen mode Exit fullscreen mode

The USE_REVERSE_PROXY is a global variable that will be applied to each server block. Whereas the app1.domain.com_* and app2.domain.com_* will only be applied to the app1.domain.com and app2.domain.com server block respectively.

When serving files, the web root directory should contains subdirectories named as the servers defined in the SERVER_NAME environment variable. Here is an example :


docker run -p 80:8080 \
           -p 443:8443 \
           -v /where/to/save/certificates:/etc/letsencrypt \
           -v /where/are/web/files:/www:ro \
           -e SERVER_NAME=app1.domain.com app2.domain.com \
           -e MULTISITE=yes \
           -e AUTO_LETS_ENCRYPT=yes \
           -e REDIRECT_HTTP_TO_HTTPS=yes \
           -e app1.domain.com_REMOTE_PHP=php1 \
           -e app1.domain.com_REMOTE_PHP_PATH=/app \
           -e app2.domain.com_REMOTE_PHP=php2 \
           -e app2.domain.com_REMOTE_PHP_PATH=/app \
           bunkerity/bunkerized-nginx
Enter fullscreen mode Exit fullscreen mode

The /where/are/web/files directory should have a structure like this :

/where/are/web/files
├── app1.domain.com
│   └── index.php
│   └── ...
└── app2.domain.com
    └── index.php
    └── ...
Enter fullscreen mode Exit fullscreen mode

Automatic configuration

The downside of using environment variables is that you need to recreate a new container each time you want to add or remove a web service. An alternative is to use the bunkerized-nginx-autoconf image which listens for Docker events and "automagically" generates the configuration.

First we need a volume that will store the configurations :

docker volume create nginx_conf
Enter fullscreen mode Exit fullscreen mode

Then we run bunkerized-nginx with the bunkerized-nginx.AUTOCONF label, mount the created volume at /etc/nginx and set some default configurations for our services (e.g. : automatic Let's Encrypt and HTTP to HTTPS redirect) :

docker network create mynet

docker run -p 80:8080 \
           -p 443:8443 \
           --network mynet \
           -v /where/to/save/certificates:/etc/letsencrypt \
           -v /where/are/web/files:/www:ro \
           -v nginx_conf:/etc/nginx \
           -e SERVER_NAME= \
           -e MULTISITE=yes \
           -e AUTO_LETS_ENCRYPT=yes \
           -e REDIRECT_HTTP_TO_HTTPS=yes \
           -l bunkerized.nginx.AUTOCONF \
           bunkerity/bunkerized-nginx
Enter fullscreen mode Exit fullscreen mode

When setting SERVER_NAME to nothing bunkerized-nginx won't create any server block (in case we only want automatic configuration).

Once bunkerized-nginx is created, let's setup the autoconf container :

docker run -v /var/run/docker.sock:/var/run/docker.sock:ro \
           -v nginx_conf:/etc/nginx \
           bunkerity/bunkerized-nginx-autoconf
Enter fullscreen mode Exit fullscreen mode

We can now create a new container and use labels to dynamically configure bunkerized-nginx. Labels for automatic configuration are the same as environment variables but with the "bunkerized-nginx." prefix.

Here is a PHP example :

docker run --network mynet \
           --name myapp \
           -v /where/are/web/files/app.domain.com:/app \
           -l bunkerized-nginx.SERVER_NAME=app.domain.com \
           -l bunkerized-nginx.REMOTE_PHP=myapp \
           -l bunkerized-nginx.REMOTE_PHP_PATH=/app \
           php:fpm
Enter fullscreen mode Exit fullscreen mode

And a reverse proxy example :

docker run --network mynet \
           --name anotherapp \
           -l bunkerized-nginx.SERVER_NAME=app2.domain.com \
           -l bunkerized-nginx.USE_REVERSE_PROXY=yes \
           -l bunkerized-nginx.REVERSE_PROXY_URL=/ \
           -l bunkerized-nginx.REVERSE_PROXY_HOST=http://anotherapp
           tutum/hello-world
Enter fullscreen mode Exit fullscreen mode

Swarm mode

Automatic configuration through labels is also supported in swarm mode. The bunkerized-nginx-autoconf is used to listen for Swarm events (e.g. service create/rm) and "automagically" edit configurations files and reload nginx.

As a use case we will assume the following :

  • Some managers are also workers (they will only run the autoconf container for obvious security reasons)
  • The bunkerized-nginx service will be deployed on all workers (global mode) so clients can connect to each of them (e.g. load balancing, CDN, edge proxy, ...)
  • There is a shared folder mounted on managers and workers (e.g. NFS, GlusterFS, CephFS, ...)

Let's start by creating the network to allow communications between our services :

docker network create -d overlay mynet
Enter fullscreen mode Exit fullscreen mode

We can now create the autoconf service that will listen to swarm events :

docker service create --name autoconf \
                      --network mynet \
                      --mount type=bind,source=/var/run/docker.sock,destination=/var/run/docker.sock,ro \
                      --mount type=bind,source=/shared/confs,destination=/etc/nginx \
                      --mount type=bind,source=/shared/letsencrypt,destination=/etc/letsencrypt \
                      --mount type=bind,source=/shared/acme-challenge,destination=/acme-challenge \
                      -e SWARM_MODE=yes \
                      -e API_URI=/ChangeMeToSomethingHardToGuess \
                      --replicas 1 \
                      --constraint node.role==manager \
                      bunkerity/bunkerized-nginx-autoconf
Enter fullscreen mode Exit fullscreen mode

You need to change API_URI to something hard to guess since there is no other security mechanism to protect the API at the moment.

When autoconf is created, it's time for the bunkerized-nginx service to be up :

docker service create --name nginx \
                      --network mynet \
                      -p published=80,target=8080,mode=host \
                      -p published=443,target=8443,mode=host \
                      --mount type=bind,source=/shared/confs,destination=/etc/nginx \
                      --mount type=bind,source=/shared/letsencrypt,destination=/etc/letsencrypt,ro \
                      --mount type=bind,source=/shared/acme-challenge,destination=/acme-challenge,ro \
                      --mount type=bind,source=/shared/www,destination=/www,ro \
                      -e SWARM_MODE=yes \
                      -e USE_API=yes \
                      -e API_URI=/ChangeMeToSomethingHardToGuess \
                      -e MULTISITE=yes \
                      -e SERVER_NAME= \
                      -e AUTO_LETS_ENCRYPT=yes \
                      -e REDIRECT_HTTP_TO_HTTPS=yes \
                      -l bunkerized-nginx.AUTOCONF \
                      --mode global \
                      --constraint node.role==worker \
                      bunkerity/bunkerized-nginx
Enter fullscreen mode Exit fullscreen mode

The API_URI value must be the same as the one specified for the autoconf service.

We can now create a new service and use labels to dynamically configure bunkerized-nginx. Labels for automatic configuration are the same as environment variables but with the "bunkerized-nginx." prefix.

Here is a PHP example :

docker service create --name myapp \
                      --network mynet \
                      --mount type=bind,source=/shared/www/app.domain.com,destination=/app \
                      -l bunkerized-nginx.SERVER_NAME=app.domain.com \
                      -l bunkerized-nginx.REMOTE_PHP=myapp \
                      -l bunkerized-nginx.REMOTE_PHP_PATH=/app \
                      --constraint node.role==worker \
                      php:fpm
Enter fullscreen mode Exit fullscreen mode

And a reverse proxy example :

docker service create --name anotherapp \
                      --network mynet \
                      -l bunkerized-nginx.SERVER_NAME=app2.domain.com \
                      -l bunkerized-nginx.USE_REVERSE_PROXY=yes \
                      -l bunkerized-nginx.REVERSE_PROXY_URL=/ \
                      -l bunkerized-nginx.REVERSE_PROXY_HOST=http://anotherapp \
                      --constraint node.role==worker \
                      tutum/hello-world
Enter fullscreen mode Exit fullscreen mode

Web UI

This feature exposes, for now, a security risk because you need to mount the docker socket inside a container exposing a web application. You can test it but you should not use it in servers facing the internet.

A dedicated image, bunkerized-nginx-ui, lets you manage bunkerized-nginx instances and services configurations through a web user interface. This feature is still in beta, feel free to open a new issue if you find a bug and/or you have an idea to improve it.

First we need a volume that will store the configurations :

docker volume create nginx_conf
Enter fullscreen mode Exit fullscreen mode

Then, we can create the bunkerized-nginx instance with the bunkerized-nginx.UI label and a reverse proxy configuration for our web UI :

docker network create mynet

docker run -p 80:8080 \
           -p 443:8443 \
           --network mynet \
           -v nginx_conf:/etc/nginx \
           -v /where/are/web/files:/www:ro \
           -v /where/to/save/certificates:/etc/letsencrypt \
           -e SERVER_NAME=admin.domain.com \
           -e MULTISITE=yes \
           -e AUTO_LETS_ENCRYPT=yes \
           -e REDIRECT_HTTP_TO_HTTPS=yes \
           -e DISABLE_DEFAULT_SERVER=yes \
           -e admin.domain.com_SERVE_FILES=no \
           -e admin.domain.com_USE_AUTH_BASIC=yes \
           -e admin.domain.com_AUTH_BASIC_USER=admin \
           -e admin.domain.com_AUTH_BASIC_PASSWORD=password \
           -e admin.domain.com_USE_REVERSE_PROXY=yes \
           -e admin.domain.com_REVERSE_PROXY_URL=/webui/ \
           -e admin.domain.com_REVERSE_PROXY_HOST=http://myui:5000/ \
           -l bunkerized-nginx.UI \
           bunkerity/bunkerized-nginx
Enter fullscreen mode Exit fullscreen mode

The AUTH_BASIC environment variables let you define a login/password that must be provided before accessing to the web UI. At the moment, there is no authentication mechanism integrated into bunkerized-nginx-ui.

We can now create the bunkerized-nginx-ui container that will host the web UI behind bunkerized-nginx :

docker run --network mynet \
           -v /var/run/docker.sock:/var/run/docker.sock:ro \
           -v nginx_conf:/etc/nginx \
           -e ABSOLUTE_URI=https://admin.domain.com/webui/ \
           bunkerity/bunkerized-nginx-ui
Enter fullscreen mode Exit fullscreen mode

After that, the web UI should be accessible from https://admin.domain.com/webui/.

Antibot challenge

docker run -p 80:8080 -v /path/to/web/files:/www -e USE_ANTIBOT=captcha bunkerity/bunkerized-nginx
Enter fullscreen mode Exit fullscreen mode

When USE_ANTIBOT is set to captcha, every users visiting your website must complete a captcha before accessing the pages. Others challenges are also available : cookie, javascript or recaptcha (more info here).

Hardening

By default, bunkerized-nginx runs as non-root user inside the container and should not use any of the default capabilities allowed by Docker. You can safely remove all capabilities to harden the container :

docker run ... --drop-cap=all ... bunkerity/bunkerized-nginx
Enter fullscreen mode Exit fullscreen mode

Going further

Top comments (0)