# 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` | warning | CNAME to a CNAME: works, but slows resolution | | `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) | ## 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 | 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 | ## 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.