Files
adamandClaude Opus 5.5 aca0abc780 Add the jarld.service alias for jarl.service
systemctl restart/status jarld now work too. The package relinks a unit
enabled by an older version on upgrade, so the alias appears without
re-enabling; make uninstall removes the link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:54:57 +01:00

16 KiB

Installing Jarl and adopting an existing BIND server

This guide targets Debian/Ubuntu with BIND 9.20 (bind9 package, /etc/bind, user and group bind, AppArmor). Other layouts work too: every path is configurable in /etc/jarl/jarl.yaml (see configuration.md).

The migration is designed to be safe. Your current configuration is imported into Jarl and checked with named-checkconf before named is touched. Adopting keeps a copy of the old named.conf, and if named fails to load Jarl's configuration the old one is put back automatically.

1. Build

Each release is already built: the Debian packages are in the Gitea package registry (below), and the release page (https://gitea.adbraun.lol/adam/jarl/releases) has the packages, the linux tarballs, and the CLI for Linux, macOS and Windows. Build it yourself for an unreleased version.

Jarl is two static binaries with no runtime dependencies (SQLite is compiled in). Building needs Go 1.27 or newer, on any machine:

make            # bin/jarld and bin/jarl
make test       # optional: unit and integration tests
make dist       # optional: dist/jarl-<version>-linux-{amd64,arm64}.tar.gz

If you build on another machine, copy the binaries plus the deploy/, docs/ and contrib/ directories to the DNS server, or build a Debian package (below) and copy that.

2. Install the files

There are two ways: a Debian package, which is easiest to upgrade and remove, or make install.

With the Debian packages

There are two packages:

Package Holds Install it
jarl the server: jarld, its systemd units, the docs on the DNS server
jarl-cli the jarl CLI, its bash completion, the ACME hooks wherever you manage Jarl from; jarl depends on it

From the package registry

Every release is published to Gitea's Debian registry (amd64 and arm64). Add it to apt once, and apt then finds jarl-cli by itself and apt upgrade brings new releases:

sudo install -d /etc/apt/keyrings
sudo curl -fsSL https://gitea.adbraun.lol/api/packages/adam/debian/repository.key -o /etc/apt/keyrings/jarl.asc
echo "deb [signed-by=/etc/apt/keyrings/jarl.asc] https://gitea.adbraun.lol/api/packages/adam/debian stable main" \
  | sudo tee /etc/apt/sources.list.d/jarl.list
sudo apt update
sudo apt install jarl

Release candidates (0.2.0-rc.1) are published to a separate testing distribution, so the stable line above only ever installs releases. To try release candidates on a test machine, add testing as well. apt then installs the newest of either, which is the RC until the final release replaces it:

echo "deb [signed-by=/etc/apt/keyrings/jarl.asc] https://gitea.adbraun.lol/api/packages/adam/debian testing main" \
  | sudo tee -a /etc/apt/sources.list.d/jarl.list

The packages are as visible as the Gitea account that owns them, not the repository: with a public account, apt needs no login even if the repository is private. For a private or limited account, give apt a token with the read:package scope in /etc/apt/auth.conf.d/jarl.conf (mode 0600):

machine gitea.adbraun.lol/api/packages/adam/debian
login adam
password <token>

From files

Download the two .deb files from the release page, or build them:

make deb                                  # dist/jarl_0.2.0_amd64.deb and dist/jarl-cli_0.2.0_amd64.deb
make deb DEB_ARCH=arm64                   # for another architecture (also armhf, i386, ...)
sudo apt install ./dist/jarl_0.2.0_amd64.deb ./dist/jarl-cli_0.2.0_amd64.deb

Give apt both files: jarl needs jarl-cli of the same version. make deb needs only dpkg-deb (no root, no debhelper) and cross-builds for any architecture Go supports. apt also installs bind9 (recommended) if it's missing. Installing the server package:

  • puts jarld in /usr/bin, its bash completion in /usr/share/bash-completion/completions, the systemd units in /usr/lib/systemd/system (jarl.service, and jarl-backup.service with its daily jarl-backup.timer), the sysusers and tmpfiles files in /usr/lib, and the docs in /usr/share/doc/jarl;
  • creates the jarl user (in group bind) and Jarl's directories, and writes /etc/jarl/jarl.yaml from the example on the first install. No file in /etc belongs to the package, so your configuration is never replaced or asked about on upgrades;
  • turns on the daily database backup, jarl-backup.timer (operations.md). It does nothing until the database exists. If you turn it off (systemctl disable jarl-backup.timer), upgrades leave it off;
  • doesn't start jarld: continue with 3. Configure, then systemctl enable --now jarl in step 8.

jarl-cli installs /usr/bin/jarl, its completion, and /usr/share/doc/jarl-cli (the CLI, ACME and permissions docs, and the acme.sh and lego hooks in contrib/).

Upgrading: sudo apt update && sudo apt upgrade with the registry, or install the newer files the same way. A running jarld is restarted, and upgrades its database as it starts. Up to 0.1.0 everything was one jarl package; installing the two new ones over it moves /usr/bin/jarl to jarl-cli by itself.

Removing (sudo apt remove jarl) stops jarld and the backup timer and removes the server (the CLI stays until you remove jarl-cli too), but named keeps running with the configuration Jarl last wrote. Even apt purge jarl keeps /etc/jarl, /var/lib/jarl (the database), /var/backups/jarl and the files in /etc/bind/jarl, /var/lib/bind/jarl and /var/cache/bind/jarl, since named.conf may still include them. Remove them by hand once named no longer uses them.

Just the CLI, on another machine

To manage Jarl servers from your laptop or another server, install only jarl-cli. It needs nothing else and changes nothing on the machine (no users, services or directories):

sudo apt install jarl-cli      # with the registry above; or: sudo apt install ./jarl-cli_0.2.0_amd64.deb
jarl config set server https://ns1.adbraun.lol:8053
jarl config set token

Elsewhere, download the CLI from the release page: jarl-cli-<version>-<os>-<arch> for Linux, macOS (darwin) and Windows (.exe), with their checksums in SHA256SUMS. It is one static file; put it in your PATH as jarl (on macOS, xattr -d com.apple.quarantine jarl lets an unsigned download run). make cli-dist builds the same files, and make builds it for this machine in bin/. cli.md explains the setup, and several servers in one configuration.

With make install

On the DNS server, as root:

make install           # or: make install PREFIX=/usr

This installs:

File Purpose
/usr/local/bin/jarld, /usr/local/bin/jarl daemon and CLI
/usr/local/share/bash-completion/completions/jarl, .../jarld bash completion (loaded automatically by the bash-completion package)
/etc/systemd/system/jarl.service systemd unit (Type=notify, sandboxed)
/etc/systemd/system/jarl-backup.service, jarl-backup.timer the daily database backup, DNSSEC keys included (operations.md)
/usr/lib/sysusers.d/jarl.conf creates the jarl system user and adds it to group bind
/usr/lib/tmpfiles.d/jarl.conf creates /etc/jarl, /var/lib/jarl, /etc/bind/jarl, /var/lib/bind/jarl, /var/cache/bind/jarl, /var/backups/jarl
/etc/jarl/jarl.yaml example configuration (only if none exists)
/usr/local/share/doc/jarl/ these docs and the acme.sh hook

Then create the user and directories:

systemd-sysusers jarl.conf
systemd-tmpfiles --create jarl.conf
chgrp jarl /etc/jarl/jarl.yaml
systemctl daemon-reload
systemctl enable --now jarl-backup.timer   # daily backups; idle until the database exists

Without make (from a release tarball), the same steps are:

install -m 0755 jarld jarl /usr/local/bin/
install -m 0644 deploy/systemd/jarl.service deploy/systemd/jarl-backup.service deploy/systemd/jarl-backup.timer /etc/systemd/system/
install -m 0644 deploy/sysusers.d/jarl.conf /usr/lib/sysusers.d/
install -m 0644 deploy/tmpfiles.d/jarl.conf /usr/lib/tmpfiles.d/
systemd-sysusers jarl.conf && systemd-tmpfiles --create jarl.conf
jarld config --example > /etc/jarl/jarl.yaml
chgrp jarl /etc/jarl/jarl.yaml && chmod 0640 /etc/jarl/jarl.yaml
systemctl daemon-reload
systemctl enable --now jarl-backup.timer

Why the jarl user is in group bind

jarld runs as jarl, not root. Membership of bind lets it read named's configuration and /etc/bind/rndc.key (for named-checkconf and rndc), and give the files it generates to group bind so named can read them. named in turn can't write Jarl's configuration (/etc/bind/jarl is 2750), but it can write journals in /var/lib/bind/jarl (2770).

3. Configure

Edit /etc/jarl/jarl.yaml. The defaults match Debian's BIND 9.20 layout, so usually only listen and tls need changes:

listen:
  - "127.0.0.1:8053"     # always keep localhost for the local CLI and hooks
  - "192.168.1.2:8053"   # a management address for the web UI
tls:
  cert_file: /etc/jarl/tls/fullchain.pem
  key_file: /etc/jarl/tls/privkey.pem

Keep the web UI and API off the internet: listen on localhost and a management network only, and restrict the port in your firewall. When the server becomes internet-facing for DNS, that changes nothing for Jarl.

A plain-HTTP listener is fine on localhost. Anything reachable over the network should use TLS, either with tls: above or behind a reverse proxy that terminates TLS (set trusted_proxies so client IPs in the audit log are right). See "TLS certificate from Let's Encrypt" below.

Check the result with jarld config (prints the effective configuration).

4. Back up BIND

Jarl keeps a copy of named.conf, but take your own backup anyway:

tar czf /root/bind-before-jarl-$(date +%F).tgz /etc/bind /var/lib/bind /var/cache/bind

5. Create the database and the first administrator

sudo -u jarl jarld init --token -u adam \
    --nameservers ns1.adbraun.lol. --contact hostmaster@adbraun.lol

It asks for the administrator's password. Name the account after yourself (-u, default admin): each person should have their own account, with their own two-factor login, which you can turn on under Account once you have logged in. --token also prints an API token for the CLI, shown only once. --nameservers and --contact are the defaults for zones you create later, and you can change them in the UI or with jarl settings set.

6. Import the current configuration

sudo -u jarl jarld import --dry-run   # see what would be imported
sudo -u jarl jarld import

The importer reads /etc/bind/named.conf and everything it includes: ACLs, keys, options, logging, views, zones, and every primary zone file (with comments, $TTL, $ORIGIN and $INCLUDE; $GENERATE ranges are expanded into individual records). It converts legacy spellings (master/slave/masters) and keeps rndc.key as an include.

If everything validates, the import is applied as changeset #2. Jarl writes its files into /etc/bind/jarl and /var/lib/bind/jarl, but named keeps running on the old configuration. If there are problems, the import is left as an open changeset to fix in the web UI or CLI first, and the command tells you what is wrong.

Look at what Jarl generated before going further:

less /etc/bind/jarl/named.conf
ls -R /var/lib/bind/jarl
sudo -u jarl jarld check    # validation + named-checkconf -z

7. Adopt: switch named over to Jarl

sudo jarld adopt

This needs root because it rewrites /etc/bind/named.conf:

  1. bumps the serial of every primary zone, so secondaries and caches notice the change,
  2. saves named.conf as named.conf.pre-jarl-<date> and replaces it with a single include "/etc/bind/jarl/named.conf";,
  3. runs named-checkconf, rndc reconfig and reloads every zone,
  4. checks that named serves every zone with the new serial.

If any step fails, the previous named.conf is restored and named is reloaded. Afterwards, check that the answers match:

dig @127.0.0.1 adbraun.lol SOA +short
dig @127.0.0.1 -x 192.168.1.1 +short

From now on, change DNS only through Jarl. Hand edits to the generated files are detected as drift and are never silently overwritten (see operations.md).

Going back. cp /etc/bind/named.conf.pre-jarl-<date> /etc/bind/named.conf && rndc reconfig restores the old setup. Your old zone files were never modified.

8. Start the daemon

systemctl enable --now jarl
systemctl status jarl
journalctl -u jarl -f

The unit also answers to jarld (Alias=jarld.service), so systemctl restart jarld and journalctl -u jarl work alike once it is enabled.

Open https://<management-address>:8053/ and log in as admin.

9. Set up the CLI

On the server itself, root needs nothing: sudo jarl status and every other command use jarld's local socket without a token (cli.md).

On other machines, or as an ordinary user, give the CLI a server and a token:

jarl config set server https://ns1.adbraun.lol:8053
jarl config set token          # paste the token from 'jarld init' at the prompt
jarl status
jarl zone list

The CLI's settings are stored in ~/.config/jarl/config.yaml (mode 0600; macOS and Windows use their own folders, see cli.md). JARL_SERVER and JARL_TOKEN override them, which is handy in scripts. Create more tokens in the web UI (Account → API tokens) or with jarl token create NAME. See cli.md.

Optional: query statistics

To see query volume, top names and clients in the UI and CLI, set stats: {enabled: true} in /etc/jarl/jarl.yaml and restart jarl. See stats.md.

TLS certificate from Let's Encrypt

Jarl can publish the DNS-01 challenge for its own certificate. On the DNS server, run certbot with the hooks against the local listener:

# As yourself (with an admin token configured): a service account and a
# token that can only touch _acme-challenge TXT records.
jarl user create certbot --no-password --role acme
jarl token create certbot --user certbot --role acme -q > certbot.token
sudo install -m 0600 certbot.token /etc/jarl/certbot.token && rm certbot.token

# certbot runs as root and reads the token from the file.
sudo certbot certonly --manual --preferred-challenges dns \
  --manual-auth-hook 'jarl --server http://127.0.0.1:8053 --token @/etc/jarl/certbot.token acme certbot-auth' \
  --manual-cleanup-hook 'jarl --server http://127.0.0.1:8053 --token @/etc/jarl/certbot.token acme certbot-cleanup' \
  -d ns1.adbraun.lol

certbot keeps its keys readable only by root, so give jarld a copy after each renewal with a deploy hook, /etc/letsencrypt/renewal-hooks/deploy/jarl:

#!/bin/sh
set -e
install -d -m 0750 -g jarl /etc/jarl/tls
install -m 0640 -g jarl "$RENEWED_LINEAGE/fullchain.pem" "$RENEWED_LINEAGE/privkey.pem" /etc/jarl/tls/
systemctl reload jarl

systemctl reload jarl sends SIGHUP, and jarld rereads the certificate without dropping connections. More in acme.md.

Upgrading

sudo apt update && sudo apt upgrade      # Debian packages from the registry
make && sudo make install && sudo systemctl restart jarl   # make install

Database migrations run automatically at startup. An older Jarl refuses to start on a database a newer one has upgraded ("database schema version 4 is newer than this Jarl"). So going back to an earlier version, after trying a release candidate say, means restoring a backup taken before the upgrade. Before an upgrade, take a backup (it is safe while jarld runs); jarld restore puts it back if needed:

sudo jarld backup

Uninstalling

cp /etc/bind/named.conf.pre-jarl-<date> /etc/bind/named.conf   # or write a named.conf that includes Jarl's files
rndc reconfig
systemctl disable --now jarl jarl-backup.timer
make uninstall

/etc/bind/jarl and /var/lib/bind/jarl contain plain named.conf and zone files that keep working without Jarl. make uninstall leaves them, Jarl's database and its backups in place.