Securing your site with nginx

This week in the Netherlands the news hit again that some secure websites where vulnerable to a downgrade attack. This attack is not new, but for the average user it is hard to detect. You have to be careful that you see the lock when you are entering your credentials.

Fortunately, most new web servers and browsers have a setting for it, called HTTPs Strict Transport Security (HSTS). With that feature enabled, if your browser has ever contacted a website over a secure link (HTTPS), then it will not allow a downgrade to plain HTTP for that host. This of course means that you are more secure, at least as long as you watch out for certificate warnings. I use the nginx webserver, and use some other things for security, which I’ll share with you below. The SSLLabs test will give this configuration an A+ currently.

Edit 26/3: @okoeroo gave me a better list of ciphers which scores even higher with SSLabs:

    ssl_ciphers  ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA:DHE-RSA-CAMELLIA256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-RSA-CAMELLIA256-SHA:ECDH-RSA-AES256-SHA:TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:TLS_DHE_RSA_WITH_AES_128_CBC_SHA:CAMELLIA256-SHA:AES256-SHA;

The nginx webserver is a delight to configure. I’ve been using Apache for a long time, but moved over to nginx some time ago. I had planned a couple of days to switch, but I was done in an afternoon. nginx configuration files are simple, brief and easy to understand. I’m going through the configuration of this site here piece by piece, the complete configuration file is included at the end for your copy-paste enjoyment.

server {
        listen 80;
        listen [::]:80;
        server_name 1sand0s.nl;
        return 301 https://$server_name$request_uri;  # enforce https
}

The first part defines a server object that listens on IPv4 and IPv6 port 80, using the name 1sand0s.nl. It then uses a 301 (Moved Permanently) code to redirect the browser to the https variant. Note that this in itself is not HSTS yet, but comes close.

server {
        listen 443 ssl;
        listen [::]:443 ssl;
        server_name 1sand0s.nl;

Basic stuff so far to setup an SSL server listening on IPv4 and IPv6 port 443.

        ssl_certificate /path/to/certificate.crt;
        ssl_certificate_key /path/to/site.key;
        ssl_dhparam /path/to/dhparam.pem;
        add_header Strict-Transport-Security "max-age=15768000";
        ssl_prefer_server_ciphers   on;
        ssl_ciphers 'EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS';
        ssl_session_cache builtin:1000 shared:SSL:10m; 

This is where the important stuff happens. We add the SSL certificate and it’s key. For nginx the certificate should contain the complete chain to the trust anchor.

We also add an Ephemeral Diffie-Hellman parameters file to make sure that we use a 2048 bit key-exchange (otherwise the 2048 bit certificate would be kind of pointless). You generate this file using the ‘openssl dhparam -out dhparam.pem 2048‘ command. (Source).

The next line adds the Strict Transport Security header, with a max-age of about six months (taken from the RFC). Note the quoted “max-age=…” value, many nginx tutorials forget these and then it does not actually work.

Finally we get to the SSL ciphers. I have set the server to dictate the choice of ciphers, which is then defined in the next line. The ciphers at the end with “!” in front of them are explicitly disabled. The other ciphers are in order of preference. This list deliberately starts with ephemeral elliptic curve ciphers, and AES. These implement Perfect Forward Secrecy, which protects you from future key compromises. Another reason is because we should be moving away from RSA. This setting does mean that IE on XP will not be able to reach your site, but they have other problems to deal with anyway.

        ssl_stapling on;
        ssl_stapling_verify on;
        ssl_trusted_certificate /usr/local/etc/ssl/trustchain.crt;

OCSP is a protocol to check for certificate revocations. Browsers can do this, but it presents a privacy threat, and also adds overhead. However, the webserver can also do this itself. It regularly contacts the CA to get a signed response that the certificate is still valid, and then ‘staple’ this onto the handshake. To make sure that the CA is also not tampered with, the staple response is also verified using the trusted chain to that CA (the other certs you include in the certificate above).

        root /path/to/root/;
        index index.php index.html index.htm;

        access_log /var/log/nginx/1sand0s-access.log;
        error_log /var/log/nginx/1sand0s-error.log;

        client_max_body_size 10G; # set max upload size
        fastcgi_buffers 64 4K;

        location / {
           if (!-e $request_filename) {
               rewrite ^ /index.php last;
           }
        }

        location ~ ^(.+?\.php)(/.*)?$ {
                try_files $1 = 404;

                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$1;
                fastcgi_param PATH_INFO $2;
                fastcgi_param HTTPS on;
                fastcgi_pass 127.0.0.1:9000;
        }

        location ~* ^.+\.(jpg|jpeg|gif|bmp|ico|png|css|js|swf)$ {
                expires 30d;
                access_log off;
        }
}

