Debian 9 server, part II: Website and SSL


About adventures with setting up nginx with Let’s Encrypt SSL, easy expandability for upcoming services, and stuff.

I’ll skip the ups and lows and I’ll get straight to the point where everything (more or less) works.

Nginx

Why nginx?
Nginx was simply bundled with a piece of software I was trying to run on my server with the option to easily use an external instance, so I went with that.
After some time with it I must say that configuring it is wayyy nicer than Apache while still having so many optional features one can get easily lost in the manpages.

Basic setup

First comes nginx installation:

sudo apt install nginx

The default configuration is sane enough to leave it alone, but feel free to check out /etc/nginx/nginx.conf and other files in the same directory.

Further configuration of nginx consists of two steps: creating a new site or module as a file in either modules-available or sites-available folder and enabling it by softlinking it in proper -enabled directory.
In my case the modules enabled by default were good enough so I’ve focused on properly configuring the sites.

Default site

The only site available (and enabled) by default is the, nomen omen, default.

To make the site more 2018-ish I wanted to use HTTPS where possible and to catch all sub-domains that were not explicitly registered (for example one with www. prefix) and display the main site there.
And so, here are the contents of the config file /etc/nginx/sites-available/default:

# HTTP->HTTPS
server {
	listen 80 default_server;
	listen [::]:80 default_server;

	server_name example.com;
	server_tokens off;

	access_log /var/log/nginx/http_access.log;
	error_log /var/log/nginx/http_error.log;

	location /.well-known {
		root /var/www/letsencrypt/;
	}

	location / {
		return 301 https://$http_host$request_uri;
	}
}

# SSL configuration
server {
	listen 443 ssl http2 default_server;
	listen [::]:443 ssl http2 default_server;

	server_name example.com;
	server_tokens off;

	gzip off;
	ssl on;
	ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

	ssl_protocols TLSv1.1 TLSv1.2;
	ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
	ssl_prefer_server_ciphers on;
	ssl_session_cache shared:SSL:10m;
	ssl_session_timeout 5m;
	ssl_session_tickets off;

	root /var/www/html;
	index index.html;

	location / {
		try_files $uri $uri.html $uri/ =404;
		error_page 404 /404.html;
	}
}

Lets get over it briefly:

  • First server section:
    • Registers default server (handling all additional subdomains) on port 80 (HTTP);
    • Disables server tokens, i.e. it doesn’t allow the client to know it’s nginx nor what version etc;
    • Sets up log files;
    • Handles a special request URI by serving files from a special directory (more on that later);
    • Redirects all other requests to their HTTPS equivalent.
  • Second server section:
    • Registers default server on port 443 (HTTPS);
    • Disables server tokens;
    • Sets up SSL:
      • Disables gzip compression (not working with SSL);
      • Enables actual SSL;
      • Points to certificate files;
      • Configures SSL options (important part: ssl_protocols should not include TLSv1.0 or most browsers will get angry);
    • Registers main directory to serve as webpage content;
    • Sets up the order in which to try files:
      • First the file as requested;
      • Then the file with .html postfix;
      • Then the directory of the same name;
      • Then redirects to 404 error page.

The default site should be enabled by default, pun not intended.

Additional sites template

All additional sites will follow the same template, derived from the default site configuration:

server {
	listen 80;
	listen [::]:80 ipv6only=on;

	server_name subdomain.example.com;
	server_tokens off;

	access_log /var/log/nginx/subdomain_access.log;
	error_log /var/log/nginx/subdomain_error.log;

	location /.well-known {
		root /var/www/letsencrypt/;
	}
	location / {
		return 301 https://$http_host$request_uri;
	}
}

server {
	listen 443 ssl http2;
	listen [::]:443 ipv6only=on ssl http2;

	server_name subdomain.example.com;
	server_tokens off;

	gzip off;
	ssl on;
	ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
	ssl_protocols TLSv1.1 TLSv1.2;
	ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
	ssl_prefer_server_ciphers on;
	ssl_session_timeout 5m;
	ssl_session_cache shared:SSL:10m;
	ssl_session_tickets off;

	location / {
		proxy_pass http://127.0.0.1:<SERVICE_PORT>;
		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;
	}
}

As my usecase for subdomains is to provide additional services (like e.g. webmail client) doing a proxy_pass is the simplest option.
Note that some services may require additional headers to be set in location / block, like Host, X-Forwarded-Ssl or Upgrade.
Also some services may require some URI mapping and such, with most notable example I’ve encountered being Gitlab (see its example nginx config for details).

