Files
jarl/docs/dnssec.md
adamandClaude Opus 5.5 113a6913a2 Give the DS command for a parent zone in Jarl, and allow port 443
The DNSSEC tab and 'jarl zone dnssec status' give the jarl record add
command (and an Add to parent button) when the parent zone is in Jarl.
jarl.service grants CAP_NET_BIND_SERVICE so the web UI can listen on 443.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:42:16 +00:00

17 KiB

DNSSEC signing

DNSSEC lets resolvers that validate (Cloudflare 1.1.1.1, Google 8.8.8.8, Quad9, many ISPs) detect forged answers for your domain. Jarl signs zones with BIND's own key and signing policy (dnssec-policy), and guides you through the steps that involve the parent zone, where getting the order wrong takes a domain offline.

Should you sign?

Sign a zone that the internet sees through a delegation from its parent: your registered domain (adbraun.lol), as served to the outside world. Validation only happens once the parent zone (lol) publishes a DS record for the zone's key, which you add at your registrar.

Signing an internal-only zone (one no registrar delegates, or a split-DNS view only your own network reaches) adds nothing. There is no parent to publish a DS, and BIND doesn't validate the zones it serves itself.

Split DNS. In Jarl a zone belongs to a view, so you sign the external view's adbraun.lol and leave the internal view's copy unsigned. That is the usual setup, with one catch. Once the DS is at the parent, the world expects adbraun.lol to be signed. A device on your network that validates DNSSEC itself and gets the internal, unsigned copy rejects it. Examples are a Pi-hole or Unbound with DNSSEC on, systemd-resolved with DNSSEC=yes, and some routers. Devices that just ask this server are fine. If you run such a validator, sign the internal copy too and publish both DS records.

What it costs. It is one more way for the domain to break: if the DS at the parent stops matching the zone's keys (keys lost, wiped, or a server moved without them), every validating resolver rejects the whole domain. Most of Jarl's DNSSEC features exist to stop that from happening.

How it works

named signs the zone itself, with inline signing:

  • Jarl writes the zone file as before (/var/lib/bind/jarl/<view>/<zone>.zone). named keeps a signed copy next to it (.signed, .signed.jnl), signs every change Jarl makes, and re-signs before signatures expire.
  • The keys live in Jarl's key directory, /var/lib/bind/jarl/dnssec-keys/<view>/. The subdirectory is named after the view's ID, so renaming a view doesn't move the keys. Each key is three files, K<zone>.+013+<tag>.key, .private (readable only by named's user) and .state. Zones of the same name in two views have separate directories, because named requires that.
  • With BIND's default policy there is one key, a CSK (combined signing key) using ECDSA P-256 (algorithm 13). It is never replaced, so the DS record at the registrar never has to change.
  • The signed zone has its own serial, usually a little ahead of Jarl's. Jarl's reload check compares the serial of the file it wrote. The serials check (jarl zone serials) compares the signed serial, which is what secondaries serve.
  • Secondaries transfer the signed zone and need nothing else.
  • The server's clock must be right (NTP): signatures are valid for a fixed time window.

Turning signing on

jarl zone dnssec enable adbraun.lol --view external
jarl zone dnssec status adbraun.lol --view external

or, in the web UI, the zone's DNSSEC tab → Turn signing on. This is safe at any time. Resolvers keep treating the zone as unsigned until its DS record is at the parent.

status shows the key, the DS record, and when it is safe to publish:

Status:   named is signing the zone. Resolvers still treat it as unsigned, ...
Next:     Wait until Fri 9 Oct 21:13 UTC before adding the DS at the parent: ...

KEY    ROLE  ALGORITHM        GOAL         DNSKEY    DS AT PARENT  CREATED
51126  CSK   ECDSAP256SHA256  omnipresent  rumoured  hidden        2026-10-08 21:08

DS record for the parent zone (at your registrar):
  (not yet: wait until Fri, 09 Oct 2026 21:13:51 UTC)
  adbraun.lol. 3600 IN DS 51126 13 2 FFBECE3D...
  key tag 51126, algorithm 13 (ECDSAP256SHA256), digest type 2 (SHA-256), digest FFBECE3D...

