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 includeTLSv1.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