Jarl 0.1.0: manage BIND through a web UI, REST API and CLI

Jarl keeps a BIND 9.20 server's whole configuration (views, ACLs, TSIG
keys, zones of every type, records, named.conf options) in SQLite and
renders named.conf, keys.conf and zone files from it.

Every change goes through one changeset pipeline: whole-configuration
validation, named-checkconf -z and named-checkzone in a staging
directory, atomic install, rndc reconfig/reload, serial verification
with rndc zonestatus, and rollback if named does not load the change.
Changes apply one at a time by default or are bundled and applied
together. Applied changesets can be reverted.

- jarld: the daemon with the REST API and an embedded web UI, plus import,
  adopt, check, resync, backup and local user recovery commands
- jarl: the CLI, including ACME DNS-01 hooks for certbot and acme.sh
- users with argon2id passwords and CSRF-protected sessions; hashed API
  tokens with granular grants and roles; an audit log
- systemd unit, sysusers.d/tmpfiles.d files, Makefile
- unit tests, integration tests against a real named, scripts/e2e.sh
- docs/ guides, README and CHANGELOG

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-07 15:03:13 +00:00
commit bd6b8778e4
122 files changed
+29217

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
# Users, tokens and permissions
## Ways to authenticate
| Who | How | Notes |
|-----|-----|-------|
| People in the web UI | Username + password → session cookie | argon2id password hashes. The session cookie is HttpOnly and SameSite=Strict and lasts 12h (2h idle). Every change also needs the session's CSRF token, which the UI sends in `X-CSRF-Token`. Changing a password logs out the user's other sessions. |
| CLI, scripts, certbot | API token: `Authorization: Bearer jarl_...` | Shown once at creation, stored hashed. Optional expiry. Revocable. Last use time and IP are recorded. |
| Root on the server | `jarld` subcommands (`init`, `user`, `import`, `adopt`, `check`, `resync`, `backup`) | Work directly on the database. They are for bootstrapping and recovery, and are audited as "local". |
Failed logins are rate limited: 8 per username and 20 per client IP within
15 minutes. Failed token attempts count against the IP too. Every attempt
is in the audit log.
A user **without a password** (`--no-password`) is a service account. It
can't log in to the web UI and is used only through its tokens, which suits
certbot, CI jobs and backups.
## Grants
A user's permissions are a list of grants. Administrators (`admin: true`)
may do everything, and only they can manage users, see everyone's tokens
and run `resync`.
A grant is written as:
```
RESOURCE:ACCESS [zones=PATTERNS] [views=PATTERNS] [names=PATTERNS] [types=TYPES]
```
| Resource | Covers |
|----------|--------|
| `zones` | zone definitions (create, delete, SOA, TTL, view) and zone options |
| `records` | records in primary zones |
| `views` | views and view options |
| `acls` | ACLs |
| `tsig` | TSIG keys (`write` is needed to see a secret) |
| `options` | server options, top-level statements, the defaults for new zones, `jarl check` |
| `audit` | everyone's audit events and changesets (without it, users see only their own) |
| `*` | all of the above |
`ACCESS` is `read` or `write` (write includes read). Filters narrow a grant.
Each takes comma separated patterns where `*` and `?` are wildcards, and a
missing filter matches everything:
* `zones=adbraun.lol,*.adbraun.lol`: zone names, without the final dot
* `views=internal`: view names (`_default` means zones outside any view)
* `names=_acme-challenge.*`: record owner names, as FQDNs without the final dot
* `types=TXT,CNAME`: record types
Examples:
```
*:read read everything
records:write zones=lab.adbraun.lol edit records in one zone
zones:write zones=*.lab.adbraun.lol create/delete zones below lab
records:write zones=adbraun.lol views=internal only the internal copy of a split zone
records:write names=_acme-challenge.* types=TXT ACME challenges anywhere
records:write zones=acme.adbraun.lol types=TXT only TXT records in a dedicated ACME zone
```
A user who may see some records of a zone may also see that the zone exists
(its name and type), but not its other records.
### Roles
Roles are ready-made grant lists. Choosing a role copies its grants, which
you can then edit.
| Role | Grants |
|------|--------|
| `readonly` | `*:read` |
| `dns-editor` | `zones:write`, `records:write`, `views:read`, `acls:read`, `options:read` |
| `operator` | `*:write` (everything except user management) |
| `acme` | `records:write names=_acme-challenge.* types=TXT` |
```sh
jarl user create alice --role dns-editor
jarl user update alice --grant "records:write zones=adbraun.lol" --grant "zones:read" # replaces all grants
jarl user update alice --role readonly
```
## Tokens
Every token belongs to a user, and every token request acts as that user.
The audit log shows the user and the token name.
* A token **without grants** has all of its user's permissions, and they
follow the user: if the user loses a permission, so does the token.
* A token **with grants** is restricted to them. Effective permission is
*user grants ∩ token grants*: a token can never do more than its user.
Admin rights don't pass through a restricted token.
* A restricted token can create further tokens, but only ones no wider than
itself.
* Disabling or deleting a user stops all their tokens at once.
```sh
jarl token create laptop # full rights of the user, no expiry
jarl token create ci --grant "records:write zones=lab.adbraun.lol" --expires 90d
jarl token create certbot --user certbot --role acme # admin: a token for another user
jarl token list --all
jarl token revoke 12
```
In the web UI: Account → API tokens (your own) and Users → *user* →
Tokens (admin).
## Recovering access
On the server, as root or the `jarl` user:
```sh
sudo -u jarl jarld user list
sudo -u jarl jarld user passwd admin # set a new password and re-enable the account
sudo -u jarl jarld user add --admin rescue # a new administrator
sudo -u jarl jarld user token admin cli # a new API token for a user
```