named first publishes the key for long enough that every resolver's cache has caught up: about a day with the default policy. Adding the DS earlier can make validating resolvers reject cached answers. Then:

  1. Make sure the domain is delegated to servers that serve the signed zone. Your own servers, or secondaries that transfer from them. Publishing a DS while the registrar still points at other name servers (Porkbun's own, say) breaks the domain. jarl zone delegation ZONE shows where the parent points.

  2. Publish the DS record at the registrar, by hand (the DNSSEC tab has copy buttons for the whole record and for its four fields), or with Jarl's Porkbun support, which checks step 1 first.

  3. Tell named. Once the parent zone has the DS (minutes to hours after the registrar accepts it), run the DS check:

    jarl zone dnssec check-ds adbraun.lol --view external
    

    It asks every server of the parent zone for the zone's DS records, and tells named (rndc dnssec -checkds) when all of them have it. named waits for the parent's DS TTL before it counts the DS as in place.

  4. Check. jarl zone delegation ZONE now also compares the parent's DS with the zone's keys. A DS that matches no key is an error. DNSViz gives an independent picture.

If the parent zone is itself a primary zone in Jarl (home.adbraun.lol inside adbraun.lol), the DS goes in as a record in that zone, and Jarl gives you both ways to add it: an Add to adbraun.lol button on the DNSSEC tab, and the command, which jarl zone dnssec status prints too:

jarl record add adbraun.lol home DS 51126 13 2 FFBECE3D...

Once the parent zone has it, the tab and status say so. When turning signing off, the steps give the matching jarl record delete adbraun.lol home DS. The zone must be delegated first (NS records for home in adbraun.lol), or validation refuses the DS.

Putting your own name servers on the internet

To serve your domain from your own server, the parent zone must delegate it to your server's name (NS records can't hold addresses). If that name is inside the domain itself (ns1.adbraun.lol for adbraun.lol), resolvers couldn't look it up without already reaching your server. Glue records solve this: the registry publishes ns1.adbraun.lol's address together with the delegation. Each name server inside the domain needs its own glue record, with all of its addresses (IPv4 and IPv6).

The order matters:

  1. In the zone (external view): NS records for your name servers, and A/AAAA records for the ones inside the domain.
  2. At the registrar: a glue record for each of those (Porkbun calls them "glue records", others "host records" or "child name servers").
  3. Check that every name server answers for the zone from the internet.
  4. At the registrar: set the domain's name servers.
  5. jarl zone delegation ZONE until it is all green. Resolvers may use the old name servers for a day or two after the change, so keep those serving the zone until then.

Jarl's Porkbun support does steps 2 to 4 with checks.

Before you go public:

  • Two name servers. Registries and RFC 1034 expect at least two, on different networks: with one server at home, a power or internet outage takes the domain off the internet. A free secondary service (Hurricane Electric's dns.he.net, for example) or a small VPS transfers the zone from Jarl, signed zone included. A hidden primary goes one step further: your server feeds the secondaries but isn't in the NS records, so your home address isn't advertised.
  • A dynamic home address means the glue must follow it.
  • Inbound port 53 (UDP and TCP) must reach the server: some ISPs block it, and CGNAT makes it impossible.

Porkbun

With a Porkbun API key, Jarl manages a domain's name servers, glue records and DS records at Porkbun, and checks each change before making it.

Setting it up

  1. At porkbun.com, Account → API Access: create a key. If you can, limit it to the domain and the permissions it needs (domains, read), and to your server's address.

  2. On the domain's page, switch API Access on (Porkbun refuses API calls for a domain until you do).

  3. Put the key in /etc/jarl/jarl.yaml and restart jarld:

    porkbun:
      api_key: pk1_...
      secret_api_key: sk1_...
      domains: [adbraun.lol]   # optional: only these
    

The key can move your domain anywhere, so it lives only in that file: never in Jarl's database or its backups, and there is no API or CLI command to upload it. Keep the file readable by root and jarld only (chown root:jarl, chmod 640, as the package installs it); jarld warns at start when everyone can read it. Only administrators can use the registrar features.

Using it: the zone's Registrar tab, or jarl zone registrar:

status ZONE Name servers, glue and DS records at Porkbun next to the zone's, with what doesn't match.
sync-glue ZONE Gives every name server inside the domain a glue record with the zone's A and AAAA records for it. Changing the glue of a name server the domain already uses sends resolvers to the new addresses, so Jarl checks first that they answer for the zone.
set-ns ZONE [NS...] Sets the domain's name servers (default: the zone's NS records). Checked first: each answers for the zone with the current serial, the ones inside the domain have matching glue, and DS records at Porkbun match the zone's keys (otherwise the domain goes dark for validating resolvers).
publish-ds ZONE Publishes the zone's DS record. Refused until named says the zone is ready, and unless the domain is delegated to the zone's name servers and they serve the signed zone.
remove-ds ZONE KEYTAG Removes a DS record (the step when turning signing off; also the fix for a DS that matches no key).

Every change can be tried first with --dry-run, which shows the checks. Errors refuse the change; --force (in the web UI, a checkbox) overrides them, for example when this server can't reach its own public address to check it.

Turning signing off

Never switch a signed zone straight to unsigned while its DS might still be at the parent: validating resolvers would reject the whole domain until the DS is gone and its TTL has passed (often a day). Jarl refuses that step and goes through BIND's insecure policy instead:

