Luc Shelton

Securing HTTP Headers with NGINX in Docker

Securing HTTP Headers with NGINX in Docker

Updated 6 months ago
7 Minute(s) to read
Posted 6 months ago Updated 6 months ago 7 Minute(s) to read 0 comments

Nginx, stylized as NGINX, nginx or NginX, is a web server that can also be used as a reverse proxy, load balancer, mail proxy and HTTP cache. More typically it is the web server of choice for most applications and APIs targeting the web. Like other popular web servers, NGINX tends to emit more data than necessary when serving HTTP requests when using the default configuration, which in turn can make it susceptible to (zero day) exploits.

An example of what is meant can be found in the image below.

HTTP headers from our page response using Google Chrome's developer tools.

HTTP headers from our page response using Google Chrome's developer tools.

This is the emitted response from navigating to any page on this website when using the default configuration. This is not ideal, as hackers or penetration testers can use automated tools for scanning and detecting websites that have potential vulnerabilities.

This article will cover some steps that you can take for securing the HTTP responses that your NGINX server emits when using the Alpine Linux version of the Docker image.

This article assumes that you have appropriately configured your development environment for usage with Docker's BuildKit features.


Extending NGINX

 In the root of your project, create the following directory layout.

containers/
- nginx/
-- build/
--- Dockerfile
-- configuration/
--- templates/
docker-compose.yaml
.env

Your docker-compose.yaml should look something like this...

version: '3.9'

services:
  nginx:
    container_name: '${SERVICE_NAME}-nginx'
    image: '${REMOTE_REGISTRY_HOST}${SERVICE_NAME}/nginx:${BUILD_VERSION}'
    build: 
      context: '${BUILD_ROOT}' 
      dockerfile: '${CONTAINERS_ROOT}/nginx/build/Dockerfile'
      target: portfolio-nginx-build
      args:
        NGINX_VERSION: ${NGINX_VERSION}
        NGINX_HEADERS_MORE_VERSION: ${NGINX_HEADERS_MORE_VERSION}
    environment:
      NGINX_ENVSUBST_TEMPLATE_DIR: /etc/nginx/templates
      NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx/conf.d
      NGINX_ENVSUBST_TEMPLATE_SUFFIX: .template

Your .env file should look something like this...

SERVICE_NAME=your-service-name-goes-here
COMPOSE_PROJECT_NAME=${SERVICE_NAME}
BUILD_ROOT=${PWD}
PROJECT_ROOT=${PWD}
CONTAINERS_ROOT=${PROJECT_ROOT}/containers

So far, so good, and nothing out of the ordinary either.

Dockerfile

Keep in mind that this article assumes that you are intending on using the Alpine Linux flavour or variant of NGINX's Docker image. It's significantly smaller in size, as the operating system variant comes typically bundled with far less system utilities and running services. The Linux variant is typically used in embedded systems for the same reasoning.

The beginning of our Dockerfile is nothing out of the ordinary. It provides 3 arguments that are used for describing this particular flavour of the image, which are

  • CUSTOM_BUILD_VERSION
  • CUSTOM_BUILD_DATE
  • CUSTOM_BUILD_UID

These are ideal if you are generating the Docker images as part of your continuous integration or build pipeline. At the time of writing this article, we are using version 1.20.1 of NGINX, which is compatible with version 0.33 of the Headers More plugin.

In addition, our Dockerfile "parameterises" the version numbers used for retrieving the correct version of NGINX and Headers More. 

ARG CUSTOM_BUILD_VERSION
ARG CUSTOM_BUILD_DATE
ARG CUSTOM_BUILD_UID

ARG NGINX_VERSION 1.20.1

FROM nginx:${NGINX_VERSION}-alpine AS builder

ARG CUSTOM_BUILD_VERSION
ARG CUSTOM_BUILD_DATE
ARG CUSTOM_BUILD_UID

ARG NGINX_VERSION 1.20.1
ARG NGINX_HEADERS_MORE_VERSION 0.33

Next, we are going to have to download the source files for both NGINX and the Headers More plugin.