Starting the server

To start the server, simply run this command:

sudo systemctl start nginx

and if not typos or errors are present in the configuration files it will just silently start.
However, may such misbehaviour happen, checking the log is usually enough to pinpoint the issue:

sudo systemctl status nginx

To ensure the server is started everytime after a reboot, enable the service:

sudo systemctl enable nginx

Now you can open your browser, navigate to http://example.com and observe being redirected to HTTPS and greeted by an ‘invalid certificate’ page.
We’ll fix that in a minute, so read on.

Let’s Encrypt

When setting up a website or a service with a web interface HTTPS is virtually a requirement.
However, regular certificates cost quite much (nobody knows why, that’s free market for you) and self-signed certificates aren’t really a solution.

Luckily that’s where Let’s Encrypt comes into play.
It’s a Certificate Authority providing free certificates that are accepted by most (if not all) modern browsers, systems etc.

There are, however, some restrictions:

  • 3 month certificate validity
  • limit of 20 certificates per week per Registered Domain (e.g. example.com)
  • limit of 5 duplicates per week
  • other rate limits, like failed validation limit, request per second limit etc

The first one is the easiest to hit, as e.g. user1.hostingcompany.com and user2.hostingcompany.com fall into the same bin.
Therefore it’s best to have a Registered Domain of your own, cheapest options being free domains under .tk, .eq etc TLDs (watch for caveats and small font prints!) or regional subdomains (e.g. .city1.tld, .rene.de, .lub.pl and such).
.eu domains are cheap too.

Setup

The easiest way to obtain a certificate from Let’s Encrypt is to use their own tool, the certbot.
Before installation make sure that stretch-backports repo is enabled as the version in the main repo is ancient.

sudo apt install certbot

Configuration file

Certbot can be used directly from the command line by just passing all the parameters.
However filling all the fields makes the command very long and quite of a mess.

It is however possible to create a configuration file and make certbot use it.
The configuration is just a regular .ini file:

authenticator = webroot
webroot-path = /var/www/letsencrypt
domains = example.com,www.example.com,subdomain1.example.com
email = user@example.com
rsa-key-size = 4096

The contents are quite self-explanatory.
The domains field is translated into alternative DNS names (except the first one, used as the CN).
Also note that webroot-path field must be the same as set up as root in nginx’s configuration for location /.well-known section.

To use the file as the default one, move it to /etc/letsencrypt/cli.ini.

Requesting/expanding/renewing certificate

First, let’s try requesting a test certificate.
It will not validate in browsers but it will not add up to rate limit (useful in case something fails etc).

sudo certbot --test-cert certonly -c config.ini

This will request a certificate from the test endpoint using previously created config file, automatically go through the validation process and save the certificate and other files in /etc/letsencrypt/test/example.com/* (nginx config again).
No other actions will be done.
certonly option is required for this to work along with nginx.

Requesting a live certificate is the same command simply without the --test-cert argument:

sudo certbot certonly -c config.ini

To renew or extend the certificate for new subdomains, just update the config file and run the same command.
It will interactively ask for confirmation with 2-3 choices of further action, like ‘do nothing’, ‘renew’ etc.

Automatic renewal

There are multiple ways to achieve this, including ones built into certbot itself.
Certificates from LE are valid for only 3 months so it’s best to renew them every two months or even a month (in case the automatic method fails it).
The simplest way is to add the command above to cron.

There are also systemd units available, certbot.service and certbot.timer.
Consult the certbot documentation for details.

Wildcard certificates

Since 13th of March, 2018, Let’s Encrypt supports wildcard certificates via their new protocol, ACMEv2.
Wildcard certificates are ones issued for all subdomains under a certain domain, e.g. *.example.com.

Why a wildcard certificate?
I can think of few reasons, main two being:

  • no need for renewing/extending a certificate when adding new services, endpoints etc
  • hiding services available on a server (with regular certificate one can just check the DNS records and then probe the registered names)

However, certbot supports ACMEv2 since version 0.22.0 and debian-packaged one is (as of writing of this post) at 0.21.1.
Therefore you can either wait for new version to be packaged or install it manually, add a DNS plugin, configure some additional things and hope for the best…
I haven’t tried it myself yet, but this guide seems quite comprehensive and short enough to follow.
It is written with cloudflare in mind, but it should be easy enough to adjust it to a regular server.

Website content

I’ve used Jekyll and described it in another post


Share this post: