Nginx Reverse Proxy with TLS Encryption
Tags: devops, nginx
Published: 2020-06-01 12:47:22

TLS encryption of traffic is a security must have. In this article, I show how to create and install a self-signed certificate, and how to configure Nginx for providing encryption to all ingress and egress traffic.

My infrastructure at home is running: Docker as the execution environment for all applications, Nomad for scheduling Docker jobs, Consul for service discovery, and Traeffik as the edge router. In the next posts, I will evolve my infrastructure by securing the communications between the components and by adding new applications.

In the last article, I explained how to setup Nginx as a Reverse Proxy that uses Consul DNS entries to forward the requests. Now we need to encrypt all HTTP traffic. We will do this by generating TLS certificates with the `openssl` command line tool, and then configure Nginx to process HTTP requests.

With this, we will fulfill the following two requirements:

* Application Security
  * APPSEC01: Secure all client to service communication with TLS
  * APPSEC02: Secure inter service communication with TLS

_This article originally appeared at [my blog](https://admantium.com/blog/ic11_nginx_tls_encryption/)_.

## Certificate Generation process

The normal way to generate certificates involves multiple steps:

* You create a certificate signing request on your severer
* You forwarded the request to a registration authority
* The registry authority checks the claim that you are the owner of the server, and sends an ok to a certificate authority
* The certificate authority generates the certificate and the private key
* The certificate is forwarded to a validation authority
* The certificate and the private key will be sent to you
* You install the certificate on your server

Do you need to follow all these steps? Well, it depends. If you want to provide a certificate for a service or web page which is public, then you need to involve a paid certificate authority or use the free, three months valid certificates from [Lets Encrypt]( https://letsencrypt.org/how-it-works/).  If you need a certificate inside a local network, for a local domain, you can self-sign certificates.

Self-signed certificate will be our choice.

## Creating Certificates with OpenSSL

The `openssl` command line utility is a [versatile tool](https://wiki.openssl.org/index.php/Command_Line_Utilities): It offers commands to create, sign and validate different types of certificates, t can be used to encrypt files or compute hash values from files, and you can test TSL connections from client to server [^1].  

To generate a certificate, we need to execute the following command:

```bash
openssl req -new -x509 -days 90 -newkey rsa:4096 -sha512 -nodes -out admantium.cert -keyout admantium.key
```

The flags look scary - lets understand them:

* `req`: Make a new certificate request
* `x509`: Create the certificate in [X509 format](/https://en.wikipedia.org/wiki/X.509)
* `days`: Set the viability period of this certificate
* `newkey`: Generate a new key using [4096bit RSA](https://en.wikipedia.org/wiki/RSA_(cryptosystem))
* `sha512`: Use [SHA-512](https://en.wikipedia.org/wiki/SHA-3) as the message digest algorithm
* `nodes`: Do not require a password for the key file (ONLY ok for self-signed certificates)
* `out`: Path and filename where the certificates should be saved
* `keyout`: Path and filename where the certificate should be saved

The next step is to use the required directives in Nginx, as shown in the next snippet:

```nginx
server {
  server_name prometheus.infra;
  listen 443 ssl;

  ssl_certificate /etc/nginx/certs/admantium.cert;
  ssl_certificate_key /etc/nginx/certs/admantium.key;
}
```

* `listen 443 ssl`: Listen on port 443 for incoming TLS connection
* `ssl_certificate`: Path and filename of the certificate
* `ssl_certificate_key`: Path and filename of the certificate key

Finally, to increase the security, I also recommend to add the following directives to the `http` block:

```nginx
http {
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256";
  ssl_prefer_server_ciphers on;
}
```

* `ssl_protocols`: The supported protocols - only use TLS1.2 and TLS1.3 since older versions have [security weaknesses](https://www.acunetix.com/blog/articles/tls-vulnerabilities-attacks-final-part/)
* `ssl_ciphers`: With this list you control the available encryption methods that Nginx and the client can use for exchanging data, select only [strong ciphers](https://www.acunetix.com/blog/articles/tls-ssl-cipher-hardening/)
* `ssl_prefer_server_ciphers`: A flag that specifies to use the offered ciphers, not those that the client offers.

## Testing the Connection

Restart Nginx and browse to a page. Your browser will notify you that "the page is not secure" - a very misleading message! The message is about complaining that the certificate authority is not a well-known one - which is true, since we self-signed the certificates. But the connection is secured with TLS encryption. You need to accept the notification, and then browse normally.

![ic12_firefox_warning.png](https://admantium.com/images/blog/infra/ic12_firefox_warning.png)

## Configure DNSmasq

The final piece is to set DNS resolution so that clients and services reach each other only via your TLS enabled proxy server. When you have followed my series, you know that I'm using [DNSMasq](https://manpages.debian.org/jessie/dnsmasq-base/dnsmasq.8.en.html). On each server, the DNSmasq configuration file needs to include an `address` setting that points to the server running Nginx.

```ini
address=/infra/192.168.2.201
```

Now you can resolve any `https://*.infra` addresses.

## Critical Review

So, the goal is achieved, but it has two drawbacks:

* Static configuration: The configuration is determined when I configure Nginx by running an Ansible script. When a new service starts, I need to modify the Nginx configuration. If an existing service stops, Nginx will show an error. However, when the service comes back, which is the default behavior of the job scheduler Nomad, the Consul DNS resolution points to the new space.
* No dynamic ports: The open source version of Nginx cannot parse DNS SRV records to determine the dynamic ports of a service - this feature is available only on the paid version [Nginx Plus](https://www.nginx.com/blog/dns-service-discovery-nginx-plus/). For now, I configure the all Services that are run with Nomad to use static ports. Then, in the Nginx configuration, I can refer to these static ports. This works for now. If the demand to oversee a high amount of services with dynamic ports arises, I could either use the [Consul Template](https://learn.hashicorp.com/consul/integrations/nginx-consul-template) extension, a daemon with the task to watch variables and writes config files when they change, or I go back to a zero-conf router like [Traeffik](https://docs.traefik.io/) or [Fabio](https://fabiolb.net/).

## Summary

This article explained how to provide TLS encryption to Nginx. We explored the OpenSSL command line utility to create a self-signed certificate. We showed how to configure Nginx to listen to TLS connection and provide the generated certificate. Finally, I showed how to configure DNSmasq to use Nginx for resolving all traffic inside the cluster.

## Footnotes

[^1]: See also this [cookbook-style article](https://www.madboa.com/geek/openssl/) for quickly to apply examples.