Introduction

My go to web server has always been NGINX and it’s been my default choice since I started this blog. Any time I need a web server it’s without question NGINX. That is until recently. I’ve been playing around with containerization and specifically making my vaultwarden deployment better. But that’s for another post.

As I was looking into this, the Caddy web server kept coming up. Vaultwarden seems to really like it. So I put some time into learning how to deploy it and I’m impressed. So much so I decided to convert this blog from NGINX over to Caddy.

At least for my use, Caddy makes a lot of things easier. That said, I did not evaluate enterprise use cases and I have no idea how or what with Caddy when it comes to things like clustering, high availability, load balancing, or high load performance metrics. I’m looking at this from the standpoint of needing a single server that sees low traffic. Ease of configuration, maintenance and security are all I really care about.

Why Do I Like Caddy

Caddy is really easy to work with. It does so many things by default that NGINX needs explicit configuration.

TLS

TLS is built in and a major focus of the software. Caddy automatically handles getting a TLS cert and it handles http to https redirects by default. You actually have to configure Caddy to not use TLS if you don’t want it. Which is the opposite of pretty much every other web server that defaults to http and you have to provide a TLS certificate to enable TLS.

Caddy will use one of several ACME providers, including Let’s Encrypt, for the TLS certificates. It handles renewal and everything for you using HTTP and TLS-ALPN challenge methods. However, there are a number of plugins you can install that support other methods. Caddy handles generating certificates across every configured domain. If you have a domain listed it will get a certificate.

Wildcards

Caddy can support wild card certificates using the DNS challenge but you’d need to install a plugin. I was previously using a wild card certificate but with Caddy transparently handling every listed domain as a built in operation, I really don’t need wild card certificates anymore.

This simplifies things for me even further.

HTTPS Upgrade

Caddy redirects http to https by default with no configuration necessary.

Redirects

I have multiple domain names that I have redirecting to the main domain. All I needed to do was list the domains and put a simple redirect directive in the Caddyfile.

Secure TLS Configuration

By default Caddy uses strong crypto algorithms and ciphers for TLS connections. As well as strong protocols. Specifically, only TLSv1.2 and TLSv1.3 are enabled.

Less Configuration

I had to configure far less with Caddy than what’s required with NGINX. At least for my needs. Pretty much everything you’d want is already enabled by default. There also seemed to be less boiler plate configuration.

Less Maintenance

Caddy building in a lot of things means there is less maintenance too. For example, a while back I updated my NGINX configuration to disable TLSv1.1 and enable TLSv1.3. This required me to edit the NGINX configuration to explicitly state what TLS versions I wanted enabled. I also had to lookup secure cipher lists and provide a list of which ones the server should use.

With Caddy, this is all built in. It already handles best practices for TLS version and ciphers. If I were using Caddy back then, it would have just happened when I installed monthly updates and pulled down the release that defaults to TLSv1.3 enabled and TLSv1.1 disabled.

Another thing that I like is HTTP protocols. If Caddy add support for a protocol is enabled by default. When NGINX added support for HTTP/2 I had to keep track of when it was supported, what version, the version I had installed, then I had to make configruation changes myself. I’m in the same position with HTTP/3 with NGINX. With Caddy, HTTP/2, and HTTP/3 are supported without me having to do any configurate. I just have to start Caddy and it’s there.

Server Changes for Caddy

Moving to Caddy was easy but there were a few hiccups I had to work through. I had to make a few changes to the server and blog specifically for Caddy. Some small and some big.

Certbot

The most trivial change I made was removing Certbot. Since Caddy handles TLS certificates I didn’t need Certbot anymore.

uWSGI

One of the hiccups I ran into was with the uWSGI service I use for my search service. This is a small Python app that provides full text search for my Blog posts. Caddy reverse proxies requests to the service so I don’t have the service exposed to the internet.

I was running the uwsgi app using a unix socket for connectivity. Caddy is supposed to work with unix sockets but I couldn’t get it to work no matter what I tried. In the end I switched to having the service listen on 127.0.0.1:8181 and had Caddy reverse proxy via an HTTP request instead of a unix socket.

This really isn’t a big deal in the grand scheme of things but does reduce security slightly because anything on the local machine can access the search service. Whereas, with the socket, only processes allowed to read/write to the socket via file permissions could access the search service.

Files Changes

Two files had to change

/etc/uwsgi/search.ini

This line was removed

socket = /run/uwsgi/%n.sock

And replaced with

http = 127.0.0.1:8181

/etc/systemd/system/uwsgi-app\@search.service

The systemd service file also needed to be tweaked. The ExecStart line had to remove the reference to the socket.

ExecStart=/usr/bin/uwsgi \
        --ini /etc/uwsgi/%i.ini

Search Service Javascript

This was a bit more interesting of a change and took way too long for me to figure out.

On the search page, there is an input box for the search query. When you enter text in the search box and press enter the Javascript uses window.location.href to redirect you back to the search page with the search query parameters. If there are search query parameters when the page loads, the Javascript runs the search and populates the results under the search box.

The problem I was having was the redirect was taking place but the query parameters were always missing. Interestingly it was due to the way Caddy serves static files and the structure of my Blog. The structure is the same when I was using Jekyll so it didn’t break from switching to Hugo. It was also working with NGINX so this was purely a difference between NGINX and Caddy.

In search_box.js

The Javascript was written like this and wasn’t working.

window.location.href = "/search?" + params;

Changing it to this made searching work again.

window.location.href = "/search/?" + params;

