The tab picked the first other view in view order, so a zone linked with one view opened compared with an unrelated one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
310 lines
13 KiB
Markdown
310 lines
13 KiB
Markdown
# Split DNS with views
|
|
|
|
Views let named answer differently depending on who asks. The usual setup
|
|
for a homelab that is also authoritative on the internet has two views:
|
|
|
|
* **internal**: your networks get recursion (they use the server as their
|
|
resolver) and see private addresses (`nas.adbraun.lol → 10.10.0.20`) and
|
|
internal-only zones such as reverse zones for private networks.
|
|
* **external**: everyone else gets authoritative answers only. That means
|
|
public addresses, no recursion, and no internal zones.
|
|
|
|
## How named treats views (and why Jarl asks to move your zones)
|
|
|
|
Without any `view` statements, named puts every zone into an implicit
|
|
default view. That's why an imported server shows no views. **As soon as
|
|
one view exists, named requires every zone to be inside a view.** So the
|
|
first view you create has to take over the existing zones, including the
|
|
root hints zone `.` that Debian/Ubuntu configure.
|
|
|
|
Jarl does this in the same changeset that creates the view:
|
|
|
|
* web UI: Views → New view. The box "Move the N zone(s) that are not in a
|
|
view into this view" is ticked by default.
|
|
* CLI: `jarl view create NAME --adopt-zones`
|
|
* API: `"adopt_zones": true`
|
|
|
|
Creating the first view without it is refused before anything reaches
|
|
named (`zone_outside_view`).
|
|
|
|
Views are matched in order: the first view whose `match-clients` (and
|
|
`match-destinations`) fit the client wins. Put the specific view first and
|
|
the catch-all last.
|
|
|
|
## Step by step
|
|
|
|
### 1. Check the ACL for your networks
|
|
|
|
The internal view matches your networks. Usually that's the ACL you already
|
|
use for `allow-recursion`:
|
|
|
|
```sh
|
|
jarl acl list
|
|
jarl acl update homelab --add localhost # if missing; see the note below
|
|
```
|
|
|
|
Include **localhost** if the DNS server itself uses `127.0.0.1` as its
|
|
resolver. Otherwise the server's own lookups land in the external view and
|
|
get no recursion.
|
|
|
|
### 2. Create the internal view and move the existing zones into it
|
|
|
|
```sh
|
|
jarl view create internal --match-clients homelab --adopt-zones --dry-run # look first
|
|
jarl view create internal --match-clients homelab --adopt-zones
|
|
```
|
|
|
|
```
|
|
create view internal
|
|
move zone . into view internal
|
|
move zone 0.10.10.in-addr.arpa into view internal
|
|
move zone adbraun.lol into view internal
|
|
applied (changeset 4)
|
|
```
|
|
|
|
Nothing changes for your clients: they match `internal` and get the same
|
|
answers as before. The server-wide options (`recursion`, `allow-recursion`,
|
|
`forwarders`, ...) still apply, because views inherit them.
|
|
|
|
### 3. Create the external view
|
|
|
|
```sh
|
|
jarl view create external --match-clients any
|
|
jarl option set recursion no --view external
|
|
jarl option set allow-recursion '{ none; }' --view external
|
|
```
|
|
|
|
The inherited `allow-recursion { homelab; }` already keeps outsiders from
|
|
recursing. The explicit `recursion no` also stops them reading the cache,
|
|
and makes the intent obvious. Setting `allow-recursion { none; }` as well
|
|
avoids named-checkconf's warning that `allow-recursion` is still active in a
|
|
view with recursion off. Jarl warns (`security_open_resolver`) if a view
|
|
ever offers recursion to everyone.
|
|
|
|
Jarl counts the external view as reachable from the internet (its
|
|
match-clients end in `any`) and checks it: rate limiting, ANY queries,
|
|
the version string and more. See the **Exposure** page or `jarl exposure`,
|
|
and [exposure.md](exposure.md) to set up response rate limiting.
|
|
|
|
### 4. Give the external view its zones
|
|
|
|
**Mostly the same, a few differences (split DNS).** Create the external
|
|
copy *linked* ([below](#linked-zones)): Jarl keeps the shared records the
|
|
same in both views, and each view can have records of its own:
|
|
|
|
```sh
|
|
jarl zone create adbraun.lol --view external --link-with internal
|
|
jarl record set adbraun.lol www A 203.0.113.20 --view external --local # public address, external only
|
|
jarl record delete adbraun.lol nas-internal A --view external # gone from both: it was shared
|
|
```
|
|
|
|
Delete records you don't want outside before you link, or make them local
|
|
to the internal view first (`jarl record update ID --local`): a shared
|
|
record is in both views.
|
|
|
|
**Two separate copies.** `--clone-from` copies the zone once; from then on
|
|
the two are separate zones, and a record added to one isn't in the other:
|
|
|
|
```sh
|
|
jarl zone create adbraun.lol --view external --clone-from adbraun.lol --clone-view internal
|
|
```
|
|
|
|
**The same data in both views.** Use an `in-view` zone. The external view
|
|
then serves the internal view's zone, so there is one copy to maintain:
|
|
|
|
```sh
|
|
jarl zone create example.org --view external --type in-view --in-view internal
|
|
```
|
|
|
|
Don't do that for zones with private data. Internal-only zones (reverse
|
|
zones for 10.x / 192.168.x) simply don't get an external copy: named
|
|
answers outsiders with NXDOMAIN from its built-in empty zones.
|
|
|
|
### 5. Check from both sides
|
|
|
|
```sh
|
|
dig @ns1.adbraun.lol nas.adbraun.lol +short # from the LAN: 10.10.0.20
|
|
dig @<public-ip> nas.adbraun.lol +short # from outside: 203.0.113.20
|
|
dig @<public-ip> example.com # from outside: status REFUSED
|
|
jarl check
|
|
```
|
|
|
|
On the server, `dig -b <source address>` picks which view you test, or,
|
|
from anywhere, sign the query with the view's key (below):
|
|
|
|
```sh
|
|
jarl view key external --named-conf > external.key
|
|
dig -k external.key @ns1.adbraun.lol nas.adbraun.lol +short # 203.0.113.20, even from the LAN
|
|
```
|
|
|
|
## View keys
|
|
|
|
named picks a view by where a query comes from, so a query this server
|
|
sends to its own public address usually lands in the internal view: its
|
|
source is one of your own networks. That makes checks of the external view
|
|
from the server itself (such as Jarl's delegation check) see the wrong
|
|
view.
|
|
|
|
So Jarl gives every view a TSIG key, `jarl-view-NAME`, and puts it at the
|
|
front of the view's `match-clients`:
|
|
|
|
```
|
|
view "internal" {
|
|
match-clients { !key jarl-view-external; key jarl-view-internal; homelab; };
|
|
...
|
|
view "external" {
|
|
match-clients { key jarl-view-external; any; };
|
|
```
|
|
|
|
A query signed with a view's key lands in that view, whatever its source;
|
|
the `!key` stops an earlier view from taking a later view's queries.
|
|
Clients don't send these keys, so for them nothing changes. Jarl signs its
|
|
own queries to this server's addresses with the key of the zone's view.
|
|
The keys live in `keys.conf` with your other keys, never in diffs or in
|
|
`jarl tsig list`; their secrets are derived from a random value in Jarl's
|
|
database (and in its backups), so they stay the same from one apply to
|
|
the next. `jarl view key VIEW`
|
|
shows one (it needs `tsig:write` and is audited). Key names starting with
|
|
`jarl-view-` are reserved, and the view keys can be used in your own
|
|
options (`allow-transfer { key jarl-view-external; }`).
|
|
|
|
## Linked zones
|
|
|
|
Most split-horizon zones are the same in every view apart from a few
|
|
names. Linking zones of the same name in different views lets Jarl keep
|
|
the shared records in step, while each view can still have records of its
|
|
own and its own zone options.
|
|
|
|
* **Shared records** are in every linked view. Adding, changing,
|
|
disabling or deleting one in any view does the same in the others, in
|
|
the same changeset: the diff shows every view, and history, undo and
|
|
`jarl check` treat them together.
|
|
* **Local records** are in one view only (`--local`, or **Only here** in
|
|
the web UI). A local record overrides the shared records of the same name
|
|
and type in that view: `www A 10.0.10.5` local to internal, while the
|
|
external view keeps the shared `www A 203.0.113.20`. Delete the local
|
|
records and the view gets the shared ones back.
|
|
* A new record joins the set of the same name and type: local if that set
|
|
is local in its view, shared otherwise.
|
|
* **Sharing a version** (`jarl record update ID --shared`, **Share** in the
|
|
web UI) makes that view's records of that name and type the shared
|
|
version: the other views' records of that name and type are replaced.
|
|
* Zones stay separate in named, so each keeps its own serial, options
|
|
(allow-transfer, DNSSEC signing, ...) and file, and the views keep their
|
|
own `match-clients`, recursion and order.
|
|
|
|
```sh
|
|
jarl zone link adbraun.lol --view internal --with external # link two existing zones
|
|
jarl zone create adbraun.lol --view vps --link-with external # a new zone, linked from the start
|
|
jarl record list adbraun.lol --view internal # LINK column: shared or local
|
|
jarl record add adbraun.lol nas A 10.0.10.20 --view internal --local
|
|
jarl record update 1234 --local # this name and type: only this view
|
|
jarl record update 1234 --shared # this view's version, everywhere
|
|
jarl zone unlink adbraun.lol --view vps # records stay, all local
|
|
```
|
|
|
|
**Linking never changes what named answers.** A name and type with the
|
|
same records in every view becomes shared; one that differs stays local to
|
|
each view (Jarl lists them). Share the right version afterwards. Unlinking
|
|
keeps every record, all local; a link left with one zone is dissolved, as
|
|
when the other zone is deleted.
|
|
|
|
`--local` also works on a zone that isn't linked (its records are local
|
|
anyway), so scripts don't need to know; `--shared` there is refused.
|
|
|
|
**Permissions** are checked in every view a change reaches. A token with
|
|
a grant for one view only (`views=internal`) can change that view's local
|
|
records and make overrides, but not a shared record, since that would
|
|
change the other views too: Jarl says so and suggests a local record. A
|
|
grant without a view (the usual `zones=adbraun.lol`) covers every view.
|
|
|
|
**Importing a zone file** into a linked zone (`jarl zone import`) adds
|
|
the records like any others: shared, unless the name and type is local
|
|
there. With `--replace`, shared records missing from the file are deleted
|
|
in every linked view.
|
|
|
|
**ACME challenges** are written once per link, and so reach every linked
|
|
view; `jarl acme`, lego's httpreq and certificates Jarl obtains all do
|
|
this.
|
|
|
|
**Compared with BIND's `in-view`**, where one view serves another's zone
|
|
as it is: linked zones can differ per view, keep their own options, and
|
|
don't depend on the view order (`in-view` can only use a view defined
|
|
before it).
|
|
|
|
## Comparing views
|
|
|
|
A zone's **Compare views** tab, or `jarl zone diff`, compares it with the
|
|
same zone in another view (or all of them), from the view you're on.
|
|
The tab starts with a view the zone is linked to, if any, else the
|
|
first other view; `jarl zone diff` compares with every other view unless
|
|
`--with` names some:
|
|
|
|
```
|
|
$ jarl zone diff adbraun.lol --view internal
|
|
adbraun.lol in internal, compared with external
|
|
linked: a difference is a view's own records, there on purpose
|
|
external: 1 answered differently, 24 only in internal, 2 only in external, 31 the same
|
|
|
|
ANSWERED DIFFERENTLY (1)
|
|
nas A
|
|
internal 10.0.10.20 [local, id 812]
|
|
external 203.0.113.20 [local, id 790]
|
|
|
|
ONLY IN INTERNAL (24)
|
|
printer A 10.0.10.30
|
|
router A 10.0.10.1
|
|
...
|
|
|
|
ONLY IN EXTERNAL (2)
|
|
_acme-challenge TXT "…"
|
|
...
|
|
|
|
31 names and types are the same (--all lists them)
|
|
```
|
|
|
|
* **Answered differently**: names and types more than one view has, with
|
|
different records. In linked zones that is a view's own (local)
|
|
records, there on purpose; between zones that aren't linked, it is a
|
|
difference that crept in. **Use this everywhere** gives every view one
|
|
view's records (in linked zones by sharing them: `jarl record update ID
|
|
--shared`).
|
|
* **Only in …**: names and types some views lack. **Add to external**
|
|
gives that view the same records, its own.
|
|
* **The same**: how many; `--all` (or "List them") shows them.
|
|
|
|
One line per record: the CLI cuts long values to fit the terminal
|
|
(`--wide` shows them whole); the web UI shows the whole value on hover.
|
|
The apex NS records are left out, since views often list their name
|
|
servers differently on purpose.
|
|
|
|
**One name.** `jarl zone diff adbraun.lol nas` (and a type: `... @ TXT`)
|
|
shows just that name in every view, the same records included. In the
|
|
web UI, a record's **Compare** button does the same. `--with external`
|
|
(repeat it) picks the views to compare with.
|
|
|
|
## Living with views
|
|
|
|
* Commands that name a zone that exists in several views need `--view`
|
|
(`jarl record add adbraun.lol www A ... --view external`). Jarl refuses to
|
|
guess.
|
|
* `jarl acme` writes challenge records into **every** view that has the
|
|
zone, so Let's Encrypt (external) and internal clients see the same
|
|
thing (once per link, for linked zones). Use `--view` to limit it.
|
|
* `--view all` (or a list) on record commands changes each linked zone
|
|
once: the change reaches the others through the link. With `--local` it
|
|
goes to each view named.
|
|
* `jarl view order internal external` changes the match order, and
|
|
`jarl view list` shows it.
|
|
* Deleting a view is refused while it still has zones, unless you pass
|
|
`--cascade`.
|
|
* Each view has its own page (Views → the view's name): its settings,
|
|
options and zones.
|
|
* Give each view a colour (the view's page → Settings → Colour, or
|
|
`jarl view color internal green`) to tell the zones of each view apart
|
|
in the zone list. The colour only affects the web UI, so it applies at
|
|
once, with no changeset or deploy. `jarl view color internal default`
|
|
goes back to the link colour.
|
|
* Zone files are stored per view: `/var/lib/bind/jarl/internal/adbraun.lol.zone`,
|
|
`/var/lib/bind/jarl/external/adbraun.lol.zone`.
|