The need for sensible defaults

A case of switching from Nginx to Caddy

When installing a new VPS for a new project, I had to dive in my previous nginx boilerplate configuration files to create a new one, slightly different, to accomodate for my various needs on the project.

While doing so, I remembered how long and tedious these configuration files were — let's see for example a simple reverse proxy for a simple blog like Ghost, with redirects and SSL (this is not optimized - just a quick snippet):

server {
    listen 80;
    server_name www.myghost.blog myghost.blog;
    return 301 https://www.myghost.blog$request_uri;
}
server {
    server_name myghost.blog;

    ssl_certificate /etc/letsencrypt/live/myghost.blog/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myghost.blog/privkey.pem;

    return 301 https://www.myghost.blog$request_uri;
}
server {
    server_name www.myghost.blog;

    client_max_body_size 10m;

    listen 443 ssl;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    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:kEDH+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_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_stapling on;
    ssl_stapling_verify on;
    add_header Strict-Transport-Security max-age=15768000;

    ssl_certificate /etc/letsencrypt/live/myghost.blog/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myghost.blog/privkey.pem;

    location / {
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   Host      $http_host;
        proxy_set_header   X-Forwarded-Proto https;
        proxy_pass         http://127.0.0.1:4369;
    }
}

This is a lot (and some parts are missing here still), and it's mainly here to compensate the facts that the defaults are not suited.

I decided to look for an alternative web server, that would be production-grade, but that would be much more easy to configure while enforcing safe standards by default.

... and I came across Caddy.

The same configuration in Caddy looks like this :

myghost.blog, www.myghost.blog {
    encode zstd gzip
    reverse_proxy localhost:4369 {
        header_up X-Forwarded-Proto {scheme}
    }
    header {
        Strict-Transport-Security max-age=31536000;
        X-Content-Type-Options nosniff
        Referrer-Policy no-referrer-when-downgrade
    }
}

And that's it ! No supplementary configuration, no need to indicate the SSL certificates and the sensible ciphers or the stapling (Caddy provision and renew them automatically), and every default is well-chosen and does not need tweaking (in this case).

This gives an A+ in SSL labs with basically no effort :

Performance-wise, I find it on par with what nginx does, delivering roughly the same amount of requests per second. I haven't extensively tested it yet though — your mileage may vary.

If you read through the docs (https://caddyserver.com/docs/), you can see all the options that are available for all the directives. It's pretty modular and you can change pretty much every aspect of the server.

But in most cases, you don't need too.

The need for sensible defaults

In fact, what this boils down to is that the extensive configuration in nginx is very powerful, but it lacks consistent sensible default values for all of them.

I find that Caddy is quite opinionated in this regard, but this is quite a good thing in fact; when you read the Caddy configuration, you instantly get, in 10 lines, the full grasp of what it does.

You don't even need to scroll to see the full configuration file.

A lot of expert, production-grade software would clearly benefit from this approach. When using sensible (and probably opinionated) default values :

  1. the configuration is easier, simpler, shorter,
  2. it promotes good practices,
  3. you can configure it right, even if you don't know all the configuration specifics.

It has cons, too. Specifically, you can lose touch with all the under-the-hood technicalities of the software you use, where all the defaults are hidden at first sight.

But I think that this is a false problem; The majority of nginx configuration files I encountered along my way were mere patchworks of copy/paste of StackOverflow answers. So a long, convoluted, very specific configuration does not necessarily mean that the person who uses it does understand all the ins and outs of it. There's no relation between your expertise and the use of default values, if they are well-chosen.

And "opinionated" means that different best practices exist. In the case of Caddy vs. Nginx, I think we can all agree that enforcing HTTPS, and setting everything so that your website gets an A+ in the ssllabs test is enough a consensus to be declared a universal best practice. But it may depend on the software.


Bonus: Some more advantages of Caddy

  • Automatic certificates and renewal via LE
  • Static binary, no dependencies
  • Zero-downtime configuration reload

Give it a try !