/home/hardbidouille

$ man nginx | less +/TLS\ pass-through

Thu 26 Mar 2020

Goal: Setting up frontal nginx server with ssl passthrough to apache backends using let's encrypt and dehydrated client.

At home I have multiple VMs providing different services to the web via Apache. In order for these services to respond on the same IP address I have been setting up a reverse proxy using Nginx.
Lately I have been worried about something: To prevent any "man in the middle" attack, even if it was to come from my own local network (e.g. if my wifi was to be compromised by the NSA), I would have to make sure the traffic is encrypted all the way towards my backends.
This is the topic of this article and why decided to configure Nginx with SSL passthrough to my Apache servers, enabeling traffic to be ciphered until it's final destination.

Let's explain more in depth how this is going to work:
Whenever someone makes a request through http or https (respectively ports 80 and 443), nginx will first redirect the traffic to port 443, then it is going to pass the request to the backend(s) which are holding their own certificates. This is different from the usual setup where the certificate are holded by the reverse proxy.
The certificates will be generated by dehydrated on Apache server and will be automatically renewed whenever needed.

Let's get started,
I will assume here that you use a Debian based environement and that your front nginx server and bakends are already set up like this is the case for me.

Configuring Nginx

Sources:

https://stackoverflow.com/questions/34741571/nginx-tcp-forwarding-based-on-hostname/40135151#40135151
https://nginx.org/en/docs/http/ngx_http_map_module.html#map

On the Nginx server, we first need to add the following line in /etc/nginx/nginx.conf :

include /etc/nginx/passthrough.conf

This is in order to call the configuration file we are about to create. Make sure you place this outside of the http block as they need to be on the same level with the stream block.

Now create and edit the /etc/nginx/passthrough.conf file, mine look like this (obviously you should adapt it to your own configuration) :

stream {
 map $ssl_preread_server_name $name { # Redirecting request to the server (first field) to the right upstream (second feild)
  hardbidouille.com/git git;
  hardbidouille.com/gitweb git;
  hardbidouille.com/drupal7 drupal;
  default apache; # The default upstream if the request dosen't match any of the above
 }

 # defining the IP and port of the server for each upstream
 upstream git {
  server 192.168.0.4:443;
 }

 upstream apache {
  server 192.168.0.2:443;
 }

 upstream drupal {
  server 192.168.0.3:443;
 }

 server {
  listen 443; # we are listening for HTTPS on the nginx server
  proxy_pass $name; # using the variable mapped above with the proxy_pass directive
  ssl_preread on;
 }
}

We also need to enable requests through http in order for Let's Encrypt to work properly, create a file under /etc/nginx/site-available and add the follwing :

upstream git {
 server 192.168.0.4;
}

upstream drupal {
 server 192.168.0.3;
}

upstream apache {
 server 192.168.0.2;
}

server {
 listen 80;
 server_name .hardbidouille.com;
 #return 301 https://$host$request_uri; #only needed if you want to force https once the first certificate has been issued

 location /git {
  include proxy_params;
  proxy_pass http://git;
 }

 location /drupal {
  include proxy_params;
  proxy_pass http://drupal;
 }

 location / {
  include proxy_params;
  proxy_pass http://apache;
 }
}

If you happen to have multiple hostnames or subdomains (e.g. cloud.hardbidouille.com) make sure you add a new server block around the location block for each of them.
Once set you will need to create a symbolic link with the new file in order to enable it ( ln -s /etc/nginx/site-available/yourfile /etc/nginx/site-enabled/yourfile )

You can test your configuration with nginx -t before restarting Nginx.

Configuring Let's Encrypt with Dehydrated

Sources:

https://technique.arscenic.org/ssl-securisation-des-communications-serveur-client/article/installer-et-configurer-un-certificat-let-s-encrypt

Once connected to our Apache server, we first need to install Dehydrated :

# apt install dehydrated

We will create the configuration file for Apache following the recommandation in "/usr/share/doc/dehydrated/docs/wellknown.md" :

# echo "Alias /.well-known/acme-challenge /var/lib/dehydrated/acme-challenges
<Directory /var/lib/dehydrated/acme-challenges>
 Options None
 AllowOverride None
 Require all granted
</Directory>" > /etc/apache2/conf-available/dehydrated.conf

Enable the configuration :

# ln -s /etc/apache2/conf-available/dehydrated.conf /etc/apache2/conf-enabled/dehydrated.conf
# systemctl reload apache2

And make sure the folder is reachable from outside :

# echo "it works" > /var/lib/dehydrated/acme-challenges/test.txt
# curl http://192.168.0.2/.well-known/acme-challenge/test.txt
it works

We need to add the following directives to /etc/dehydrated/config (don't forget to change for your email) :

CONFIG_D=/etc/dehydrated/conf.d
BASEDIR=/var/lib/dehydrated
WELLKNOWN="${BASEDIR}/acme-challenges"
DOMAINS_TXT="/etc/dehydrated/domains.txt"
CONTACT_EMAIL=admin@somewhere.com
#Path to certificate authority
CA="https://acme-v02.api.letsencrypt.org/directory"
#CA="https://acme-staging-v02.api.letsencrypt.org/directory" # (switch to this CA while testing)

Finally we need to create a file with all the domain names we wish to create certificates for :

# echo 'hardbidouille.com www.hardbidouille.com' > /etc/dehydrated/domains.txt

Now lets create a test certificate (don't forget to redirect the port on your router/box) :

dehydrated -c

You might have to register on first run with the command : dehydrated --register --accept-terms.
Also, don't forget to switch back to the regular CA if you switched it for tests earlier, then renew the certs with "dehydrated -c -x".

We're now ready to configure Apache, my configuration look like this :

<VirtualHost *:80>

 ServerName hardbidouille.com
 ServerAlias www.hardbidouille.com
 DocumentRoot /var/www/html/

 ErrorLog ${APACHE_LOG_DIR}/error.log
 CustomLog ${APACHE_LOG_DIR}/access.log combined

</VirtualHost>

<VirtualHost *:443>

 ServerName hardbidouille.com
 ServerAlias www.hardbidouille.com
 DocumentRoot /var/www/html/

 ErrorLog ${APACHE_LOG_DIR}/error.log
 CustomLog ${APACHE_LOG_DIR}/access.log combined

 SSLEngine on
 SSLCertificateFile /var/lib/dehydrated/certs/hardbidouille.com/fullchain.pem 
 SSLCertificateKeyFile /var/lib/dehydrated/certs/hardbidouille.com/privkey.pem
 # enable HTTP/2, if available
 Protocols h2 http/1.1
 # HTTP Strict Transport Security (mod_headers is required) (63072000 seconds)
 Header always set Strict-Transport-Security "max-age=63072000"

</virtualhost>

Don't hesitate to use this handy tool provided by mozilla.

Final steps :

# a2enmod ssl
# a2enmod headers # as needed
# systemctl restart apache2

And at last, we are going to automate renewal of the certificates, edit the root crontab (sudo crontab -e) and add the following :

15 3 * * 4 /usr/bin/dehydrated -c

This will run every Thursday at 3:15 AM and renew the certificates if they expire in less than 30 days.