Module 7 · 7.4
English
Caddy, HSTS and security.txt
Module 7: Domain, web server and https · Reading time: 12 minutes
Learning objective: After this lesson you can use Caddy as a simple web server with automatic https, set HSTS and the most important security headers (in Caddy and nginx) without making your site unreachable, and publish a valid security.txt.
Key concepts: Caddy, Caddyfile, automatic https, reverse_proxy, file_server, header, HSTS (Strict-Transport-Security), max-age, includeSubDomains, preload, X-Content-Type-Options, frame-ancestors, Referrer-Policy, security.txt, Contact, Expires, /.well-known/
Caddy: https without a second thought
In lessons 7.2 and 7.3 you built a site with https using nginx and certbot. That is two programs, a few files and a timer. Caddy does it all in one: it is a web server with a built-in ACME client. If a name is in the configuration, points to the server and ports 80 and 443 are reachable, Caddy fetches a certificate itself, renews it in time and redirects http to https.
Caddy is in the Debian and Ubuntu package repositories (a somewhat older version); its makers also offer their own apt repository with the latest version, with its own key and Signed-By as in lesson 4.1. In module 9, Caddy runs as a container in front of your Docker services (lesson 9.4). Here you use it as an ordinary service.
anna@web1:~$ sudo apt install caddy
anna@web1:~$ systemctl status caddyUse either nginx or Caddy on ports 80 and 443, not both: only one program can listen on a port.
The Caddyfile
The configuration lives in /etc/caddy/Caddyfile. A static site and an application behind a reverse proxy:
example.com, www.example.com {
root * /var/www/example.com
file_server
encode zstd gzip
}
app.example.com {
reverse_proxy 127.0.0.1:3000
}That's all. Caddy automatically listens on IPv4 and IPv6, requests certificates for the three names, redirects http to https, and with reverse_proxy it passes on the headers X-Forwarded-For and X-Forwarded-Proto by itself (something you did by hand in nginx, lesson 7.2).
Testing and reloading works just like with nginx:
anna@web1:~$ caddy validate --config /etc/caddy/Caddyfile
Valid configuration
anna@web1:~$ sudo systemctl reload caddy
anna@web1:~$ sudo journalctl -u caddy -fThe journal shows the certificates being fetched. If something goes wrong, the reason is there: usually the same as with certbot (port 80 closed, the name points somewhere else, broken IPv6; lesson 7.3).
HSTS: making the browser remember it's https
Redirecting http to https has one weak moment: the very first request over http is still unencrypted. Someone on the same network can intercept it and keep the visitor on http. HSTS (HTTP Strict Transport Security, RFC 6797) solves this. Over https, your site sends this header:
Strict-Transport-Security: max-age=31536000The browser then remembers for a year (31,536,000 seconds) that this name may only be used over https. If the visitor later types http://, the browser turns it into https itself, before anything goes over the network. The browser ignores an HSTS header that arrives over http; it only counts over https.
There is a downside. Once a browser has remembered HSTS and sees a certificate error (expired, wrong name), the visitor gets no button to continue anyway. Your site is then unreachable for those visitors until the certificate is correct. So:
- Only turn on HSTS once https works everywhere and renewal is automatic (lesson 7.3).
- Start with a short
max-age, for example one day (86400), and increase it step by step to a year. - Be careful with
includeSubDomains: it applies to every subdomain, including an old subdomain with a device or service that has no valid certificate. preloadasks for inclusion in a list built into browsers (hstspreload.org). Among other things, this requires amax-ageof at least a year,includeSubDomains, and https on all subdomains. Getting removed takes months. Only do it if you are sure the whole domain is https for good.
The other security headers
Besides HSTS, a few headers help a lot for little effort:
| Header | Protects against | Value to start with |
|---|---|---|
X-Content-Type-Options | the browser guessing what a file is, so an uploaded "image" can run as a script | nosniff |
Content-Security-Policy: frame-ancestors (or the older X-Frame-Options) | clickjacking: another site loads your page invisibly in a frame | frame-ancestors 'self' (or X-Frame-Options: SAMEORIGIN) |
Referrer-Policy | the address of your page (with search terms or order numbers) leaking to other sites | strict-origin-when-cross-origin |
A full Content-Security-Policy (which scripts and styles may load) is tailored to each site and is outside the scope of this course; start with just frame-ancestors.
In Caddy you set them with the header directive in a site's block:
example.com, www.example.com {
root * /var/www/example.com
file_server
header {
Strict-Transport-Security "max-age=31536000"
X-Content-Type-Options "nosniff"
Content-Security-Policy "frame-ancestors 'self'"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
}Caddy does not set HSTS or these headers itself, so you really have to add them. -Server removes the Server header. In lesson 9.4 you add your own X-Weboke-Cursus header for the exercise in the same kind of block.
In nginx you put the same lines in the server block for port 443:
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;always makes sure they are also sent with error pages. Watch out for a well-known nginx pitfall: if a location block has even one add_header of its own, the add_header lines from the server block no longer apply to that location. So always check the result:
anna@laptop:~$ curl -sI https://example.com | grep -iE 'strict|nosniff|frame|referrer'
strict-transport-security: max-age=31536000
x-content-type-options: nosniff
content-security-policy: frame-ancestors 'self'
referrer-policy: strict-origin-when-cross-originsecurity.txt: how someone can report a vulnerability to you
Suppose someone finds a vulnerability on your server, such as an open directory or an outdated service. How does that person know whom to warn? That's what security.txt (RFC 9116) is for: a small text file in a fixed place, https://<jouwdomein>/.well-known/security.txt.
Contact: mailto:security@example.com
Expires: 2027-10-31T23:00:00Z
Preferred-Languages: nl, en
Canonical: https://example.com/.well-known/security.txtThe rules:
Contactis required: amailto:,https://ortel:address where reports end up. Use an address that someone actually reads.Expiresis required and may appear only once: the date after which the file is considered outdated. The RFC recommends a date less than a year ahead. That way no file with a long-forgotten address is left lying around. Put a reminder in your calendar for when to renew it.Preferred-Languages,CanonicalandPolicy(a link to your reporting policy) are optional.- The file must be served over https, as plain text (
text/plain, UTF-8). The old location/security.txtis allowed as an extra, but/.well-known/takes precedence.
Putting it in place for a static site:
anna@web1:~$ sudo mkdir -p /var/www/example.com/.well-known
anna@web1:~$ sudoedit /var/www/example.com/.well-known/security.txt
anna@laptop:~$ curl -sI https://example.com/.well-known/security.txt | grep -i content-type
content-type: text/plain; charset=utf-8If you use the catch-all location ~ /\. { deny all; } from lesson 7.2 in nginx, put an exception before it: location ^~ /.well-known/ { }. For an application behind a reverse proxy, in Caddy you can point the path /.well-known/security.txt to a file with a handle block and file_server.
A common mistake: a site or application that answers every unknown path with the home page and status 200. Then security.txt "exists", but it's HTML. So check the content too.
On AlmaLinux and Rocky Linux
There, Caddy comes from the makers' repository or from EPEL/COPR. As with nginx, SELinux has to allow a reverse proxy (setsebool -P httpd_can_network_connect 1). The Caddyfile and the headers are the same.
Summary
- Caddy handles certificates, renewal and redirecting to https itself;
reverse_proxypasses on the right headers automatically. - HSTS makes browsers remember that your site is https only; build up
max-age, be careful withincludeSubDomainsand very careful withpreload. - Also set
X-Content-Type-Options: nosniff,frame-ancestors 'self'and aReferrer-Policy; in Caddy withheader, in nginx withadd_header ... always, and check withcurl -I. - security.txt lives at
/.well-known/security.txt, over https, as plain text, with a requiredContactand oneExpireswithin a year.
Quiz
Exercise
Exercise Z7.4: HSTS, headers and security.txt. For your site, set HSTS (at least 180 days, once https has been working well for a while), X-Content-Type-Options: nosniff and frame-ancestors 'self' (or X-Frame-Options), and publish a security.txt with Contact and an Expires within a year. Enter your hostname. The program fetches https://<hostnaam>/ and checks the headers, and fetches https://<hostnaam>/.well-known/security.txt and checks the fields. It stores nothing except the result and the Expires date.
Further reading
- Website security: HSTS and security headers for your website
- Caddy, Automatic HTTPS: https://caddyserver.com/docs/automatic-https
- Caddy, header directive: https://caddyserver.com/docs/caddyfile/directives/header
- RFC 6797, HTTP Strict Transport Security: https://www.rfc-editor.org/rfc/rfc6797.html
- The HSTS preload list and its requirements: https://hstspreload.org/
- RFC 9116, security.txt: https://www.rfc-editor.org/rfc/rfc9116.html
- securitytxt.org (create and check): https://securitytxt.org/
