NGINX Docker with SSL Encryption (Self-signed)
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 permanent301
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;