Zones get a DNSSEC setting: named signs them with BIND's default (or a custom) dnssec-policy and inline signing, with keys in a per-view key directory. Jarl shows the keys, the DS record and when it is safe to publish it, checks the parent's DS records (check-ds, which also tells named), and refuses steps that would break a signed domain: switching straight to unsigned, finishing a turn-off too early, deleting or moving a signed zone, TTLs above max-zone-ttl. Turning off goes through the insecure policy; wipe starts over once the parent has no DS. Backups store the DNSSEC keys and restore puts them back for named; the backup unit gets CAP_DAC_READ_SEARCH to read them. Jarl raises a zone's serial above named's whenever it writes an older one (after a restore). The serials check compares the signed serial. With a Porkbun API key in jarl.yaml, the registrar's name servers, glue records and DS records can be shown and changed, each change checked first (dry run, force to override). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
348 lines
21 KiB
Markdown
348 lines
21 KiB
Markdown
# Changelog
|
||
|
||
All notable changes to Jarl are documented here. The format follows
|
||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
|
||
[Semantic Versioning](https://semver.org/).
|
||
|
||
## [Unreleased]
|
||
|
||
The first version, to be released as 0.1.0.
|
||
|
||
### Added
|
||
|
||
- DNSSEC signing per zone (docs/dnssec.md): `jarl zone dnssec
|
||
enable|status|check-ds|disable|wipe` and the zone page's DNSSEC tab, or
|
||
the zone's new `dnssec_policy` field in the API. named signs the zone
|
||
with BIND's `default` policy (one ECDSA P-256 key that is never replaced)
|
||
or a custom one, using inline signing, with keys in a per-view directory
|
||
under `/var/lib/bind/jarl/dnssec-keys`. Jarl shows the keys and the DS
|
||
record to give the registrar, says when it's safe to publish it, and
|
||
checks the parent's DS records (`check-ds`, which also tells named with
|
||
`rndc dnssec -checkds`). It refuses steps that would break a signed
|
||
domain: switching straight to unsigned (it goes through BIND's
|
||
`insecure` policy instead, and finishes only once named is done),
|
||
deleting a signed zone or moving it to another view, and TTLs above the
|
||
policy's `max-zone-ttl`. `wipe` starts over with a clean slate once the
|
||
parent has no DS. Each step in the web UI opens a dialog that says what it
|
||
needs first. `jarl zone delegation` now also compares the parent's DS
|
||
records with the zone's keys.
|
||
- Porkbun support (optional, docs/dnssec.md#porkbun): with an API key in
|
||
the new `porkbun:` section of jarl.yaml (and nowhere else), `jarl zone
|
||
registrar status|sync-glue|set-ns|publish-ds|remove-ds` and the zone
|
||
page's Registrar tab show and change the domain's name servers, glue
|
||
records (one per name server inside the domain, with all its IPv4 and
|
||
IPv6 addresses) and DS records at Porkbun. Every change first checks that
|
||
it won't take the domain offline (the name servers answer for the zone,
|
||
glue matches, a DS goes out only once the domain is delegated to servers
|
||
that serve the signed zone, DS records match the zone's keys before the
|
||
name servers change); `--dry-run` shows the checks and `--force` skips
|
||
them. Administrators only.
|
||
- Backups include the DNSSEC keys (private keys too), stored inside the
|
||
backup file, and `jarld restore` puts them back for named
|
||
(`bind.named_user`, default `bind`). `jarl-backup.service` has
|
||
`CAP_DAC_READ_SEARCH` to read them; `--without-keys` skips them.
|
||
- Jarl raises a zone's serial above the one named has loaded whenever it
|
||
writes a zone file whose serial isn't higher (after restoring an older
|
||
database, say), so named loads it (it refuses otherwise for a signed
|
||
zone) and secondaries pick it up.
|
||
- Daily database backups: the `jarl-backup.timer` systemd timer (turned on
|
||
by the Debian package) runs `jarld backup`, which writes
|
||
`jarl-<UTC time>.db` into `backup.dir` (`/var/backups/jarl`), checks it
|
||
with SQLite's integrity check before giving it that name, and deletes
|
||
backups older than `backup.retention` (30d) while always keeping the
|
||
newest `backup.keep_at_least` (7). Ages come from the names, and only
|
||
files named like backups are ever deleted. Settings are in the new
|
||
`backup:` section of jarl.yaml; the schedule is the timer's. The dashboard
|
||
and `jarl status` show the newest backup, and warn when it is older than
|
||
`backup.warn_after` (2d) or the last attempt failed. `backup.stats` also
|
||
backs up the statistics database.
|
||
- `jarld restore [FILE | latest]`: lists backups, or checks one, shows what
|
||
it holds next to the current database, and after confirmation replaces
|
||
the database with it, keeping the current one as
|
||
`jarl.db.before-restore-<time>`. It refuses while jarld runs: `jarld
|
||
serve` now holds a lock (`jarl.db.lock`), which also stops two jarlds from
|
||
using the same database. docs/operations.md explains restoring.
|
||
- Warnings before API tokens expire, from 14 days ahead: on the dashboard,
|
||
in token lists ("soon", "expired"), once a day in jarld's log, and from
|
||
the `jarl` CLI on stderr whenever it uses such a token (responses carry a
|
||
`Jarl-Token-Expires` header). `jarl token list --expiring [WITHIN]` lists
|
||
them.
|
||
- `jarl zone serials ZONE` and the zone page's Name servers tab: ask the
|
||
zone's other name servers which serial they serve (a primary zone's NS
|
||
records and also-notify targets, a secondary zone's primaries, with their
|
||
ports and TSIG keys) and compare it with this server's. Exits with 2 when
|
||
one is behind or doesn't answer.
|
||
- `jarl zone delegation ZONE` and a button on the Name servers tab: for
|
||
zones on the internet, checks that the parent zone delegates the zone to
|
||
the name servers in its NS records, with matching glue, and that each of
|
||
them serves it. An undelegated (internal-only) zone gets one calm note.
|
||
Neither check runs unless asked.
|
||
- jarld sends `Server: Jarl/<version>`.
|
||
- CLI exit code 2: a check ran and found problems.
|
||
- CLI: several servers in one configuration file. Name them
|
||
(`jarl config server add ns1 URL`), pick one with `-s/--server NAME` or
|
||
`JARL_SERVER`, set a default with `jarl config use`, and group them
|
||
(`jarl config group set dns ns1 ns2`). A command aimed at a group (or
|
||
`all`) is first checked on every server as a dry run and only runs if it
|
||
succeeds everywhere, so a change one server rejects changes nothing
|
||
anywhere. Named servers always use their own token.
|
||
- Reverse zones for any network: a /32 (or /128) gets a zone that answers
|
||
for just that address, and a network between octet (or nibble)
|
||
boundaries, such as a /22, is covered by several zones (four /24s) that
|
||
are created together, from the CLI (`jarl zone create 10.0.8.0/22`) and
|
||
the web UI. Networks smaller than a /24 explain that they belong in the
|
||
enclosing /24's zone.
|
||
- docs/operations.md: setting up a secondary server with its own jarld.
|
||
- A local unix socket, `/run/jarl/jarl.sock` (`socket` in jarl.yaml).
|
||
Requests on it from root or jarld's own user need no token and act as
|
||
an administrator (the kernel's `SO_PEERCRED` says who is connecting);
|
||
anyone else must send a token. The `jarl` CLI uses it when no server is
|
||
configured, or with `-s local`, so `sudo jarl status` works on the
|
||
server with nothing set up, and losing every token or password never
|
||
locks root out. Changes are audited under the system user's name via
|
||
`socket`. The systemd unit creates `/run/jarl` (0750).
|
||
- Jarl is released under the MIT License (`LICENSE`), which is installed
|
||
with the docs and is the Debian package's `copyright` file.
|
||
- A Debian package: `make deb` builds `dist/jarl_<version>_<arch>.deb` with
|
||
only `dpkg-deb` (no root), for the host or any other architecture
|
||
(`DEB_ARCH=arm64`). Installing it creates the `jarl` user and directories
|
||
and an example `/etc/jarl/jarl.yaml`; upgrades restart a running jarld and
|
||
never touch `/etc/jarl/jarl.yaml`; removing it stops jarld, and even a
|
||
purge keeps Jarl's database and the files named uses.
|
||
- Bash completion for `jarl` and `jarld`, installed by `make install` and
|
||
the Debian package. Besides commands and flags, it completes server and
|
||
group names from the CLI configuration, zone, view, ACL, TSIG key and
|
||
user names from the server, and record types.
|
||
- `make dist` tarballs include the completion scripts (`completion/`).
|
||
- Token rotation: `jarl token rotate`, the Rotate button in the web UI and
|
||
`POST /api/v1/tokens/{id}/rotate` give a token a new secret while it
|
||
keeps its ID, name, user and grants. The old secret stops at once or after
|
||
a grace period of up to 7 days (`--grace`), during which jarld logs who
|
||
still uses it. A token that expires gets its lifetime again. `--save`
|
||
writes the new secret where the CLI got the old one (its configuration or
|
||
`--token @FILE`), `--write FILE` to any file, and `--current` picks the
|
||
token the CLI runs with, so a restricted token can renew itself.
|
||
`jarl token list` shows when a token was last rotated.
|
||
- An endpoint for lego's `httpreq` DNS provider
|
||
(`/api/v1/acme/httpreq/present` and `/cleanup`), so lego and Traefik can
|
||
publish DNS-01 challenges with only environment variables, without the
|
||
jarl CLI. lego's username and API token arrive as HTTP Basic auth, which
|
||
is accepted on these routes only. Off by default
|
||
(`acme.httpreq.enabled`), and HTTPS-only by default: jarld's own TLS, or a
|
||
trusted proxy that says so with `X-Forwarded-Proto` or `Forwarded`.
|
||
Plain HTTP can be allowed from loopback only (`allow_loopback_http`) or
|
||
everywhere (`require_https: false`). `GET /api/v1/acme/httpreq` shows how
|
||
jarld sees a request, to check a proxy setup. `jarl acme` and the endpoint
|
||
share their code (`internal/acme`).
|
||
- Trusted proxies may also send the standard `Forwarded: proto=https`
|
||
header. When a proxy header lists several values, the last one (added by
|
||
the nearest proxy) counts.
|
||
- lego support: `contrib/lego/jarl-lego.sh` is a program for lego's `exec`
|
||
DNS provider that publishes DNS-01 challenges through `jarl acme`,
|
||
including challenge names reached through a CNAME (docs/acme.md).
|
||
- docs/concepts.md: what each zone type does, what to enter as a new zone's
|
||
name servers (and when glue addresses are needed), how reverse zones map
|
||
to networks, and why dynamic updates give way to API tokens.
|
||
|
||
- A logo: a bearded Viking jarl in blue and white, whose crown holds a DNS
|
||
tree (a root node and its branches). The master is `artwork/logo.png`;
|
||
`scripts/logo.py` traces it into vector paths (`assets/logo.svg`, 6 KB,
|
||
and `assets/logo.js`). It is shown on the login page, links home in the
|
||
top left of the web UI, and is the favicon. The UI draws it inline, so it
|
||
appears together with the page.
|
||
- The login page shows an angular, Norse-style "JARL" wordmark drawn as
|
||
SVG paths (`assets/brand.js`) above a rule with a "DNS" node; it follows
|
||
the light and dark themes. The header beside the logo shows the same
|
||
letters, without the rule, so the UI needs no web font.
|
||
- The README opens with the login page's banner, the logo above the
|
||
wordmark (`artwork/banner.svg`, built by `scripts/banner.py` from the web
|
||
UI's own logo and wordmark, with light and dark colors).
|
||
|
||
- Web UI: wherever an address match list is entered (server, view and zone
|
||
options such as allow-query or allow-transfer, view match lists, ACL
|
||
elements) the existing ACLs, TSIG keys and built-in names are offered as
|
||
one-click suggestions, and names that don't exist are flagged as you type.
|
||
|
||
- **Query statistics** (off by default; `stats` in jarl.yaml, docs/stats.md).
|
||
When enabled, Jarl adds a loopback statistics channel and a response log to
|
||
named's configuration (merged into existing `logging` and
|
||
`statistics-channels` statements) and switches response logging with
|
||
`rndc responselog`. jarld aggregates named's counters and the log into a
|
||
separate SQLite database: per-minute, hourly and daily counters (queries,
|
||
result codes, record types, views, zones, cache hits, upstream latency,
|
||
DNSSEC) and hourly and daily top lists (names, clients, NXDOMAIN and
|
||
SERVFAIL names, refused clients), each with its own retention. Clients can
|
||
be anonymized or excluded. It is shown on a new Statistics page in the web
|
||
UI (KPI tiles, queries and failed answers over time, a weekday x hour
|
||
heatmap, top lists and breakdowns, each chart with a table view), in
|
||
`jarl stats status|summary|top|timeline|heatmap|breakdown`, and in the API
|
||
under `/api/v1/stats/`, behind the new `stats` permission. Works with the
|
||
bind9 packages of Debian 13 and Ubuntu 26.04.
|
||
- At startup jarld brings named's configuration up to date with jarl.yaml
|
||
settings that affect it (such as statistics), through the usual checked
|
||
pipeline. Files changed by hand are never overwritten.
|
||
- **jarld daemon**: REST API (`/api/v1`) and embedded web UI, with SQLite as
|
||
the source of truth for the whole BIND configuration. Runs under systemd
|
||
(`Type=notify`, sandboxed, unprivileged `jarl` user in group `bind`).
|
||
SIGHUP and certificate file changes reload TLS.
|
||
- **Configuration model**: views (ordered, match-clients/destinations,
|
||
recursive-only), ACLs (nested, negation, keys, reference tracking with
|
||
rename and delete protection), TSIG keys (kept in a separate `keys.conf`,
|
||
never in diffs), zones of every type (primary, secondary, stub,
|
||
static-stub, forward, mirror, hint, redirect, in-view), reverse zones
|
||
from a CIDR, records of every type, options at server, view, zone and
|
||
top-level scope, and defaults for new zones.
|
||
- **BIND 9.20 option catalog**: all 353 statements with grammar, allowed
|
||
scopes, value kinds, deprecation status and ARM descriptions, used for
|
||
validation and by the UI's option editor.
|
||
- **Changesets**: immediate one-operation changes by default; staging into
|
||
named changesets (`?changeset=`, `X-Jarl-Changeset`, `--changeset`),
|
||
batches (`POST /apply`, `jarl apply -f`), dry runs, previews with diffs,
|
||
and revert of applied changesets. Optimistic locking with object versions.
|
||
- **Apply pipeline**: whole-configuration validation, rendering, `named-checkconf -z`
|
||
and `named-checkzone` in a staging directory, atomic install, `rndc reconfig`
|
||
and per-zone reload, verification with `rndc zonestatus`, and automatic
|
||
rollback when named doesn't load the change. Automatic serials (date,
|
||
increment or unixtime policy, RFC 1982), bumped only when DNS data
|
||
changes. Drift detection for files changed outside Jarl.
|
||
- **Validation**: CNAME/DNAME rules, NS/MX/SRV targets, glue and
|
||
delegations, RRset TTL consistency, BIND 9.20 RRset/type limits, SPF/DMARC
|
||
sanity, ACL/key references and cycles, option syntax and scope, and
|
||
security lints (open resolver, open zone transfers, dynamic updates on
|
||
managed zones).
|
||
- **Records**: TXT entered as plain text (automatic quoting and 255-byte
|
||
splitting), relative and absolute names with detection of doubled
|
||
origins, PTR records kept in step with A/AAAA (`ptr`), RRset replace,
|
||
delete by match, zone file import (merge or replace).
|
||
- **Import and adoption**: `jarld import` reads an existing named.conf tree
|
||
and zone files (comments, `$INCLUDE`, `$GENERATE`, legacy
|
||
`master`/`slave` spellings); `jarld adopt` switches named to Jarl's
|
||
configuration with a backup and automatic fallback.
|
||
- **Authentication**: web logins with argon2id passwords, sessions
|
||
(HttpOnly, SameSite=Strict) and CSRF tokens, rate limiting per user and
|
||
IP; hashed API tokens (`jarl_...`) with optional expiry.
|
||
- **Permissions**: granular grants (resource × read/write, filtered by zone,
|
||
view, record name pattern and type), roles (`readonly`, `dns-editor`,
|
||
`operator`, `acme`), tokens restricted to a subset of their user's
|
||
rights, administrators.
|
||
- **Audit log** of logins, changes, users, tokens and secret reveals, with
|
||
user, token, IP and outcome, searchable and written to the journal.
|
||
- **Web UI**: dashboard, zones and records with inline editing and pending
|
||
markers, views, ACLs, TSIG keys, the option editor, review and apply,
|
||
history with revert, audit log, users, account and tokens, search.
|
||
Review mode by default, with an "apply immediately" switch.
|
||
- **jarl CLI**: zones, records, views, ACLs, TSIG keys, options, settings,
|
||
changesets, batches, history, users, tokens, audit, search, status, check
|
||
and resync. Table, JSON and YAML output, and documented exit codes.
|
||
- **ACME DNS-01**: `jarl acme set|unset`, certbot hooks
|
||
(`jarl acme certbot-auth|certbot-cleanup`), an acme.sh DNS API hook
|
||
(`contrib/acme.sh/dns_jarl.sh`), automatic zone and view discovery,
|
||
idempotent retries, and `--follow-cname` / `--resolver` for a dedicated
|
||
ACME zone.
|
||
- **Operations**: `jarld backup` (online), `jarld resync`, `jarld check`,
|
||
`jarld render`, local user recovery commands, audit retention.
|
||
- **Packaging**: Makefile (build, test, e2e, install, dist), systemd unit,
|
||
sysusers.d and tmpfiles.d files, example configuration
|
||
(`jarld config --example`).
|
||
- **Tests**: unit tests for every package, integration tests against the
|
||
real named 9.20 (checkers, reload and rollback, import and adopt), and an
|
||
end-to-end script (`scripts/e2e.sh`) that drives every CLI command
|
||
against a running named.
|
||
- **Documentation**: concepts, install and adoption, configuration, CLI,
|
||
API, permissions, ACME, validation, operations and development guides, and
|
||
a split-DNS guide for adding views to a server that has none.
|
||
|
||
### Changed
|
||
|
||
- `jarl zone serials` compares the serial named serves, which for a signed
|
||
zone is the signed zone's (ahead of Jarl's).
|
||
- `jarld import` turns `dnssec-policy` in an existing configuration into
|
||
the zones' DNSSEC setting, and warns that their keys must be moved into
|
||
Jarl's key directory before adopting.
|
||
- `dnssec-policy` is no longer accepted as a zone, view or server option
|
||
(zones have a DNSSEC setting instead), nor `key-directory` as a zone
|
||
option.
|
||
- Deleting a zone also removes the journal and signed files named kept
|
||
next to it.
|
||
|
||
- Web UI: the review/live switch is a small "Review edits" / "Live edits"
|
||
button in the header, and the bar below it only appears while there are
|
||
pending changes.
|
||
- TSIG keys: the name help no longer calls `xfer-key` a domain name (any
|
||
letters, digits, hyphens and dots will do; both servers must use the same
|
||
name), and the keys page points to restricted API tokens for giving
|
||
scripts or people limited access to zones and records.
|
||
- Web UI, new zone dialog: choose **Domain** or **Reverse zone**. For a
|
||
reverse zone you give the network (10.10.0.0/24, 2001:db8:1::/48) and the
|
||
zone name is generated. Either way the dialog shows the exact zone name it
|
||
will create and explains problems before submitting: an address or URL
|
||
instead of a name, a host-like name such as www.example.com, a zone that
|
||
already exists in the view, a child or parent of an existing zone, and
|
||
networks that don't follow the octet or nibble boundaries.
|
||
- Web UI, record form: after adding a record every field is cleared except
|
||
the type, and the cursor returns to the name. A "Will add:" line shows the
|
||
record exactly as it will be stored, with warnings for unqualified names
|
||
(e.g. a CNAME to readthedocs.io without the final dot). In reverse zones
|
||
the form asks for the address, defaults to PTR, offers the host names of
|
||
A/AAAA records with that address, warns about a second PTR for an
|
||
address, and the record list shows each record's address.
|
||
- Reverse zones: a record name can be given as an address (10.10.0.20) in
|
||
the API and CLI too, and a PTR target without a final dot that contains a
|
||
dot (nas.adbraun.lol) is read as the full host name (with a warning); a
|
||
bare label is refused.
|
||
|
||
- `jarl resync` / `jarld resync` refuse to overwrite files that were changed
|
||
outside Jarl unless `--force` is given, as documented. Before, they always
|
||
overwrote them.
|
||
|
||
### Fixed
|
||
|
||
- The logo popped in after the page: it was an 880 KB SVG wrapping a
|
||
1254-pixel PNG, downloaded in full on every page load. It is now traced
|
||
into 6 KB of vector paths and drawn inline. Static files also get an ETag,
|
||
so reloads revalidate them with 304 answers instead of downloading them
|
||
again.
|
||
- `jarl token create --save` stored the token at the top level of the
|
||
CLI configuration even when a named server was selected; it now goes into
|
||
that server's entry.
|
||
- The web UI showed future times, such as token expiry dates, as "just
|
||
now"; they now read "in 29 days".
|
||
- The dashboard's "Pending changes (1 open)" counted empty changesets (and
|
||
other users'), so the Changes page then showed none. It now counts your
|
||
own changesets that contain changes, and the status API's
|
||
`open_changesets` only counts non-empty ones. In review mode, a draft
|
||
created for an edit that the server rejects is discarded instead of
|
||
being left open and empty.
|
||
- Under the shipped systemd unit (`RestrictSUIDSGID=yes`), the first zone
|
||
in a new view failed with "chmod /var/lib/bind/jarl/VIEW: operation not
|
||
permitted": setting the setgid bit on the new directory is refused there.
|
||
Jarl now falls back to the permission bits, so the directory is
|
||
group-writable for named, and only changes the mode when it needs to.
|
||
- Web UI: the "Jarl 0.1.0" brand link no longer underlines the gap between
|
||
the name and the version on hover.
|
||
- Web UI: creating a zone from the dashboard's "New zone" link no longer
|
||
opens the dialog a second time.
|
||
- Warnings noticed while parsing record data (a name read as absolute) were
|
||
lost before reaching the result; they are shown again.
|
||
|
||
- The negative-caching warning now uses the effective negative TTL, which
|
||
is the smaller of the SOA minimum and the zone's default TTL (RFC 2308).
|
||
It explains the real effect (new names can stay invisible to resolvers
|
||
that already asked) and gives the command that fixes it. Before, it
|
||
fired on the SOA minimum alone and wrongly blamed BIND's resolver cap.
|
||
- Problems printed by `jarld check` show zone names without the trailing dot.
|
||
- `jarl` now shows the warnings a change introduces when it is applied or
|
||
staged, not only on dry runs.
|
||
- `jarl check` and `jarl resync` no longer print named-checkconf's routine
|
||
"loaded serial" lines, only its errors and warnings.
|
||
- `jarl zone list` showed only primary zones: `zone create`'s default
|
||
`--type primary` leaked into the list filter through a shared variable.
|
||
Hint, secondary, forward and other zones are listed again.
|
||
- named-checkconf and named-checkzone output is classified by the tools'
|
||
exit code. Their warnings (e.g. `both "recursion no;" and "allow-recursion"
|
||
active`) were shown as errors. They are now shown as warnings in the
|
||
CLI, `jarld check` and the web UI.
|
||
- The `zone_outside_view` error says how to fix it (`--adopt-zones`).
|
||
- The end-to-end test starts named with `-f` like the Debian unit; with `-g`,
|
||
named sends all logging to stderr.
|