RUN wget "http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" -O nginx.tar.gz && \
    wget "https://github.com/openresty/headers-more-nginx-module/archive/v${NGINX_HEADERS_MORE_VERSION}.tar.gz" -O headers-more.tar.gz

This is necessary as we have to produce a local build of NGINX that integrates Headers More as a dynamic module. Therefore this means that we need to ensure that Alpine Linux has the GCC compiler toolchain available, along with make tool and some additional packages.

RUN apk add --no-cache --virtual .build-deps \
  git \
  gcc \
  libc-dev \
  make \
  openssl-dev \
  pcre-dev \
  zlib-dev \
  linux-headers \
  curl \
  gnupg \
  libxslt-dev \
  gd-dev \
  geoip-dev

This bit consists of preparing the compilation environment for NGINX, including adjusting environment variables used by the compilation toolchain.

RUN mkdir -p /usr/src

# Reuse same cli arguments as the nginx:alpine image used to build
RUN CONFARGS=$(nginx -V 2>&1 | sed -n -e 's/^.*arguments: //p') \
	tar -zxC /usr/src -f "nginx.tar.gz"

RUN tar -zxvC /usr/src -f "headers-more.tar.gz" 

RUN HEADERSMOREDIR="/usr/src/headers-more-nginx-module-0.33" && \
  cd /usr/src/nginx-$NGINX_VERSION && \
  ./configure --without-http_autoindex_module --with-compat $CONFARGS --add-dynamic-module=$HEADERSMOREDIR && \
  make && make install

This does the following:

  1. Uses the tool sed to search and replace part of the string output from invoking nginx -V in the shell.
    1. The output from nginx -V displays the switch parameters used for compiling NGINX and configuring make.
    2. It makes use of the output generated, while appending our additional module (which is Headers More).
  2. Extracts the contents of the Headers More plugin that we downloaded in our previous snippet.
  3. Defines a variable pointing to the path where Headers More has been extracted to
  4. Runs make.

In this next stage, we finally make use of the generated build output after downloading and building NGINX and Headers More together. We make use of a feature in Docker called "multi-stage" builds, which enables us to copy the contents from another stage of a build. This approach to generating Docker images is considered to be significantly more efficient, as it enables us to reuse cached stages when rebuilding the Docker image, or making iterative changes.

FROM nginx:${NGINX_VERSION}-alpine as your-service-name-goes-here-nginx

# Extract the dynamic module "headers more" from the builder image
COPY --from=builder /usr/local/nginx/modules/ngx_http_headers_more_filter_module.so /usr/local/nginx/modules/ngx_http_headers_more_filter_module.so

The rest of the Dockerfile definition is entirely up to you. Depending on the flavour of the build (i.e. development or production), you can choose to copy or include other files that you would find useful in that particular flavour of the build.

You can find the complete Dockerfile constructed in this article here.


Configuration

Now that you have a working Dockerfile definition for your NGINX image, we can now configure how the NGINX server will behave. The intent behind this configuration is to ensure that we are not sending more data that necessary in our HTTP responses. Using the Headers More extension that we downloaded, compiled, included as part of our NGINX Dockerfile, we can now prepare a NGINX server block configuration that can remove headers using the appropriate syntax.

The Headers More extension makes the more_clear_headers command available for us to use, meaning that we can now remove headers that are added by default by the NGINX web server.

more_clear_headers 'Server';
more_clear_headers 'X-Powered-By';

A more complete example would look like this. Find below an example root nginx.conf configuration file.

# Load in the headers more module
load_module /usr/local/nginx/modules/ngx_http_headers_more_filter_module.so;

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {

    more_clear_headers 'Server';
    more_clear_headers 'X-Powered-By';

    server_tokens off;

    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;


    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    include /etc/nginx/conf.d/*.conf;
}

This configuration ensures that all HTTP responses served, regardless of which server blocks you have configured, will have the X-Powered-By and Server headers stripped from the HTTP response. Handy!

Now you can rest assured that visitors will be none-the-wiser about which kind of software you are using for serving your web applications, and thus mitigating the ability for a malicious visitor to find or target exploits in your software stack.


Further Reading

You might find these links interesting if you wish to take further steps to mitigate security vulnerabilities on your web application.


Technologies:

Docker NGINX


Comments

Comments