Lets Encrypt with an nginx reverse proxy

Introduction

I have a dev server (currently running Ubuntu 14.04 LTS), which I have been using for a while now for hosting various development tools on different ports. Because the ports can be hard to remember I have decided to to use port 80 for all of my services and do the port forwarding internally, based off hostname.

Instead of writing domain.com:5432, I can simply access it through sub.domain.com

For example the application X, which is using the port 7547 and is running on sub.domain.com has the following nginx configuration:

upstream sub {
    server 127.0.0.1:7547;
}

server {
    listen 80;
    server_name sub.domain.com www.sub.domain.com;
    access_log /var/log/nginx/sub.log combined;
    location / {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://127.0.0.1:7547;
            proxy_set_header Authorization "";
    }
}

The Question

Given the current configuration structure, which I have chosen, is it possible to use letsencrypt and run the different services under https?


Yes, you can have nginx proxy requests to HTTP servers, and then itself respond to clients over HTTPS. When doing this, you will want to be sure that the nginx<->proxy connect is unlikely to be sniffed by whoever is your expected attacker. Safe-enough approaches might include:

  • proxying to the same host (as you do)
  • proxying to other hosts behind your firewall

Proxying to another host on the public Internet is unlikely to be safe-enough.

Here are instructions for obtaining a Let's Encrypt certificate using the same webserver you are using as a proxy.

Requesting your initial certificate from Let's Encrypt

Modify your server clause to allow the subdirectory .well-known to be served from a local directory, eg:

server {
    listen 80;
    server_name sub.domain.com www.sub.domain.com;
    […]
    location /.well-known {
            alias /var/www/sub.domain.com/.well-known;
    }

    location / {
        # proxy commands go here
        […]
    }
}

http://sub.domain.com/.well-known is where the Let's Encrypt servers will look for the answers to the challenges it issues.

You can then use the certbot client to request a certificate from Let's Encrypt using the webroot plugin (as root):

certbot certonly --webroot -w /var/www/sub.domain.com/ -d sub.domain.com -d www.sub.domain.com

Your key, certificate, and certificate chain will now be installed in /etc/letsencrypt/live/sub.domain.com/

Configuring nginx to use your certificate

First create a new server clause like this:

server {
    listen 443 ssl;

    # if you wish, you can use the below line for listen instead
    # which enables HTTP/2
    # requires nginx version >= 1.9.5
    # listen 443 ssl http2;

    server_name sub.domain.com www.sub.domain.com;

    ssl_certificate /etc/letsencrypt/live/sub.domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/sub.domain.com/privkey.pem;

    # Turn on OCSP stapling as recommended at 
    # https://community.letsencrypt.org/t/integration-guide/13123 
    # requires nginx version >= 1.3.7
    ssl_stapling on;
    ssl_stapling_verify on;

    # Uncomment this line only after testing in browsers,
    # as it commits you to continuing to serve your site over HTTPS
    # in future
    # add_header Strict-Transport-Security "max-age=31536000";

    access_log /var/log/nginx/sub.log combined;

    # maintain the .well-known directory alias for renewals
    location /.well-known {
        alias /var/www/sub.domain.com/.well-known;
    }

    location / {
        # proxy commands go here as in your port 80 configuration
        […]
    }
}

Reload nginx:

service nginx reload

Verify that HTTPS now works by visiting https://sub.domain.com and https://www.sub.domain.com in your browser (and any other browsers you specifically wish to support) and checking that they don't report certificate errors.

Recommended: also review raymii.org: Strong SSL Security on nginx and test your configuration at SSL Labs.

(Recommended) Redirect HTTP requests to HTTPS

Once you have confirmed that your site works with the https:// version of the URL, rather than have some users served insecure content because they went to http://sub.domain.com, redirect them to the HTTPS version of the site.

Replace your entire port 80 server clause with:

server {
    listen 80;
    server_name sub.domain.com www.sub.domain.com;
    rewrite     ^   https://$host$request_uri? permanent;
}

You should also now uncomment this line in the port 443 configuration, so that browsers remember to not even try the HTTP version of the site:

add_header Strict-Transport-Security "max-age=31536000";

Automatically renew your certificate

You can use this command (as root) to renew all certificates known to certbot and reload nginx using the new certificate (which will have the same path as your existing certificate):

certbot renew --renew-hook "service nginx reload"

certbot will only attempt to renew certificates that are more than 60 days old, so it is safe (and recommended!) to run this command very regularly, and automatically if at all possible. Eg, you could put the following command in /etc/crontab:

# at 4:47am/pm, renew all Let's Encrypt certificates over 60 days old
47 4,16   * * *   root   certbot renew --quiet --renew-hook "service nginx reload"

You can test renewals with either a dry-run, which will contact Let's Encrypt staging servers to do a real test of contacting your domain, but won't store the resulting certificates:

certbot --dry-run renew

Or you can force an early renewal with:

certbot renew --force-renew --renew-hook "service nginx reload"

Note: you can dry run as many times as you like, but real renewals are subject to Let's Encrypt rate limits.


Yes, you can use nginx as end point of https and cooperate with backends via http. For example my config:

server {
        server_name host;
        listen 443 ssl;
...
 location /svn/ {
            auth_ldap off;

            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_pass http://localhost:1080/svn/;
            proxy_redirect http://localhost:1080/ https://host/;
        }
...
}

But as I know, with let's encrypt you have to point all subdomains when you get certificate, and if this is a problem, then you choose url https://host/service instead of https://service.host