ACLs, views' match lists, options with address match lists and blocking client groups take ranges such as 10.0.10.101-10.0.10.199 or 2001:db8::100-2001:db8::1ff (negated, or with spaces around the dash, too). Jarl stores the range as written and writes the fewest prefixes that cover exactly that range to named.conf, with the range as a comment. New package internal/iprange; namedconf gets an AMRange element (a range used to parse as an ACL name), ExpandRanges and statement comments. Mistakes are refused when entered, validation sees what ranges cover, and IPv6 ranges in the IPv4-mapped space get a warning. The web UI shows what a range becomes while typing (GET /catalog/ranges). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 KiB
Validation
Every change is checked twice before named sees it:
- 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.
- BIND's own checkers:
named-checkconf -zon the complete rendered configuration (which also loads every zone) andnamed-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) |
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. 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.
| 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.