User Tools

Site Tools


2017:07:03:secure-nginx-in-docker

Secure Nginx in Docker

Introduction

Docker and the official Nginx image make development and administration of public sites much easier, but there's still many sites that don't use encryption. The fact that my blog has better encryption than many large, commercial businesses is a sad commentary on where we stand.

Google penalizes sites for not having proper encryption, and in $theCurrentYear, there's really not a reason why you shouldn't, especially considering that it's available for free via an E.F.F. Project (as in speech, not as in beer - please feel free to donate to Let's Encrypt).

Prerequisite

Setup source

H5BP

Create source directory, and clone H5BP Nginx configuration repository:

mkdir -pv ~/src;

git clone \
    https://github.com/h5bp/server-configs-nginx \
    ~/src/server-configs-nginx \
    ;

Site templates

Download template-http.conf and template-https.conf to your ~/src/ directory:

(

wget \
    -O ~/src/template-http.conf \
    https://thad.getterman.org/_export/code/2017/07/03/secure-nginx-in-docker?codeblock=2

wget \
    -O ~/src/template-https.conf \
    https://thad.getterman.org/_export/code/2017/07/03/secure-nginx-in-docker?codeblock=3

)

HTTP

template-http.conf
#
# Based upon:
# https://github.com/h5bp/server-configs-nginx/blob/master/sites-available/ssl.example.com
#
 
# Choose between www and non-www, listen on the *wrong* one and redirect to
# the right one -- http://wiki.nginx.org/Pitfalls#Server_Name
server {
    listen [::]:80;
    listen 80;
 
    # listen on both hosts
    server_name %HOSTNAME% www.%HOSTNAME%;
 
    # Path for static files
    root /sites/%HOSTNAME%/public;
 
    # Let's Encrypt challenge-response
    location /.well-known {
        alias /sites/%HOSTNAME%/public/.well-known;
    }
 
    # and redirect to the https host (declared below)
    # avoiding http://www -> https://www -> https:// chain.
    location / {
        return 301 https://%HOSTNAME%$request_uri;
    }
 
}
2018/01/24 13:09 · Louis T. Getterman IV

HTTPS

template-https.conf
#
# Based upon:
# https://github.com/h5bp/server-configs-nginx/blob/master/sites-available/ssl.example.com
#
 
# https://www.%HOSTNAME%/
server {
	listen [::]:443 ssl http2;
	listen 443 ssl http2;
 
	# listen on the wrong host
	server_name www.%HOSTNAME%;
 
	include h5bp/directive-only/ssl.conf;
 
	ssl_certificate /etc/letsencrypt/live/%HOSTNAME%/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/%HOSTNAME%/privkey.pem;
 
	# and redirect to the non-www host (declared below)
	return 301 https://%HOSTNAME%$request_uri;
}
 
# https://%HOSTNAME%/
server {
 
    location / {
        root   /sites/%HOSTNAME%/public;
        index  index.html index.htm;
    }
 
	listen [::]:443 ssl http2;
	listen 443 ssl http2;
 
	# The host name to respond to
	server_name %HOSTNAME%;
 
	access_log	/var/log/nginx/%HOSTNAME%/access.log	main;
	error_log	/var/log/nginx/%HOSTNAME%/error.log	warn;
 
	include h5bp/directive-only/ssl.conf;
 
	# Path for static files
	root /sites/%HOSTNAME%/public;
 
	# Specify a charset
	charset utf-8;
 
	# Custom 404 page
	error_page 404 /404.html;
 
	# Include the basic h5bp config set
	include h5bp/basic.conf;
 
	ssl_certificate /etc/letsencrypt/live/%HOSTNAME%/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/%HOSTNAME%/privkey.pem;
 
}
2018/01/24 13:09 · Louis T. Getterman IV

Create Nginx paths

mkdir -pv \
    ~/srv/nginx/ \
    ~/srv/nginx/log/ \
    ~/srv/sites/ \
    ;

Initialize Nginx

