Hugo, Let's Encrypt, and Caddy

· Submitted · Read in about 5 min · (1054 Words)
tags: · tech · programming ·

In February, I converted my site to the static site generator Hyde from WordPress. However, after using Hyde for a few weeks, I had some concerns. The setup was more complex than I liked, and the version of Hyde that I was using was already deprecated. I wanted writing to be simpler, and Hyde wasn’t meeting that goal.


I researched various SSGs and settled on Hugo. It’s a nice and tidy single executable and is very easy to extend via shortcodes. It’s also still in active development, which is nice. Because I’d already converted all my content to Markdown, it was easy to switch generators. The structure of Hugo is easy to follow, and you can override any style/code/option by creating a copy of the file in your layouts folder.

Here’s how easy it was to implement sidenotes on Hugo, which took me a week with Hyde:

file: layouts/shortcodes/sidenotes.html
<label class="margin-toggle sn-number" for="{{ .Get 0 }}">
  <input class="margin-toggle" id="{{ .Get 0 }}" type="checkbox" />
  <span class="sn">{{ .Inner }}</span>

To use the shortcode, you just call it via {{% shortcode_name shortcode_parameters %}} syntax. To create a sidenote, I just wrap the text in the shortcode open/close: Here’s a sidenote {{%% sidenote 00 %}}some inner text!{{% /sidenote %}}.

and Vine embeds:

file: layouts/shortcodes/vine.html
<iframe src="{{ .Get 0 }}/embed/simple" width="600" height="600" frameborder="0"></iframe>
<script src=""></script>

I was able to get everything converted to Hugo in about two weeks. Ironically, I ended up starting with a theme named Hyde-Y and modifying it to the current style. I killed the scrolling headers but updated the sidenote CSS.

Let’s Encrypt

I have used StartSSL for many years, paying the $75 fee for their enhanced verification in order to get certificates for wildcard domain names. The enhanced verification is a pain if you don’t have any physical utility bills that reference your street address, which has been my situation for years as a renter. I was tired of faxing in my driver’s license and other documents just to sign up for another 2 years.

Let’s Encrypt is a free Certificate Authority that’s sponsored by some big tech companies, like Mozilla, Facebook, Cisco, and the EFF. They created a certificate request/renewal protocol called ACME and Let’s Encrypt is the first major CA to support ACME. There are several ACME clients for automating the request/renewal of certificates, including CertBot. CertBot was previously called the LetsEncrypt client and was provided directly by Let’s Encrypt. Now, the EFF is developing CertBot in order to prevent a monopoly/conflict of interest. The eventual goal of the entire project (Let’s Encrypt, ACME, etc) is to make certificates easy and ubiquitous, which includes many CAs supporting ACME for free certificates.

ACME has various ways of proving that you own the domain that you’re requesting a certificate for. HTTP-01 and DNS-01 are two of these challenges, which check for the existence of a code in a particular file on a website or DNS SRV record (respectively). I created an include file (letsencrypt.include) which I append to my nginx config files to automatically handle the serving of that code:

location /.well-known/acme-challenge {
    alias /etc/letsencrypt/webrootauth/.well-known/acme-challenge;
    location ~ /.well-known/acme-challenge/(.*) {
        add_header Content-Type application/jose+json;

All the ACME requests to the /.well-known/acme-challenge subdirectory are automatically routed to the /etc/letsencrypt folder structure and the appropriate headers added.

Here’s the command that I ran to originally create the certificates:

./certbot-auto certonly  --webroot -w /etc/letsencrypt/webrootauth -d -d  -d -d -d -d -d -d -d -d -d -d -d

I also added lines to my crontab to renew the certs automatically and to restart nginx to pick up the new certificate configuration:

0 */22 * * * /srv/letsencrypt/certbot-auto renew --rsa-key-size 4096 >> /var/log/letsencrypt/renew.log
30 3 * * 7 service nginx restart

Here’s my full nginx SSL configuration:

server {
    listen       23.***.***.***:444 ssl http2; ##more on the :444 later

ssl_certificate /etc/letsencrypt/live/****/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/****/privkey.pem;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_dhparam sites/enabled/dhparams.pem;

HTTP/2 and Caddy

HTTP/2 requires SSL, which is part of why I went through the trouble of getting certs for everything. However, I found out that my specific server configuration (Ubuntu 14.04, nginx) won’t be supported for HTTP/2 by Chrome browsers after May 15, 2016. Due to the version of OpenSSL that ships with Ubuntu 14.04, I can’t get ALPN support, which Chrome will require for HTTP/2. I could compile nginx myself with the right version of OpenSSL, but that’s just a big hassle that I don’t want to deal with managing and updating. I also don’t want to hop to Ubuntu 16.04 just a month after its release. I was stuck trying to figure out how to get HTTP/2 support without major work.

Enter Caddy. Caddy is a web server written in Go, and is also a nice single-file drop-in like Hugo. It’s designed to be as simple as possible, which I appreciate. I wasn’t ready to cut my entire site over to Caddy as I have some pretty gnarly nginx configs for custom redirection, etc. Instead, I simply put Caddy in front of nginx. Caddy terminates SSL flawlessly (‘A’ rating from SSLLabs, HTTP/2 supported out of the box) and simply proxies everything to nginx. I put Caddy on :443 and moved nginx to :444.

Here’s my caddyfile:



log / /var/log/caddy/access.log "{when} {proto} Request from {remote} type {method} to {path} proxy to {upstream} and status {status}" {
    rotate {
        size 100
        age 14
        keep 10

errors {
    log /var/log/caddy/errors.log {
        size 100
        age 14
        keep 10

tls /etc/letsencrypt/live/****/fullchain.pem /etc/letsencrypt/live/****/privkey.pem

header / {
    X-Frame-Options "SAMEORIGIN"
    X-Content-Type-Options "nosniff"
    Content-Security-Policy "default-src 'self' * * *; script-src 'self' * * * * data: 'unsafe-inline'; style-src 'unsafe-inline' 'self' * *; font-src 'self' *; img-src 'self' * data:;"
    Access-Control-Allow-Origin "{scheme}://{host}"

proxy / https://23.***.***.***:444 {
    proxy_header Host {host}
    proxy_header X-Real-IP {remote}
    proxy_header X-Forwarded-For {remote}
    proxy_header X-Forwarded-Proto {scheme}

All requests are piped back to my nginx instance which handles serving the actual page. It’s been working great for a couple of days now!

I am hoping that I’m finished tweaking my site for a while, so that I can get back to focusing on building out my networking labs.