Skip to content

Cookie settings

Choose which kinds of cookies you allow. You can change your choice at any time via Cookie settings at the bottom of every page. Privacy statement

Necessary

Always on

Needed to log in, keep your cart and remember your choices. These are always on.

Which cookies?
  • weboke_sessie, weboke_klant, weboke_remember: your session and staying logged in
  • weboke_basket: your cart
  • wkBtwIncl: showing prices with or without VAT
  • wok_cookies: your cookie choice, 12 months

Measures which ad leads to a visit or order, so we do not pay for ads that do nothing.

Which cookies?
  • Google Ads via Google Tag Manager (_gcl_au, _gcl_aw), up to 90 days
  • OpenAI Ads, ads in ChatGPT
  • wok_aff: which partner referred you to us (affiliate programme), 60 days

4.6

Country and language

Each country has its own site, with prices and VAT for that country.

View

Module 7 · 7.4

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 caddy

Use 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 -f

The 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=31536000

The 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:

  1. Only turn on HSTS once https works everywhere and renewal is automatic (lesson 7.3).
  2. Start with a short max-age, for example one day (86400), and increase it step by step to a year.
  3. Be careful with includeSubDomains: it applies to every subdomain, including an old subdomain with a device or service that has no valid certificate.
  4. preload asks for inclusion in a list built into browsers (hstspreload.org). Among other things, this requires a max-age of 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:

HeaderProtects againstValue to start with
X-Content-Type-Optionsthe browser guessing what a file is, so an uploaded "image" can run as a scriptnosniff
Content-Security-Policy: frame-ancestors (or the older X-Frame-Options)clickjacking: another site loads your page invisibly in a frameframe-ancestors 'self' (or X-Frame-Options: SAMEORIGIN)
Referrer-Policythe address of your page (with search terms or order numbers) leaking to other sitesstrict-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-origin

security.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.txt

The rules:

  • Contact is required: a mailto:, https:// or tel: address where reports end up. Use an address that someone actually reads.
  • Expires is 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, Canonical and Policy (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.txt is 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-8

If 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_proxy passes on the right headers automatically.
  • HSTS makes browsers remember that your site is https only; build up max-age, be careful with includeSubDomains and very careful with preload.
  • Also set X-Content-Type-Options: nosniff, frame-ancestors 'self' and a Referrer-Policy; in Caddy with header, in nginx with add_header ... always, and check with curl -I.
  • security.txt lives at /.well-known/security.txt, over https, as plain text, with a required Contact and one Expires within a year.

Quiz

1. Why does HSTS help when you already redirect http to https?



2. You set HSTS with max-age=31536000; includeSubDomains. A week later, camera.example.com turns out not to have a valid certificate. What do visitors who have been to your site before notice?



3. Which fields are required in a security.txt?



4. In nginx you put the security headers in the server block, but they are not sent for /api/. The location /api/ block has one add_header of its own. What's going on?



5. What must you do before adding preload to your HSTS header?



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.

Exercise Z7.4: HSTS, headers and security.txt

Only for registered students. We fetch https://<naam>/ and /.well-known/security.txt (GET only).




What you enter and what we find isn't stored. Your browser remembers your answers and whether you've passed the exercise. If you're signed in, we only save that you got the exercise (almost) right in My courses; you can delete that there too.

Further reading

One moment…

Set up your domains

For each domain, arrange the nameservers, the link to your hosting and who becomes the owner.

Loading your domains…