jarl zone dnssec disable adbraun.lol --view external
  1. The zone switches to the insecure policy. named keeps it signed and publishes a CDS "delete" record. Nothing breaks.
  2. Remove the DS record at your registrar (jarl zone registrar remove-ds with Porkbun). Once the parent zone no longer has it, run jarl zone dnssec check-ds so named knows.
  3. named waits until resolvers have forgotten the DS (the parent's DS TTL plus a margin, about a day), then removes the keys and signatures. status shows the progress.
  4. Run jarl zone dnssec disable again (or Finish turning off in the web UI) when status says so: DNSSEC is then off for the zone.

If the DS never reached the parent, named skips the wait and you can finish at once. The old keys stay in the key directory (named keeps them retired; signing again makes a new key) until you wipe them.

Wipe: a clean slate

jarl zone dnssec wipe adbraun.lol --view external

or Wipe (clean slate) on the DNSSEC tab. It switches signing off at once, without the insecure policy, and deletes the zone's keys and named's signed copy, so signing again starts from scratch. It also cleans up a zone that is off but still has old keys.

That is only safe when the parent zone has no DS record for the zone, so Jarl asks the parent's servers first and refuses while they have one. It also refuses while named still counts a recently removed DS as cached. When the parent can't be asked (no internet access, or a zone that isn't on the internet), --force wipes anyway. Wiping needs an administrator, and the web UI asks you to type the zone's name.

Deleting a zone that is signed is refused: turn signing off first.

Keys, backups and moving servers

The zone's keys exist only in the key directory: Jarl's database doesn't hold them. Losing them while the parent has a DS for them breaks the domain.

  • Backups. Every backup (jarld backup, and the daily jarl-backup.timer) stores the key directory's files inside the backup file, private keys included. Keep /var/backups/jarl private (it is 0700). named's private keys are readable only by named's user, so jarl-backup.service runs with the one capability it needs to read them (CAP_DAC_READ_SEARCH); a manual backup must run as root. --without-keys backs up only the database.
  • Restoring. jarld restore puts the keys back, owned by named's user (bind.named_user), which needs root. Key files that exist but aren't in the backup are left alone.
  • Serials after a restore. A restored database may have older zone serials than named has. named won't load a signed zone's file unless its serial went up, and secondaries ignore a serial that went down. Jarl checks this whenever it writes a zone file, and raises the serial above named's when needed (it shows in the result, and in the audit log).
  • Moving to a new server. Restore the backup before named loads Jarl's configuration there. Otherwise named finds no keys and makes new ones, which the parent's DS doesn't match.

What Jarl refuses, and why

Change Why
Switching a signed zone straight to unsigned The parent may still have the DS: use disable (insecure first), or wipe when the parent has none.
Finishing disable before named is done Resolvers may still have the DS cached.
Deleting a signed zone Turn signing off first.
Moving a signed zone to another view It would get new keys (another key directory) that the DS doesn't match.
A TTL above the policy's max-zone-ttl (a day by default) named refuses to load such a signed zone and keeps serving the old one.
dnssec-policy as a zone, view or server option Jarl sets it per zone, so it can guard the steps. Top-level dnssec-policy NAME { ... } statements (custom policies) are fine.
A custom policy with an empty keys { } list It crashes named 9.20 when the configuration loads (named-checkconf accepts it). Use the built-in insecure policy.
inline-signing no on a signed zone Jarl rewrites zone files; named must keep its signatures in a separate copy.
Signing a secondary zone A secondary gets its signatures from its primary.

Phases

status, the DNSSEC tab and the API report a phase:

Phase Meaning Next
off Not signed.
leftover_keys Not signed, but old keys are in the key directory. Wipe them, or sign again.
starting Signing is on; named hasn't made the key yet. Wait a moment.
signing Signed, but caches may not have the key yet. Wait until the time shown, then publish the DS.
ready Safe to add the DS at the parent. Publish it, then run the DS check.
ds_pending named knows the DS is at the parent and waits for its TTL.
secure Signed, and the parent's DS points at the key.
removing_ds Turning off; the DS must go from the parent first. Remove it, then run the DS check.
ds_expiring The DS is gone; caches may still have it. Wait.
removing_keys named removes the keys and signatures. Finish when you like (it's safe now).
unsigned Turning off has finished. Switch DNSSEC off for the zone.

The key states come from named (rndc dnssec -status ZONE shows the same): rumoured means published, but caches may not have it yet; omnipresent means everyone has it; unretentive means removed, but caches may still have it; hidden means gone.

Custom policies

default suits most domains. For anything else (scheduled key rollovers, NSEC3, other algorithms), define a policy as a top-level statement (jarl option set dnssec-policy 'NAME { ... }' --toplevel, or Server options in the web UI) and enable it with jarl zone dnssec enable ZONE --policy NAME. With a policy that rolls the key-signing key, the DS at the parent must change at every rollover: status and the DS check tell you when, and named waits for the DS check before it retires the old key.

Adopting a signed zone

When jarld import finds a zone that named already signs, it imports its policy and warns. Before jarld adopt, copy the zone's key files (K<zone>.+*) from its old key directory into the one Jarl uses (jarl zone dnssec status ZONE shows it), owned by named's user. Otherwise named makes new keys, and if the parent has a DS for the old ones, the zone stops validating.