9 minute read

This is the second post in the series dedicated to describe the hardware and the configuration of this site. The first post in this series is available here. In this post, I will describe the installation and configuration of the web server on this machine.

It has already been some time since I did all this, and despite I took notes on the process, I still might have skipped something, or even introduced some errors. If you feel something is amiss, please drop me a commend and I will be happy to fix this post.

1. The Goals

First, some indications on what I want to achieve:

  • I want to serve only static content: easier to maintain, lower resource usage, and harder to exploit :smile:. Anyway, this might change in the future.
  • HTTP/2 enabled, just because it sounds cool.
  • HTTPS enabled and forced. Because some encryption is always welcome.
  • I need three virtual hosts:
    1. One to host my web site (this one).
    2. A second one to host my private git repository. This means I will also need WebDAV features. The details about this will come in the next post in this series.
    3. A third virtual host to act as a proxy for my testing machine.
  • HTTP Auth and uWSGI capability, for future projects.

All these features can be provided both by the nginx web server and the Apache web server, but as nginx goes lighter on resources, I’ll use that one.

2. SSL certificates

In the highly improbable case that you have never heard of Let’s Encrypt, you should go and check their website. Basically, they are a free, automated, and open certificate authority (CA) with the goal of improving security and privacy in the Web by issuing free SSL/TLS certificates. These certificates are valid for 90 days, and can be renewed any number of times, still free of charge.

Requesting a certificate is quite easy: they have developed a script to automate the requesting and renewal processes. In Ubuntu 16.04, this script is available as a package, so that we just need to use the package manager to install it:

sudo apt-get install letsencrypt

At the time of writing this, while the installation of these certificates on Apache servers is quite automated, it is still mostly manual on nginx. This means that we have to run the script in manual mode, and then add the certificate to the nginx configuration file.

We will start by using letsencrypt with the the certonly parameter. Also, we will add the --http-01-port [port] and --tls-sni-01-port [port] options, indicating two unused ports under which our server is reachable. In my case, the command might look like this:

sudo letsencrypt certonly --http-01-port 36099 --tls-sni-01-port 36100

We will get an ncurses based interface in which we will have to provide a contact email for urgent notifications, then, accept the Terms of Service, and finally, the domain name for which we want to request the certificate.

Once it has that information, the letsencrypt script will use the ports we passed as arguments to validate our request by calling in a connection attempt from the Let’s Encrypt website to the given ports on our domain name. In case we did not pass the --http-01-port [port] and --tls-sni-01-port [port] options, the ports will default to 80 and 443.

In case the ports we provide are in use, or not reachable, the script will fail, and we will not get our certificate, and we will need to fix the issues and try again.

Once we succeed in requesting one or more certificates, they will be stored under /etc/letsencrypt, as well as any renewals which happen over time. We will always find the last instance of our certificates symlinked inside the /etc/letsencrypt/live directory, and it is those file names that we should include in the configuration of our web server.

Finally, to automatically renew our certificates when their expiration date gets close, we need to include the letsencrypt script in our root user’s crontab. Mine looks like this:

# Two checks every day, at 3:37 and 15:37. Reload nginx each time, if the check is successful.
37 3,15 * * * /usr/bin/letsencrypt renew --http-01-port 36099 --tls-sni-01-port 36100 &>/dev/null && /usr/sbin/nginx -s reload &>/dev/null

Again, do not forget to indicate alternative ports for the validation: the default ports won’t be available, because our web server will be using them! After updating our certificates when it is necessary, the script will also make sure our links in /etc/letsencrypt/live stay up to date.

3. The web server

a. Software installation

Ubuntu 16.04.1 provides four packages for nginx: nginx-light,nginx-core,nginx-full and nginx-extras. These packages differ in the number of available features, which have to be included as plugins at compilation time.

I will go for the nginx-core package, as it includes everything I will need for my setup, but not much more, so that it will not eat up too much resources.

As with every package in the Ubuntu repositories, installation is as easy as:

sudo apt-get install nginx-core

b. Main nginx settings

After the installation, I need to tweak some settings. To better explain them, I will break down the sections in the file. I will also highlight my changes and additions to each section in bold.


    ##
    # Basic Settings
    ##

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    server_tokens off;  # Let's hide which nginx version we use.

    # Have utf-8 as default encoding: Useful if your native language is not English
    charset utf-8;

    map_hash_bucket_size 64;  # I had to fix this for some configuration

    # Increase this to be able to have more virtual hosts
    server_names_hash_bucket_size 64;

    # server_name_in_redirect off;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

The SSL section required some more work to discard some vulnerable ciphers. Most of this comes from Qualys SSL Labs forums, but I lost the link to the exact thread. They also have a nice tool to test SSL on your server.

In this page they also give some nice hints on security and SSL: https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html.


    ##
    # SSL Settings
    ##

    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:EDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
    ssl_dhparam /etc/nginx/dhparams.pem;  # See the 'raymii.org' page to an example on how to generate this file.

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;

    # Prevent switching to HTTP, also on subdomains
    add_header Strict-Transport-Security "max-age=31536000"; includeSubDomains

