Files
jarl/docs/validation.md
T
adamandClaude Opus 5.5 12013b12d5 Stats heatmap: empty hours same height; cname_chain only for long chains
The "Busiest times" grid's hours without data used the class "empty",
which picked up the padding of empty messages. One CNAME to another no
longer warns; three or more get an info note showing the chain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:58:40 +01:00

183 lines
14 KiB
Markdown

# Validation
Every change is checked twice before named sees it:
1. **Jarl's validator** checks the whole resulting configuration: the DNS
rules, references between objects, and option syntax against the BIND
9.20 grammar. Its messages name the record, zone or option and explain
the rule.
2. **BIND's own checkers**: `named-checkconf -z` on the complete rendered
configuration (which also loads every zone) and `named-checkzone`. Their
messages are mapped back to the object that caused them.
Problems have a severity:
* **error** blocks the change (HTTP 422 `validation_failed`, CLI exit code 6).
* **warning** is shown but allowed: probably a mistake, or a security risk.
* **info** is a hint.
Only problems that a change *introduces* block it. Problems that already
existed are reported separately (`existing_problems`) and don't stop
unrelated changes, though `jarl status`, `jarl check` and the dashboard keep
showing them.
## Records and zones
| Code | Severity | Rule |
|------|----------|------|
| `record_invalid` | error | Record data must parse for its type (addresses, numbers, names, base64/hex fields, ...). TXT/SPF strings are split into 255-byte pieces automatically |
| `record_name_invalid` | error | Owner names must be valid. Host names in A/AAAA/MX owners follow `check-names` rules (letters, digits, hyphens; `_` only where it is normal, e.g. SRV/TXT owners) |
| `record_out_of_zone` | error | The name must be inside the zone (catches `www.example.com` without the final dot) |
| `record_duplicate` | error | The same record can't exist twice |
| `cname_conflict` | error | A CNAME must be the only record at its name (RFC 1034), so no CNAME next to A, MX, TXT... |
| `disabled_conflict` | warning | A disabled record that couldn't be enabled again as the zone is now (a CNAME now shares its name) |
| `cname_multiple` | error | A name can have only one CNAME |
| `cname_apex` | error | No CNAME at the zone apex (it needs SOA and NS) |
| `cname_loop` | error | CNAMEs that point back to themselves |
| `cname_chain` | info | a chain of three or more CNAMEs (one CNAME to another says nothing): works, but each is another lookup for some resolvers |
| `cname_dangling` | warning | CNAME to a name that doesn't exist in the zone |
| `dname_multiple`, `below_dname` | error | One DNAME per name, and nothing below a DNAME (RFC 6672) |
| `apex_ns_missing` | error | A zone needs NS records at its apex |
| `ns_no_address` | error | An in-zone name server needs an A or AAAA record (named refuses to load the zone otherwise) |
| `ns_cname`, `mx_cname`, `srv_cname` | error | NS, MX and SRV targets must not be CNAMEs (RFC 2181 / RFC 2782) |
| `glue_missing` | error | A delegation to a name server inside the delegated zone needs glue records |
| `target_no_address` | warning | A delegation's name server has no address in the zone |
| `occluded` | warning | Records at or below a delegation point are not served (named answers with a referral) |
| `shadowed_by_zone` | warning | Records hidden because a more specific zone exists in the same view |
| `child_not_delegated` | info | A child zone on this server isn't delegated from its parent, so other resolvers can't find it |
| `ds_not_at_delegation` | error | DS records belong at a delegation point |
| `null_mx_not_alone` | error | A null MX (`0 .`) must be the only MX |
| `rrset_ttl_mismatch` | error | All records of an RRset need the same TTL (BIND would silently use the first one) |
| `rrset_too_large`, `too_many_types` | error | BIND 9.20's limits: 100 records per RRset (`max-records-per-type`), 100 types per name (`max-types-per-name`) |
| `spf_multiple`, `dmarc_multiple`, `dmarc_name` | warning | More than one SPF or DMARC policy (receivers then ignore them), or DMARC outside `_dmarc.` |
| `wildcard_unusual` | warning | Wildcard NS records |
| `soa_invalid`, `soa_warning`, `ttl_invalid` | error / warning | SOA fields and TTLs in range and sensible. One warning covers negative caching longer than 3 hours: resolvers cache "no such name" answers for the smaller of the SOA minimum and the zone's default TTL (RFC 2308), so a new name can stay invisible that long. 1 hour (`--minimum 3600`) is a good value |
| `zone_name_invalid`, `zone_type_invalid`, `zone_duplicate` | error | Valid zone names and types. A zone exists once per view |
| `zone_outside_view` | error | Once views exist, every zone must be in one (named requires this). Create the first view with `--adopt-zones` to move all zones into it ([views.md](views.md)) |
| `zone_missing_option` | error | Secondary, stub and mirror zones need `primaries` |
| `zone_file_required` / `zone_file_invalid` / `zone_file_managed` | error | Hint zones need a file. Forward zones have none. Primary zone files are Jarl's |
| `zone_inview_invalid`, `zone_inview_option` | error | `in-view` zones only inside views, and only with `forward`/`forwarders` |
| `zone_inview_disabled` | error | An `in-view` zone can't share a disabled zone |
| `zone_disabled_signed` | error | A signed zone can't be disabled: turn signing off first |
| `zone_records_unmanaged` | error | Only primary zones can have records in Jarl |
| `zone_serial_policy` | error | `date`, `increment` or `unixtime` |
| `zone_no_forwarders`, `zone_hint_name`, `zone_name_slash` | info / warning | Unusual but legal setups |
## ACLs, keys, views, options
| Code | Severity | Rule |
|------|----------|------|
| `acl_element_invalid` | error | Address match elements: addresses, prefixes (host bits must be zero, like BIND requires), ranges (`10.0.10.101-10.0.10.199`: both ends full addresses of one family, start not after end), `!negation`, `key NAME`, ACL names, `{ nested lists }`, `any`/`none`/`localhost`/`localnets`. Ranges in options are checked when they're entered, and only options with address match lists take them (not `forwarders`, say) |
| `range_mapped4` | warning | An IPv6 range that reaches into `::ffff:0:0/96`: named can see IPv4 clients as such addresses, so it may match IPv4 clients too |
| `acl_ref_missing`, `key_ref_missing` | error | Every ACL or TSIG key that is referenced must exist |
| `acl_cycle` | error | ACLs that include each other |
| `acl_duplicate`, `acl_name_invalid`, `acl_empty`, `acl_shadowed` | error / warning | Unique names, built-in names reserved, names that look like addresses or ranges, elements that can never match (a prefix or range an earlier one already covers) |
| `key_*` | error / warning | Valid key names, a supported algorithm, a base64 secret of a sensible length. hmac-md5/sha1 are flagged as weak. Names starting with `jarl-view-` are reserved for the view keys (`key_name_reserved`) |
| `view_duplicate`, `view_name_invalid`, `view_acl_invalid` | error | Valid, unique views and match lists |
| `view_unreachable` | warning | A view that comes after one matching every client is never used |
| `option_unknown`, `option_scope_invalid` | error | The option must exist in BIND 9.20 and be allowed where it is set (server, view, zone of this type, top level) |
| `option_syntax`, `option_value_invalid` | error | The value must parse (balanced braces, strings, the grammar's value type: yes/no, enums, numbers, durations, sizes, address match lists) |
| `option_duplicate` | error | Options that may appear once, appear once |
| `option_managed` | error | Statements Jarl manages itself can't be raw options: `zone`, `view`, `acl`, `key`, `options`, a zone's `type`/`file`/`in-view`/`key-directory`, a view's `match-*`, and `dnssec-policy` in zones, views and server options (set per zone instead; top-level `dnssec-policy NAME { }` definitions are options) |
| `option_deprecated`, `option_obsolete`, `option_unavailable` | warning | Flags from the BIND 9.20 reference |
| `option_experimental` | info | |
| `settings_invalid` | error | Defaults for new zones are valid |
## DNSSEC
These rules come from [dnssec.md](dnssec.md#what-jarl-refuses-and-why).
Those that compare the change with the current state are checked when it is
applied.
| Code | Severity | Rule |
|------|----------|------|
| `dnssec_zone_type` | error | Only primary zones are signed by Jarl |
| `dnssec_policy_unknown` | error | The zone's policy must be `default`, `insecure` or a top-level `dnssec-policy` statement |
| `dnssec_policy_no_keys` | error | A custom policy with an empty `keys { }` list (named 9.20 crashes loading it) |
| `dnssec_ttl_too_long` | error | TTLs (and the default TTL) of a signed zone must be at most the policy's `max-zone-ttl` (a day for the built-in policies); named refuses to load the zone otherwise |
| `dnssec_inline_signing` | error | No `inline-signing no` on a signed zone |
| `dnssec_off_unsafe` | error | A signed zone can't go straight to unsigned: use the `insecure` policy first (or wipe, when the parent has no DS) |
| `dnssec_off_too_early`, `dnssec_off_unknown` | error | Switching off after `insecure` waits until named is done removing the signatures |
| `dnssec_delete_signed` | error | A signed zone can't be deleted; turn signing off first |
| `dnssec_keys_moved` | error | A signed zone can't move to another view (its keys would change) |
| `dnssec_policy_change` | warning | Changing the policy may replace the keys, and with them the DS at the parent |
| `dnssec_old_keys` | info | Signing a zone whose key directory still has old keys |
| `serial_not_higher` | error | An explicit serial on a signed zone must be above the one named has loaded (automatic serials are raised by themselves) |
## TLS
| Code | Severity | Check |
|------|----------|-------|
| `tls_file_missing` | error | The files a top-level `tls` statement names (certificate, key, CA) exist: named doesn't load its configuration without them |
## Linked zones
See [views.md](views.md#linked-zones). Jarl keeps linked zones in step
itself, so these only show when something got past it (a change staged
before the zones were linked, or the database edited by hand).
| Code | Severity | Check |
|------|----------|-------|
| `linked_zone_type` | error | Only primary zones can be linked |
| `linked_zone_name` | error | Linked zones have the same name |
| `linked_mixed` | error | A name and type in a zone is either shared or local, not both |
| `linked_out_of_sync` | error | Every view that doesn't override a shared name and type has the same records for it. Link the zones again (`jarl zone link`) to work out what is shared |
| `linked_zone_alone` | info | A zone linked with no other zone |
## Blocking
From [blocking.md](blocking.md).
| Code | Severity | Rule |
|------|----------|------|
| `blocking_rule_duplicate` | error | One rule per domain and view |
| `blocking_response_policy` | error | No `response-policy` option where Jarl's blocking writes that statement (the view, or the server) |
| `zone_name_reserved` | error | Zone names under `jarl.invalid` are reserved for blocking's policy zones |
| `blocking_view_missing` | error | A list or rule names a view that doesn't exist |
| `blocking_no_view` | warning | A list or rule applies to no view: none offers recursion, and blocking only changes recursive answers |
| `blocking_no_recursion` | warning | A list or rule names a view that doesn't offer recursion, where it has no effect |
| `blocking_step_missing` | error | A list or client group is at a step that doesn't exist (a list can't be at `off`) |
| `client_group_member_invalid` | error | Client group members are addresses, networks, or ACLs made only of those |
| `client_group_overlap` | error | The same address or network is in two client groups of a view |
| `client_group_empty` | warning | A client group has no members |
| `blocklist_step_unused` | warning | A list is at a step that no client group reaches, so it blocks nothing |
## Security lints
These and the exposure checks below only look at views that are
reachable from the internet ([exposure.md](exposure.md)); without views,
the open resolver and open transfer checks look at the server. Any of them
can be acknowledged (it then shows as info, with who accepted it and why),
and a change that newly introduces an open resolver, open transfers, cache
snooping, open NOTIFY or an open management interface on a public view
must be confirmed (`--accept-risk`).
| Code | Severity | What it catches |
|------|----------|-----------------|
| `security_open_resolver` | warning | A view (or the server) where recursion is on and reachable by any client. This includes the BIND trap where `allow-recursion` is unset and inherits `allow-query { any; }`. Open resolvers are abused for DDoS amplification. Set `allow-recursion` to your own networks |
| `security_open_transfer` | warning | `allow-transfer` open to anyone. Restrict it to your secondaries or a TSIG key |
| `dynamic_update_unsupported` | error | `allow-update` / `update-policy` on a Jarl zone. named would write to files that Jarl regenerates, losing the updates. Use the API (e.g. `jarl acme`) instead |
| `exposure_rate_limit` | warning | A public view without response rate limiting (`rate-limit`) |
| `exposure_rate_limit_log_only` | info | Rate limiting in log-only mode: check what it would limit, then enforce |
| `exposure_minimal_any` | warning | A public view answering ANY queries in full (`minimal-any` off) |
| `exposure_cache_snooping` | warning | A public view without recursion whose cache anyone may query (`allow-query-cache`) |
| `exposure_open_notify` | warning | A public view accepting NOTIFY from anyone (`allow-notify`) |
| `exposure_version`, `exposure_hostname` | warning | The server is public and shows its BIND version or host name to anyone (`version none`, `hostname none`) |
| `exposure_management` | warning | `controls` or `statistics-channels` on an address that isn't loopback, open to anyone, while the server is public |
## What only BIND checks
named-checkconf and named-checkzone run on every change, so anything Jarl's
validator doesn't model is still caught before named loads it. Option
values Jarl accepts as raw named.conf syntax, such as `logging` channels
and `rrset-order`, are the main example. Their errors come back as
`validation_failed` (HTTP 422) with the checker's messages in `check`, each
tied to the object it belongs to.
named-checkconf's warnings don't block a change. They are shown with the
change, and the dashboard's configuration checks list them for as long as
they apply (`named_warning`; `named_error` if the running configuration no
longer passes, after a hand edit say). Jarl runs named-checkconf on the
running configuration for this, without loading zones, when the dashboard
asks and the configuration changed, or at most every 10 minutes.