Basically, “search” is a directory with an index.html file inside of it. You can go to https://nachtimwald.com/search/index.html and you’ll see the same page. The index.html is just hidden like when you go to the “/” root page and it loads “/index.html” without showing “index.html” in the URL. NGINX would allow me to do “/search?” and just work but Caddy recognizes “/search” as a directory and while it transparently renders the “index.html” it still treats “/search” as a directory. Hence needing the explicit “/” at the end before the “?”.

So changing ? to /? in the Javascript redirect fixed the issue.

Caddy Configuration

I’ve been going on and on about Caddy and the configuration so it’s about time I actually showed it.

/etc/caddy/Caddyfile

{
	admin off
	log {
		output file /var/log/caddy/caddy.log {
			roll_size 10MB
			roll_keep 10
		}
	}
}

(secureHeaders) {
	header {
		Strict-Transport-Security "max-age=31536000; includeSubdomains; preload"
		Permissions-Policy interest-cohort=()
		X-Content-Type-Options nosniff
		X-XSS-Protection "1; mode=block"
		X-Frame-Options "Deny"
		Content-Security-Policy "Content-Security-Policy "default-src 'self'; script-src 'self'; img-src 'self' data:; style-src 'self'; font-src 'self'; object-src 'none'"
	}
}

(allowedMethods) {
	@denyMethods {
		not method GET HEAD
	}
	respond @denyMethods "HTTP Method Not Allowed" 405
}

nachtimwald.com {
	root * /srv/http/nachtimwald.com
	file_server
	encode gzip

	log {
		output file /var/log/caddy/nachtimwald.com {
			roll_size 10MB
			roll_keep 10
		}
	}

	import secureHeaders
	import allowedMethods
}

https://search.nachtimwald.com {
	reverse_proxy 127.0.0.1:8181
	encode gzip

	log {
		output file /var/log/caddy/search.nachtimwald.com {
			roll_size 10MB
			roll_keep 10
		}
	}

	import secureHeaders
	header {
		Access-Control-Allow-Origin https://nachtimwald.com
	}
	import allowedMethods
}

niw.cx, niw.cz, niw.sx, http://, https:// {
	redir https://nachtimwald.com{uri}
}

Something to know is Caddy is really picky with the Caddyfile formatting. Like if you use spaces instead of tabs to indent lines it won’t load the file. This is such a problem they provide the caddy fmt tool to fix a Caddyfile that won’t load due to format problems. Use caddy fmt --overwrite /etc/Caddyfile if Caddy complains.

Breakdown

Global

{
	admin off
	log {
		output file /var/log/caddy/caddy.log {
			roll_size 10MB
			roll_keep 10
		}
	}
}

The global section turns off the admin interface and sets up file logging for server event logs.

Snippets

Caddy allows you to create reusable snippets that can be used in multiple places. I have two because I define the main site and search service domains that share some of their configuration.

Headers

I set a few additional headers that Caddy does not for some additional security.

(secureHeaders) {
	header {
		Strict-Transport-Security "max-age=31536000; includeSubdomains; preload"
		Permissions-Policy interest-cohort=()
		X-Content-Type-Options nosniff
		X-XSS-Protection "1; mode=block"
		X-Frame-Options "Deny"
		Content-Security-Policy "Content-Security-Policy "default-src 'self'; script-src 'self'; img-src 'self' data:; style-src 'self'; font-src 'self'; object-src 'none'"
	}
}

Allowed Methods

(allowedMethods) {
	@denyMethods {
		not method GET HEAD
	}
	respond @denyMethods "HTTP Method Not Allowed" 405
}

This isn’t straitly necessary but since this is a static site it won’t hurt to restrict requests to GET and HEAD only. Even though the search entry is an input box, it isn’t a POST and the Javascript does a GET request to the search service. This is how the Ajax results populate on the page without redirecting like would happen with a POST.

Domains

nachtimwald.com {
	root * /srv/http/nachtimwald.com
	file_server
...
}

https://search.nachtimwald.com {
	reverse_proxy 127.0.0.1:8181
...
}

The two domain sections are very similar. The big difference is setting file_server for the Blog and reverse_proxy to forward requests to the search search.

Otherwise, they setup their logging and use the snippets defined earlier. The search entry also adds an additional header to allow the search Javascript running on one domain to send requests to the sub domain for searching. Without this header browsers would error.

Redirect

niw.cx, niw.cz, niw.sx, http://, https:// {
	redir https://nachtimwald.com{uri}
}

The redir directive redirects anything that matches to the main site with a TLS connection.

Beofre we look at the list of explicit domains, let’s start with the http://, https:// part. These two are catch-alls. If you put in the IP address of the server directly without a domain this entry will trigger. Either HTTP or HTTPS. This also catches any domains that point to the server, via DNS, but aren’t explicitly listed.

I don’t rely on the catch all for my known domains and instead I have a list of domains that I have DNS records for that I want redirected. They are listed specifically for TLS certificates. Any domain listed will have a TLS certificate issued. If this wasn’t in place the TLS connection would have a mismatch before the redirect takes place resulting in an error. Not a problem if going to http://alias-domain but definitely a problem with https://alias-domain.

SSLabs Report

Testing with SSLabs shows an A rating! Looking through the report it’s pretty much exactly the same as the configuration I was using with NGINX. Which I had to research and manually configure. It’s far less work letting Caddy keep the site as secure as it should be without me having to research what configuration changes I should make.

Conclusion

I’m very pleased with how little work went into setting up and securing Caddy. I’ve been able to replicate everything I was doing with NGINX and Certbot all within Caddy and without any loss in functionality. It just works and works well. Going forward I’m going to be using Caddy instead of NGINX as my go to web server.