Pre-releases (versions with a hyphen) now upload their .debs to the "testing" distribution instead of "stable", so servers following stable never get an RC from apt upgrade. An RC can take its release notes from its final version's section or Unreleased; the workflow checks the notes with 'gitea-release.sh notes'. Also fix publishing RCs, whose .deb names carry the Debian version (0.2.0~rc.1) and were rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
408 lines
15 KiB
Markdown
408 lines
15 KiB
Markdown
# 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](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](#from-the-package-registry)), 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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
make deb # dist/jarl_0.1.0_amd64.deb and dist/jarl-cli_0.1.0_amd64.deb
|
|
make deb DEB_ARCH=arm64 # for another architecture (also armhf, i386, ...)
|
|
sudo apt install ./dist/jarl_0.1.0_amd64.deb ./dist/jarl-cli_0.1.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](operations.md#backups)). 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](#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):
|
|
|
|
```sh
|
|
sudo apt install jarl-cli # with the registry above; or: sudo apt install ./jarl-cli_0.1.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](cli.md)
|
|
explains the setup, and several servers in one configuration.
|
|
|
|
### With make install
|
|
|
|
On the DNS server, as root:
|
|
|
|
```sh
|
|
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 ([operations.md](operations.md#backups)) |
|
|
| `/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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
sudo -u jarl jarld init --token \
|
|
--nameservers ns1.adbraun.lol. --contact hostmaster@adbraun.lol
|
|
```
|
|
|
|
It asks for the administrator's password (user `admin`, change it with
|
|
`-u NAME`). `--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
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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](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
|
|
|
|
```sh
|
|
systemctl enable --now jarl
|
|
systemctl status jarl
|
|
journalctl -u jarl -f
|
|
```
|
|
|
|
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](cli.md#on-the-server-itself)).
|
|
|
|
On other machines, or as an ordinary user, give the CLI a server and a
|
|
token:
|
|
|
|
```sh
|
|
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](cli.md#setup)).
|
|
`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](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](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:
|
|
|
|
```sh
|
|
# 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`:
|
|
|
|
```sh
|
|
#!/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](acme.md).
|
|
|
|
## Upgrading
|
|
|
|
```sh
|
|
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. Before an upgrade, take
|
|
a backup (it is safe while jarld runs); `jarld restore` puts it back if
|
|
needed:
|
|
|
|
```sh
|
|
sudo -u jarl jarld backup
|
|
```
|
|
|
|
## Uninstalling
|
|
|
|
```sh
|
|
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.
|