(
    docker run --name tmp-nginx-container -d nginx:latest

    docker cp tmp-nginx-container:/etc/nginx/ ~/srv/nginx/

    docker cp tmp-nginx-container:/usr/share/nginx/html/ ~/srv/sites/default/

    mv -iv ~/srv/nginx/nginx ~/srv/nginx/etc

    docker rm -f tmp-nginx-container

    mkdir -pv \
        ~/srv/nginx/etc/sites-available \
        ~/srv/nginx/etc/sites-enabled \
        ;
)

Setup a fall-through site for sites-available, and then enable it:

mv -iv \
    ~/srv/nginx/etc/conf.d/default.conf \
    ~/srv/nginx/etc/sites-available/default \
    ;

cd ~/srv/nginx/etc/sites-enabled/
ln -sv \
    ../sites-available/default \
    ./ \
    ;

Add default_server parameter to ~/srv/nginx/etc/sites-available/default. The parameter should now look like:

server {
    listen       80 default_server;
    server_name  localhost;
...

H5BP configuration

Add

(

cp -vaR \
    ~/src/server-configs-nginx/h5bp \
    ~/srv/nginx/etc/ \
    ;

cp -vaRf \
    ~/src/server-configs-nginx/mime.types \
    ~/srv/nginx/etc/ \
    ;

cp -vaRf \
    ~/src/server-configs-nginx/nginx.conf \
    ~/srv/nginx/etc/ \
    ;

)

Modify

Open ~/srv/nginx/etc/nginx.conf and set the following variables (most already exist in the configuration, and simply need to be updated)

user nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
worker_connections  1024;
access_log  /var/log/nginx/access.log  main;
keepalive_timeout  65;

While editing this file, comment out the TCP push option:

#tcp_nopush     on;

Website

Prompt

read -p "Website FQDN : " fqdnSite

Configuration

(

# HTTP
sed "s/%HOSTNAME%/${fqdnSite}/g" < ${HOME}/src/template-http.conf > "${HOME}/srv/nginx/etc/sites-available/${fqdnSite}-http"

# HTTPS
sed "s/%HOSTNAME%/${fqdnSite}/g" < ${HOME}/src/template-https.conf > "${HOME}/srv/nginx/etc/sites-available/${fqdnSite}-https"

)

Paths

mkdir -pv \
    ~/srv/sites/${fqdnSite}/public/ \
    ~/srv/nginx/log/${fqdnSite}/ \
    ;

Content

echo "Content for ${fqdnSite} goes here." > "${HOME}/srv/sites/${fqdnSite}/public/index.html"

Enable HTTP

This step only enables HTTP for the time being. This is so that the challenge-response by Let's Encrypt is an option for you.

(

cd ~/srv/nginx/etc/sites-enabled/
ln -sv ../sites-available/${fqdnSite}-http

)

Enable HTTPS

Regardless of which challenge-response that you opt for, you'll need to perform this step after you finish the Let's Encrypt step.

I'm sorry for having this step out of order, but I'm trying to keep commands grouped together with their respective software.

Once you have setup Let's Encrypt certificates below, you can enable HTTPS for your domain in Nginx without it failing:

(

cd ~/srv/nginx/etc/sites-enabled/
ln -sv ../sites-available/${fqdnSite}-https

)

Let's Encrypt C.A.

The subsequent Docker run command is geared towards your TLS Certificate coming from the “Let's Encrypt” Certificate Authority. If you are using a different provider, you will need to modify the Site template above, and Nginx start command below, for the certificate and key files to point to the appropriate location.

Assuming that you don't have one, please follow the subsequent steps in this section.

Certificate Request

This step is required by Let's Encrypt, in order to verify ownership of the domain. Challenge-responses covered in this article:

  1. Web - Since this article is about using Nginx, this method should be simpler for you.

  2. DNS - Authorization via a TXT record is one of the most easy methodologies for validating domain ownership, since you don't have to run a server. In particular, it's great for a firewall, switch, Plex server, and other devices that can't (or more importantly, shouldn't) be running a web server. But, it's more hands-on when you need to renew a certificate (e.g. keeping a low TTL, and trying to get the records updated before the challenge times out). I always recommend FreeDNS, but there's many other providers (e.g. Linode, Digital Ocean, Google Cloud DNS, and Amazon Route 53) and APIs to control varying services (e.g. DNSControl, for which I wrote a small utility, DNSControl Compiler.)

