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.
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:
- Uses the tool
sed
to search and replace part of the string output from invokingnginx -V
in the shell.- The output from
nginx -V
displays the switch parameters used for compiling NGINX and configuring make. - It makes use of the output generated, while appending our additional module (which is Headers More).
- The output from
- Extracts the contents of the Headers More plugin that we downloaded in our previous snippet.
- Defines a variable pointing to the path where Headers More has been extracted to
- 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.
-
Security Headers
- A web-based tool for scanning HTTP responses emitted by your website for recommended HTTP headers.
- Headers More Plugin GitHub Repository
- NGINX Documentation for Headers More
Top comments (0)