Skip to main content

NGINX Docker with SSL Encryption (Self-signed)

Victoria Harbour, Hong Kong

Self-signed Certificate

In this guide, I will set up a self-signed SSL certificate for use with an Nginx proxy (Docker Container) on an Ubuntu 20.04 server.

Note: A self-signed certificate will encrypt communication between your server and any clients. However, because it is not signed by any of the trusted certificate authorities included with web browsers, users cannot use the certificate to validate the identity of your server automatically.

Creating the SSL Certificate

TLS/SSL works by using a combination of a public certificate and a private key. The SSL key is kept secret on the server. It is used to encrypt content sent to clients. The SSL certificate is publicly shared with anyone requesting the content. It can be used to decrypt the content signed by the associated SSL key. I will create a self-signed key and certificate pair with OpenSSL in a single command:

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /opt/docker-ingress/configuration/ssl/nginx-selfsigned.key -out /opt/docker-ingress/configuration/ssl/nginx-selfsigned.crt
  • openssl: This is the basic command line tool for creating and managing OpenSSL certificates, keys, and other files.
  • req: This sub-command specifies that we want to use X.509 certificate signing request (CSR) management. The “X.509” is a public key infrastructure standard that SSL and TLS adheres to for its key and certificate management. We want to create a new X.509 cert, so we are using this sub-command.
  • -x509: This further modifies the previous sub-command by telling the utility that we want to make a self-signed certificate instead of generating a certificate signing request, as would normally happen.
  • -nodes: This tells OpenSSL to skip the option to secure our certificate with a passphrase. We need Nginx to be able to read the file, without user intervention, when the server starts up. A passphrase would prevent this from happening because we would have to enter it after every restart.
  • -days 365: This option sets the length of time that the certificate will be considered valid. We set it for one year here.
  • -newkey rsa:2048: This specifies that we want to generate a new certificate and a new key at the same time. We did not create the key that is required to sign the certificate in a previous step, so we need to create it along with the certificate. The rsa:2048 portion tells it to make an RSA key that is 2048 bits long.
  • -keyout: This line tells OpenSSL where to place the generated private key file that we are creating.
  • -out: This tells OpenSSL where to place the certificate that we are creating.
Generating a RSA private key
......................+++++
........+++++
writing new private key to '/opt/docker-ingress/configuration/ssl/nginx-selfsigned.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:DE
State or Province Name (full name) [Some-State]:NRW
Locality Name (eg, city) []:Cologne
Organization Name (eg, company) [Internet Widgits Pty Ltd]:INSTAR Deutschland GmbH
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:192.168.2.111
Email Address []:m.polinowski@instar.com

The most important line is the one that requests the Common Name (e.g. server FQDN or YOUR name). You need to enter the domain name associated with your server or your server’s public IP address (in my case it is just a testing setup - so I will use my local IP address).

This will create the certificate and public key in the ssl directory:

ls -la ssl
total 8.0K
-rw-r--r-- 1 root root 1.5K Oct 11 15:29 nginx-selfsigned.crt
-rw------- 1 root root 1.7K Oct 11 15:28 nginx-selfsigned.key

Perfect Forward Secrecy

While we are using OpenSSL, we should also create a strong Diffie-Hellman group, which is used in negotiating Perfect Forward Secrecy with clients. We can do this by typing:

sudo openssl dhparam -out /opt/docker-ingress/configuration/ssl/dhparam.pem 4096

This will take a while, but when it’s done you will have a strong DH group at /opt/docker-ingress/configuration/ssl/dhparam.pem that we can use in our configuration.

Configuring Nginx to Use SSL

We have created our key and certificate files under a /opt/docker-ingress/configuration/ssl directory. Now we just need to mount this directory into our NGINX container and modify our Nginx configuration to take advantage of these. In the previous step I already ended up with a docker compose file to start NGINX - I now have to add the SSL directory to it:

version: '3.8'
services:
  ingress:
    image: nginx:stable-alpine
    container_name: ingress
    networks:
      - gateway
    ports:
      - '80:80'
      - '443:443'
    restart: unless-stopped
    volumes:
      - /opt/docker-ingress/configuration/conf.d:/etc/nginx/conf.d
      - /opt/docker-ingress/configuration/ssl:/etc/nginx/ssl
      - /opt/docker-ingress/configuration/nginx.conf:/etc/nginx/nginx.conf
      - /opt/test-static/public:/opt/test-static/public
      - /opt/downloads:/opt/downloads

networks:
  gateway: {}

Creating a Configuration Snippet Pointing to the SSL Key and Certificate

I now create a new configuration file inside the conf.d directory that holds the paths for our certificate and public key:

/etc/nginx/ssl/self-signed.conf

ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt;
ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;

Creating a Configuration Snippet with Strong Encryption Settings

Next, we will create another snippet that will define some SSL settings. This will set Nginx up with a strong SSL cipher suite and enable some advanced features that will help keep our server secure.

/etc/nginx/ssl/ssl-params.conf

ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_timeout  10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Disable strict transport security for now. You can uncomment the following
# line if you understand the implications.
# add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

Check Cipherli for details.

Because we are using a self-signed certificate, the SSL stapling will not be used. Nginx will output a warning, disable stapling for our self-signed cert, and continue to operate correctly.

2020/10/11 16:51:19 [warn] 1#1: "ssl_stapling" ignored, issuer certificate not found for certificate "/etc/nginx/ssl/nginx-selfsigned.crt"ingress    | nginx: [warn] "ssl_stapling" ignored, issuer certificate not found for certificate "/etc/nginx/ssl/nginx-selfsigned.crt"

Adjusting the Nginx Configuration

In the previous step I created the following server block:

server {
    listen       80 default_server;
    listen       [::]:80 default_server;
    server_name  localhost;
    root         /opt/test-static/public;
    index        index.html;

    ...
}

I will be modifying this existing server block to serve SSL traffic on port 443, then create a new server block to respond on port 80 and automatically redirect traffic to port 443.

server {
    listen      443 ssl;
    listen      [::]:443 ssl;
    include     ssl/self-signed.conf;
    include     ssl/ssl-params.conf;

    server_name 192.168.2.111;

    root         /opt/test-static/public;
    index        index.html;

    . . .
}

. . .

server {
    listen 80;
    listen [::]:80;

    server_name 192.168.2.111;

    return 302 https://$server_name$request_uri;
}

Note: We will use a 302 redirect until we have verified that everything is working properly. Afterwards, we can change this to a permanent 301 redirect.

Changing to a Permanent Redirect

If your redirect worked correctly and you are sure you want to allow only encrypted traffic, you should modify the Nginx configuration to make the redirect permanent:

return 301 https://$server_name$request_uri;