Zones, address sets (IPv4 and IPv6 mixed, dynamic sets with timeouts) and services, ordered rules in the early, input, forward and output chains, port forwards with hairpin NAT and IPv6 pinholes, source NAT, redirects, and switchable built-ins (loopback, conntrack, ICMPv6, antispoofing, MSS clamping, DHCPv6 client, flowtable). Every change is validated, checked with nft -c and applied in one transaction to Housecarl's own table, inet housecarl; changes made over the network roll back unless confirmed, and the confirmed ruleset is loaded at boot by housecarl-ruleset.service. An empty configuration applies nothing. Also: hit counters and the drop log (nflog), 'hc import' of an existing nftables ruleset and 'hc takeover', the hc CLI, the REST API and the web UI, users, scoped tokens (sets:write:NAME), the audit log, the web UI's certificate through Jarl, Debian packages, docs, unit tests with real nft 1.1.3, and an e2e test with real traffic (71 checks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.5 KiB
The web UI's certificate
Housecarl can serve its web UI and API over HTTPS in two ways:
- A certificate you manage:
tls.cert_fileandtls.key_fileinhousecarl.yaml. housecarld rereads them when they change (configuration.md). - A certificate it gets itself from Let's Encrypt (or another ACME CA), with Jarl publishing the DNS-01 challenge. That's this page.
With DNS-01 the web UI doesn't need to be reachable from the internet.
fw.adbraun.lol can point at a private address and the certificate
still works on your network. Only the challenge record has to be visible
on the internet, and Jarl puts it in every view that has your zone.
Setting it up
-
Connect Housecarl to Jarl with a user and a token that may only write Housecarl's own challenge record: jarl.md.
-
Give your web UI a name in DNS, such as
fw.adbraun.lolwith the Housecarl server's address, in Jarl. It is the name you will type in the browser. -
Configure the certificate in
/etc/housecarl/housecarl.yaml:listen: ["10.0.10.2:8070"] # an address your browser can reach tls: acme: domain: fw.adbraun.lol email: you@adbraun.lol # Let's Encrypt writes here if something is wrong directory: letsencrypt-staging # first; see below agree_tos: true # you accept Let's Encrypt's terms of service jarl: server: https://dns.adbraun.lol:8053 token_file: /etc/housecarl/jarl.token -
Restart housecarld (
sudo systemctl restart housecarl), and check:sudo hc cert check -
Watch it get the certificate:
sudo hc cert, or Server → Certificate in the web UI. Then openhttps://fw.adbraun.lol:8070/. -
Switch to the real CA. The staging certificate works, but browsers warn about it, because Let's Encrypt's staging service isn't trusted. It's there to prove everything works without touching Let's Encrypt's rate limits. Once it does, set
directory: letsencrypt, restart, and Housecarl gets a trusted certificate right away.
Until the first certificate arrives, the web UI is served over plain
HTTP. housecarld switches to HTTPS on the same port without a restart. From
then on, plain HTTP is redirected to HTTPS, except from the server itself
(127.0.0.1), so tools there keep working.
Checking
Before every certificate order, and whenever you ask (hc cert check, or Test in the web UI), Housecarl checks the whole path without
asking the CA. Let's Encrypt allows only 5 failed validations per name
per hour, so a broken setup shouldn't find out the hard way.
| Step | What it checks |
|---|---|
| Jarl | Jarl answers at jarl.server, and the token is valid (it says whose it is) |
| Zone | a primary zone the token can see holds _acme-challenge.<domain>, and in which views |
| Permission | the token may write the TXT record there (Jarl checks it as a dry run) |
| Write | a test record with a random value is written through Jarl, which loads it into named |
| Name servers | each of the zone's public name servers serves it, asked directly as the CA's resolvers will. A name server passes if any of its addresses does (tls.acme.check_propagation) |
| Public resolvers | the resolvers in tls.acme.resolvers (1.1.1.1, 9.9.9.9, 8.8.8.8) see it. If the name servers do but a resolver doesn't, it is only a warning: it may be holding an earlier answer |
| Clean up | the test record is removed |
Each step says what went wrong and what to do. For example:
ok Jarl the token is housecarl's, on Jarl v0.6.1
ok Zone adbraun.lol in views internal, external
FAIL Permission the token may not write the challenge record (...); give it the grant:
records:write zones=adbraun.lol names=_acme-challenge.fw.adbraun.lol types=TXT
The checks wait up to 90 seconds for the record to appear. A failing step
lists the DNS questions it asked, as dig commands, so you can ask them
yourself.
If the challenge name is a CNAME into another zone, the check follows it, as Let's Encrypt does.
Renewal
A certificate is renewed when a third of its lifetime is left: 30 days before the end for Let's Encrypt's 90-day certificates, 2 days for 6-day ones. housecarld looks twice a day. The check runs first every time, and the new certificate is used at once, without a restart.
If renewing fails, the old certificate keeps being served, and housecarld
tries again after 1 hour, then 3, 6 and every 12. The dashboard shows a
problem (certificate_failed), and certificate_expiring once less than
14 days are left. hc cert renew (Renew now in the web UI) tries
again at once.
The ACME account and the certificate, with its key, are kept in Housecarl's database, so backups include them.
Settings
All under tls.acme in housecarl.yaml (configuration.md):
| Setting | Default | |
|---|---|---|
domain |
none (off) | the name on the certificate |
email |
none | the account's contact address |
directory |
letsencrypt-staging |
letsencrypt, letsencrypt-staging, or an ACME directory URL (a private CA such as step-ca) |
agree_tos |
false |
accept the CA's terms of service; no account is made without it |
resolvers |
1.1.1.1:53, 9.9.9.9:53, 8.8.8.8:53 |
public resolvers, to see the challenge as the CA does |
check_propagation |
true |
also ask each of the zone's name servers directly |
ca_bundle |
none | extra CAs to trust for the ACME server itself |
tls.cert_file and tls.acme can't both be set.