Web-based

This can sometimes be a little more tricky than the DNS method below. Since Let's Encrypt has rate limiting (at the time of this writing; 5 requests per domain, per hour), you may want to add --staging as an argument, in order to test everything, before you make a real request.

  1. Issue a challenge request to Let's Encrypt:
    docker run -it --rm --name certbot \
        -v ~/srv/letsencrypt/etc:/etc/letsencrypt \
        -v ~/srv/letsencrypt/var/lib:/var/lib/letsencrypt \
        -v ~/srv/letsencrypt/var/log:/var/log/letsencrypt \
        \
        -v ~/srv/sites/"${fqdnSite}"/public/.well-known/:/sites/"${fqdnSite}"/public/.well-known/ \
        \
        certbot/certbot \
        \
        certonly \
        --webroot \
        --agree-tos \
        --no-eff-email \
        -m "webmaster@${fqdnSite}" \
            -w /sites/"${fqdnSite}"/public/ \
                -d "${fqdnSite}" \
                -d "www.${fqdnSite}" \
        ;


    Which should produce a successful response:

DNS-based

  1. Issue a challenge request to Let's Encrypt:
    docker run -it --rm --name certbot \
        -v ~/srv/letsencrypt/etc:/etc/letsencrypt \
        -v ~/srv/letsencrypt/var/lib:/var/lib/letsencrypt \
        -v ~/srv/letsencrypt/var/log:/var/log/letsencrypt \
        certbot/certbot \
        -d "${fqdnSite}" \
        -d "www.${fqdnSite}" \
        --manual --preferred-challenges dns certonly

Certificate Renewal

Certificates issued by Let's Encrypt expire after 3 months. To avoid certificate expiration errors being displayed to your audience, you can use Cron to automatically run the following command:

docker run -it --rm --name certbot \
    -v ~/srv/letsencrypt/etc:/etc/letsencrypt \
    -v ~/srv/letsencrypt/var/lib:/var/lib/letsencrypt \
    -v ~/srv/letsencrypt/var/log:/var/log/letsencrypt \
    certbot/certbot \
    renew

Nginx

Start

Let's run Nginx, and view the default welcome page.

docker run \
    \
    --name webserver \
    \
    --volume ~/srv/nginx/etc/:/etc/nginx/:ro \
    --volume ~/srv/nginx/log/:/var/log/nginx/ \
    \
    --volume ~/srv/letsencrypt/etc:/etc/letsencrypt/:ro \
    \
    --volume ~/srv/sites/default/:/usr/share/nginx/html/:ro \
    --volume ~/srv/sites/:/sites/:ro \
    \
    --publish 80:80 \
    --publish 443:443 \
    \
    --detach \
    --restart=always \
    \
    nginx:latest \
    ;

Restart

To reflect our new site additions, we'll need to restart Nginx.

docker restart webserver

Stop

docker stop webserver && docker rm webserver

Upgrade

There's discussion about upgrading, and the steps are simple:

  1. Pull the latest version:
    docker pull nginx:latest
  2. Stop the web server's container:
    docker stop webserver
  3. Remove the web server's container:
    docker rm webserver
  4. Re-run the web server using the Nginx run command above.

  5. Clean-up:
    (
    
    docker rm $(docker ps --all --quiet --no-trunc --filter 'status=exited')
    
    docker rmi $(docker images --quiet --filter 'dangling=true')
    
    )

Conclusion

In subsequent guides that I'll write in the future, I will demonstrate how to:

  1. Use this guide as a base for isolating domains and/or paths as Docker containers from each other, with static, and more importantly, dynamic content:
  2. Develop in a test environment that can be “hot-swapped”.
  3. Load-balancing with:
    • HAProxy
    • Nginx load balancer
    • Kubernetes
    • Docker Swarm
2017/07/03/secure-nginx-in-docker.txt · Last modified: 2018/01/31 01:13 by Louis T. Getterman IV