The final part of the configuration defines the root of the site (note the trailing “/”). It defines the order in which index files are attempted, and the location of the log files for this server.
There’s a setting to be able to upload files, and a fastcgi buffer setting.

Then I continue with a rewrite rule for files that aren’t found so that WordPress can pick those up. WordPress itself is in PHP, so we need the fastcgi_pass to pass it on to a php_fpm daemon, with the appropriate settings. And then we close off with a caching setting for images, css, javascript and swf files that I copied from (an older version) of the nginx wiki on WordPress.

server {
        listen 80;
        listen [::]:80;
        server_name 1sand0s.nl;
        return 301 https://$server_name$request_uri;  # enforce https
}
server {
        listen 443 ssl;
        listen [::]:443 ssl;
        server_name 1sand0s.nl;
        ssl_certificate /path/to/certificate.crt;
        ssl_certificate_key /path/to/site.key;
        ssl_dhparam /path/to/dhparam.pem;
        add_header Strict-Transport-Security "max-age=15768000";
        ssl_prefer_server_ciphers   on;
        ssl_ciphers 'EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS';
        ssl_session_cache builtin:1000 shared:SSL:10m; 

        ssl_stapling on;
        ssl_stapling_verify on;
        ssl_trusted_certificate /usr/local/etc/ssl/trustchain.crt;

        root /path/to/root/;
        index index.php index.html index.htm;

        access_log /var/log/nginx/1sand0s-access.log;
        error_log /var/log/nginx/1sand0s-error.log;

        client_max_body_size 10G; # set max upload size
        fastcgi_buffers 64 4K;

        location / {
           if (!-e $request_filename) {
               rewrite ^ /index.php last;
           }
        }

        location ~ ^(.+?\.php)(/.*)?$ {
                try_files $1 = 404;

                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$1;
                fastcgi_param PATH_INFO $2;
                fastcgi_param HTTPS on;
                fastcgi_pass 127.0.0.1:9000;
        }

        location ~* ^.+\.(jpg|jpeg|gif|bmp|ico|png|css|js|swf)$ {
                expires 30d;
                access_log off;
        }
}

3 comments on “Securing your site with nginx

  1. Nice! Better than my site (which uses CAcert which still is not included in the trusted roots and hence scores a C at most; it seems CAcert didn’t get the desired structure).

    A colleague of mine (Onne Zweers) recently tested the site of Dutch banks with the SSL labs test: gaaturustigslapen.nl. While I disagree with the somewhat pretentious URL, the explanation of the different options to secure SSL where very insightful.

    Also thanks for your post on RSA. For a now security techie, it is hard to keep up, even though it is important to do so. So I value your profession effort to get this out ‘to the masses’ with posts like this.

    A question though: do you know the status of DANE? From what I understand, it seems like a good way to get a double certificate chain, and thus makes it harder for attacker to use a relative weak CA as an attack vector.

    • edit: … for a now security techie -> for a non-security techie…

      (while your post is insightful, the claim that it makes me an instant security guru might be a bit overly optimistic 😉 )

    • A question though: do you know the status of DANE? From what I understand, it seems like a good way to get a double certificate chain, and thus makes it harder for attacker to use a relative weak CA as an attack vector.

      This is actually something on my list to try out. I will post something more about managing keys in DNSSEC with ZSK and KSKs soon, and then after that I’ll also describe DANE.

      One thing that I’ve learned already is that Postfix actually supports this already and seems to actively use it to confirm certificates.

Comments are closed.