# 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] ### Added - Disabled zones: a zone can be disabled instead of deleted. named stops loading it (its zone statement is left out of named.conf), while Jarl keeps it with its records, options and history, still editable; enabling it serves it as it is then. Disabling says what clients get instead (recursion, or REFUSED). Signed zones can't be disabled. Web UI: Disable/Enable on the zone page, a badge in the zone list. CLI: `jarl zone disable|enable`. API: `PATCH /zones/{zone}` with `{"disabled": true}`. Database schema 7. - Blocking (docs/blocking.md): named answers "doesn't exist" for ads, trackers and malware, using BIND's response policy zones. - Blocklists in Adblock, hosts or plain-domain format, downloaded by jarld and checked daily (`blocking.refresh`). A list is downloaded only when the server says it changed, checked with named-checkzone, and reloaded only when its content differs; a failed update keeps the previous version and shows on the dashboard. - Your own block and allow rules, for a domain and the names below it or the exact name. Rules take precedence over every list, and the most specific rule wins. - Lists and rules apply to chosen views, or to every view that offers recursion. They are configuration: staged, reviewed and reverted like any change. - Checking a name says whether it is blocked and by which rule or list entry, with an Allow button in the web UI. - Web UI: a Blocking page, with well-known lists to pick and named's memory per list. CLI: `jarl blocklist` and `jarl block`. API: `/blocking/lists`, `/blocking/rules`, `/blocking/check`. - Pausing: `jarl block pause [DURATION]` or the Blocking page lets every name through for a while (at most a day); blocking resumes by itself. The lists stay loaded, so it takes effect at once. - Statistics: blocked queries over time (a line on the queries chart) and their share, blocked per list, the most blocked names and the clients with the most blocked queries (`jarl stats top blocked`, `top blocked-clients`). Blocked answers no longer count as names that don't exist. - The view choice in the web UI's forms is a compact dropdown, however many views there are. - A `blocking` permission; database schema 6. ### Changed - Options are kept in one fixed order, in named.conf, the web UI and the CLI: grouped by topic (files, listening, access control, recursion and forwarding, DNSSEC, transfers and notify, then the rest), alphabetical within a group. The same option is now in the same place in every view and zone. The first apply after upgrading reorders named.conf (a diff that only moves lines). Top-level statements keep their order. - Diffs of named.conf say which block each change is in, after the `@@` line numbers (`@@ -50,6 +49,8 @@ view "internal" {`), like git does with function names. Moving an option from the server's options into a view no longer looks like two unrelated edits. - Web UI options (server, top-level, view and zone): values with blocks, such as logging, are shown and edited laid out as in named.conf, over several lines with indentation. The editor grows with its content, Tab indents and Enter keeps the indentation (Esc, then Tab, moves on). With a draft open, the tables show the draft's values, so a staged edit stays visible after Save. - The pending-changes banner has an Apply now button: it applies your draft without going to the Changes page, and stays on the page you're on. If the changes are refused, a dialog says why and links to Changes. - Blocking page: blocklists and rules have an Edit button (name, URL, views, comment; a rule's action and scope). The list button that downloads now is called Refresh, like `jarl blocklist refresh`. - Records table: disabled records are hatched and have a "disabled" badge next to their name, instead of only a muted colour and a badge in the comment column. ### Fixed - Statistics: after a reconfig that added, removed or recreated views (creating the first view, for instance), a minute could count everything since named started: millions of queries in an hour. A per-view counter (cache, upstream times, validation) going down was taken for a restart of named. Now only a new boot time means a restart, and a counter that goes down counts 0 for that minute. - Blocking page, checking a name: when your own block rule is for that very name, it offers to remove the rule instead of an allow rule, which was refused as a second rule for the same name. It also names the rule that matched rather than the policy zone entry. - docs/validation.md lacked the `disabled_conflict` warning added in 0.3.0. ## [0.3.0] - 2026-10-09 ### Added - Disabled records: a record can be disabled instead of deleted. It stays in Jarl with its ID, TTL and comment but is left out of the zone (the zone file lists it as a comment), and enabling it puts it back exactly as it was. Web UI: a Disable/Enable button on each record, disabled records greyed out with a badge, a filter for them, and a count in the zone list. CLI: `jarl record disable|enable` (by ID or by name, type and data, `--ptr` for the PTR record, `--missing-ok`), `record list --disabled`, and `disable:`/`enable:` entries for `jarl apply -f`. API: `POST /records/{id}/disable|enable`, `POST /zones/{zone}/records/disable|enable`, a `disabled` field on records, `?disabled=` on record lists and `record.disable|enable` operations. Validation checks the zone as it is served: the last NS record can't be disabled, enabling a record is checked like adding it, and a disabled record that a CNAME now blocks gets a warning. Database schema 5. - DNSSEC for a zone whose parent zone is in Jarl: the DNSSEC tab gives the `jarl record add PARENT NAME DS ...` command to copy and an "Add to PARENT" button (staged like any change), links to the parent zone, and says when the parent has the DS record. `jarl zone dnssec status` prints the command too, and turning signing off gives the matching `jarl record delete`. - CLI output for scripts: `-o id` prints just the IDs of what a command shows or lists (`jarl zone show adbraun.lol -o id`), `-o field=view,soa.serial` just those fields, tab separated. `-q` on `show` and `list` prints the IDs too. With a change (`jarl record add ... -o id`) they apply to the changed object. - Statistics: client lists (busiest and refused clients, in the web UI, `jarl stats top clients|refused` and `jarl stats summary`) show the host name Jarl's records have for each address: the PTR record's target, or else the name of an A/AAAA record. Looked up in Jarl's own data, so no DNS queries; an address without a record shows as before. - Zones list: a padlock after each signed zone's name, linking to its DNSSEC tab (open and amber while signing is being turned off). ### Changed - The web UI and API can listen on ports below 1024, such as 443: `jarl.service` now gives jarld the `CAP_NET_BIND_SERVICE` capability (and only that). With a unit that doesn't, jarld's error says what's missing. See docs/configuration.md, "HTTPS on port 443". - Records table: names are no longer greyed out when they repeat the row above (it made those records look inactive). Records of the same name are separated by a faint line instead, and a full line marks where the next name starts. ### Fixed - `jarl record list`: one long value (a DKIM key) padded every row to its length, so on a terminal each row wrapped onto a second, blank-looking line. Long data is now shortened to fit the terminal; `--wide` shows it in full, as does output to a pipe or file. ## [0.2.0] - 2026-10-09 ### 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. - Two-factor login to the web UI with an authenticator app (TOTP: SHA-1, 6 digits, 30 s, as every app supports). Users turn it on under Account (password, QR code, a code to confirm) and get 10 one-time recovery codes. Logging in then asks for a code (or a recovery code) after the password; each code works once. In a session with two-factor login, creating or rotating API tokens, changing users and resetting someone's two-factor login need a code from the last 5 minutes: the web UI asks for one, API clients send `X-Jarl-TOTP`. API tokens and the local socket are not affected. - `totp.require` in jarl.yaml (`none`, `admins`, `all`) makes two-factor login compulsory; such a user who hasn't set it up can only set it up after logging in. `totp.issuer` names the server in authenticator apps (default `Jarl `). - Resetting a user's two-factor login (lost phone): Users → Reset 2FA, `jarl user totp reset NAME`, or on the server `jarld user totp reset NAME` (also with jarld stopped). - The dashboard and `jarl status` list users who must use two-factor login but haven't set it up, and suggest disabling the first `admin` account when it is still active next to personal administrator accounts. `jarl user list`/`show` and the Users page show who uses it. - API: `POST /auth/login/totp`, `/me/totp` (`setup`, `enable`, `disable`, `recovery-codes`), `DELETE /users/{id}/totp`; database migration 4. - `scripts/testenv.sh`: turns a fresh Debian/Ubuntu machine into a test bed. It installs BIND and the build tools (Go from go.dev), writes a hand-made style configuration with internal and external views, a split-horizon zone (`example.com` or `JARL_TEST_ZONE`) and a reverse zone, starts named and prints the steps to install Jarl and adopt the configuration (docs/development.md). - View colours. Give a view a colour (Views → Edit, `jarl view color`, or `PUT /views/{id}/color`) and the zone list, the zone page and the views list show its name in that colour. The colours are muted, and pastel in dark mode. The default is the link colour. A colour isn't configuration, so setting one needs no changeset or deploy. - A "← Zones" link above a zone, back to the zone list, and "← History" above a changeset. The zone list keeps its filter, view and type in the address, so going back to it (with that link or the browser's back button) restores them. A view opened from a zone or the zone list has a "← example.com (external)" or "← Zones" link back. ### Changed - The version moved from the header to a footer: "Powered by Jarl v0.1.0", linking to the release on Gitea. A build that isn't of a release tag shows its commit instead, linked to the commit and marked `-modified` with uncommitted changes. Before, every build of a commit after a release still showed the release's version. `GET /status` and `/auth/whoami` report the build as `build` (docs/development.md). - `jarld version`, `jarl --version`, `jarl status` and `jarl whoami` give a version number only for release builds. Other builds say they are development builds and how far past the last tag they are: `dev ca0f148b78b6 (3 commits after v0.2.0-rc.1)`. Before, they printed the version and the commit (`0.2.0-rc.1 (ca0f148b78b6)`), which read like a release. - `make deb` gives development builds their own package version after the last tag (`0.2.0~rc.1+git3.gca0f148`, plus the build time with uncommitted changes). Installing one over the RC or an older build is an upgrade: no more `apt install --reinstall`. It also removes older packages from `dist/` (docs/development.md). - The view in the zone list, and on the zone page, links to that view, and the views page highlights it. It used to be plain text, and the zone page linked to the views page as a whole. - The zone's Name servers tab shows at once and follows the check as it happens. Each query is listed as a dig command line (`dig @192.0.2.1 example.org. SOA +norec`) with a "waiting 1.8 s" counter until it is answered. The finished result then replaces the progress, and the queries stay under "Queries sent". Results, the delegation check's included, are kept for 10 minutes, so returning to the tab shows them at once; "Check again" asks anew. A check keeps running if you leave the tab. Behind it, `GET /zones/{zone}/serials?stream=1` streams the queries as newline-delimited JSON (docs/api.md). - The delegation check streams its queries the same way, every one of them: finding the parent zone, its servers' addresses, the NS and DS queries to each parent server and the SOA from each delegated server. Each shows a short answer ("referral to katelyn.ns.cloudflare.com., …", "DS 2371", "NXDOMAIN"). The serials check now lists its address lookups as separate A and AAAA queries. `GET /zones/{zone}/delegation?stream=1` streams them. - Switching a zone's tabs (Records, Settings, …) no longer blanks the page while the tab loads: the page changes once the tab is ready, and "Loading…" appears only if a page takes a while. - `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. - Release candidates (tags like `v0.2.0-rc.1`) publish their `.deb`s to a separate `testing` distribution of the package registry instead of `stable`. Servers that follow `stable` can't pick up an RC with `apt upgrade`. An RC may also take its release notes from the `## [0.2.0]` or `## [Unreleased]` section of the CHANGELOG instead of needing its own (docs/development.md, "Release candidates"). ### Fixed - Publishing a release candidate failed: `scripts/gitea-release.sh` rejected its `.deb`s, whose names carry the Debian version (`0.2.0~rc.1`) instead of `0.2.0-rc.1`. - `jarld init -u` and the install guide recommend naming the first administrator after yourself (one account per person). - `jarld import` no longer warns that a zone with `allow-update { none; }` uses dynamic updates (validation already allowed it). - docs/cli.md gives the CLI configuration file's location on macOS (`~/Library/Application Support/jarl/config.yaml`) and Windows (`%AppData%\jarl\config.yaml`). It was documented only as `~/.config/jarl/config.yaml`. ## [0.1.0] - 2026-10-09 The first version. ### Added - Daily database backups: the `jarl-backup.timer` systemd timer (turned on by the Debian package) runs `jarld backup`, which writes `jarl-.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-