The logging section is a little bit trickier than the default one. For first, I define the $loggable variable depending on the address ($realip_remote_addr): it will be true (one) for any address but those in the LAN range (zero). I will use this for conditional logging inside the virtual hosts. Also, as I will have zillions of visitors in my site, so that will I just disable the access log by default to improve performance and save me many Mb in logs:


    ##
    # Logging Settings
    ##

    map $realip_remote_addr $loggable {
        ~192\.168\.1\.\d+    0;
        default              1;
    }

    # access_log /var/log/nginx/access.log combined if=$loggable;
    error_log /var/log/nginx/error.log;

The gzip section is also easy: I just uncommented all the lines in the section in the default config file:


    ##
    # Gzip Settings
    ##

    gzip on;
    gzip_disable "msie6";

    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/javascript text/xml
 application/xml application/xml+rss text/javascript;

After the gzip block, I added some configuration to ban spammers. You can find the details here. Do not forget to add the update script to your cron.daily dir!


    ##
    # Block lasso spammers
    ##

    include drop.lasso.conf;

Finally, the last two sections, the Virtual hosts and the mail configuration I left unchanged.

c. Hosts configuration

There are four virtual hosts configured on my server. In this post I will only talk about three of them. The final one, the one for my git repository, I did already mention in one of my previous posts.

These virtual hosts are defined as individual files inside /etc/nginx/sites-available, and symlinked into /etc/nginx/sites-enabled to let nginx see them.

- HTTP to HTTPS redirection

This is an easy one: just listen on port 80, on all IPv4 and IPv6 interfaces, and redirect to the same URI, served over HTTPS, with a 301 HTTP code.


server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 301 https://$host$request_uri;
}

- The proxy to my testing server

This one is not very important: it will only work from time to time, as I only turn on my testing server from time to time, when I want to test something.


server {

    server_name testing.invik.xyz;

    root /var/www/html;  # This is empty, but should never be served. Just in case.

    # SSL configuration: listen on port 443 of every IPv4 and IPv6 interface,
    # and answer using the http2 protocol.
    #
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

	# Auth: allow LAN access without credentials, but require basic HTTP auth
    # for any access outside the LAN.
    #
    satisfy any;

    allow 192.168.1.0/27;
    deny all;

    auth_basic           "RESTRICTED ACCESS";
    auth_basic_user_file /etc/nginx/testing.passwd;

    # Log all accesses from outside the LAN range. Remember the '$loggable' var?
    access_log /var/log/nginx/testing.access.log combined if=$loggable;

    # This virtual host has its own SSL certificate:
    ssl_certificate /etc/ssl/certs/letsencrypt_testing_cert.pem;
    ssl_certificate_key /etc/ssl/private/letsencrypt_testing_key.pem;

	# Proxy policy definition
    location / {
        include proxy_params;

        # Pass down the request using the hostname
        proxy_pass https://testing;
    }
}

- The web site - this blog

And last, this is the virtual host configuration for this blog:


server {

    server_name invik.xyz;

    root /var/www/invik.xyz;  # the root address for the blog

    # SSL configuration: listen on port 443 of every IPv4 and IPv6 interface,
    # and answer using the http2 protocol. Also, take notice this is the default host,
    # so that it will respond when none of the others does.
    #
    listen 443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server;

    # This virtual host has its own SSL certificate:
    ssl_certificate /etc/ssl/certs/letsencrypt_main_www_cert.pem;
    ssl_certificate_key /etc/ssl/private/letsencrypt_main_www_key.pem;

    # Custom Jekyll error pages
    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;

    # Default pages to be served if none is specified in the URI
    index index.html index.htm;

    location / {
        # First attempt to serve request as file, then
        # as directory, then fall back to displaying a 404.
        try_files $uri $uri/ =404;
    }

    ##
    # Content Browser Caching: 2 weeks for images, scripts and style files.
    ##

    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 2w;
    }
}

4. The blog

I already mentioned that this blog is based on Jekyll, using the Minimal Mistakes theme.

To use this theme, or any other one based on the same platform, just install Jekyll from the Ubuntu repositories. We will also take care of some dependencies we will need later:

sudo apt-get install jekyll ruby-dev libffi-dev gcc make zlib1g-dev

After installing Jekyll, we will need bundler to handle the dependencies. It is also available as a Ubuntu package, but the version included in the repositories is too old, so that we will install the ruby gem version, which is more recent:

sudo gem install bundler

Once we have the required software, we clone the theme to the place where we will edit it. Then we move into that directory, and use bundler to install the dependencies:

git clone https://github.com/mmistakes/minimal-mistakes.git /target/dir
cd /target/dir
bundler install

This should take care of downloading and building all the required dependencies. Once this is done, you just have to customize _config.yml to fit your needs, add some content to your blog, and then build the entire site. For this, the recommended command is:

bundle exec jekyll build -d [production_dir]

Just issuing jekyll build or jekyll build -d [production_dir] should also do it, but the bundler exec variant should be able to take care of any changes in the dependencies, in case you updated your theme.

Finally, in case you are interested, there are another two alternative options to deploy this theme: to deploy on github pages, and also a gemified version of the theme.

Leave a comment