Skip to content

Nuface Blog

隨意隨手記 Casual Notes

Menu
  • Home
  • About
  • Services
  • Blog
  • Contact
  • Privacy Policy
  • Login
Menu

Building a Split-Horizon + DNSSEC Authoritative/Recursive DNS with BIND 9.21

Posted on 2025-11-052025-11-05 by Rico

This post turns my containerized BIND 9.21 deployment notes into a step-by-step guide. To protect the real environment, all domains are anonymized as domainA and domainB, and public IPs are shown as examples (e.g., 203.0.113.0/24).


Target Architecture

  1. Single BIND instance with two views:
  • internal view: serves authoritative answers for internal zones and recursion (forwarding to upstreams). No DNSSEC validation on the internal resolver for simplicity.
  • external view: authoritative-only to the Internet (no public recursion). Zones are DNSSEC-signed. To let KASP run parent DS health checks, recursion + validation are enabled only for 127.0.0.1.
  1. Security boundaries
  • Public side refuses recursion; authoritative answers only.
  • AXFR/NOTIFY use TSIG and are allowed only to the designated secondary for the external zones.
  • Disable IPv6 listening (IPv4 only).
  1. Container-friendly filesystem layout
  • Config stays in /etc/bind/ (read-only).
  • Everything BIND writes (signatures, journals, keys, KASP state, trust anchors) lives under /var/cache/bind (writable, persistent).

Recommended Directory Layout

/etc/bind/
  ├─ named.conf
  ├─ named.conf.options
  ├─ named.conf.logging
  ├─ named.conf.acl
  ├─ named.conf.views
  └─ keys/
       └─ rndc.key            # TSIG for RNDC control channel

/var/cache/bind/               # options { directory } points here
  ├─ zones/
  │   └─ ext/                  # external view zone files (will create .signed/.jnl/tmp-*)
  │       ├─ db.domainA
  │       └─ db.domainB
  ├─ keys/                     # key-directory: DNSSEC K* keys + .state
  │   ├─ KdomainA.+013+11652.{key,private,state}
  │   └─ KdomainB.+013+51162.{key,private,state}
  ├─ managed-keys.bind{,.jnl}  # trust anchors (RFC 5011) – auto-managed
  └─ external.mkeys{,.jnl}     # per-view trust anchor state – auto-managed

Key points: put all K* key files + .state in /var/cache/bind/keys; keep managed-keys.bind* and *.mkeys* in the /var/cache/bind root; place external zone files in /var/cache/bind/zones/ext.


Core Configuration Snippets

1) options: working dir, key dir, IPv4-only listening

// /etc/bind/named.conf.options
options {
    directory "/var/cache/bind";
    key-directory "/var/cache/bind/keys";

    listen-on { any; };
    listen-on-v6 { none; };           // don’t listen on IPv6:53

    recursion no;                      // overridden per-view
    // Don’t set dnssec-validation globally; do it per view
};

2) RNDC control channel (bind to 127.0.0.1 only)

// /etc/bind/named.conf
include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.logging";
include "/etc/bind/named.conf.acl";
include "/etc/bind/keys/rndc.key";

controls {
  inet 127.0.0.1 port 953 allow { 127.0.0.1; } keys { "rndc-key"; };
};

include "/etc/bind/named.conf.views";

If you want to run rndc from the host, ensure it can read the same rndc.key (bind-mount or use -c).

3) internal view: internal auth + recursion (no DNSSEC validation)

// /etc/bind/named.conf.views
view "internal" {
    match-clients { "internal_nets"; };      // define in named.conf.acl, e.g. 192.168.10.0/24
    recursion yes;
    dnssec-validation no;                    // simplest user experience internally

    allow-recursion   { "internal_nets"; 127.0.0.1; };
    allow-query       { "internal_nets"; 127.0.0.1; };
    allow-query-cache { "internal_nets"; 127.0.0.1; };

    // forward recursion to upstreams (reduces noise & outbound chatter)
    forwarders { 1.1.1.1; 8.8.8.8; };
    forward first;

    // internal authoritative zones (unsigned)
    zone "domainA" { type master; file "/etc/bind/zones/int/db.domainA"; };
    zone "domainB" { type master; file "/etc/bind/zones/int/db.domainB"; };

    // localhost & reverse zones (optional) live here
    zone "localhost"        { type master; file "/etc/bind/db.local"; };
    zone "127.in-addr.arpa" { type master; file "/etc/bind/db.127";  };

    // root hints used only by internal view
    zone "." { type hint; file "/etc/bind/root.hints.v4"; };
};

4) external view: public authoritative (signed), local-only recursion+validation for KASP

view "external" {
    match-clients { any; };

    // allow recursion+validation ONLY to localhost so KASP can check DS
    recursion yes;
    allow-recursion   { 127.0.0.1; };
    allow-query-cache { 127.0.0.1; };
    dnssec-validation auto;

    forwarders { 1.1.1.1; 8.8.8.8; };
    forward only;

    allow-query { any; };               // authoritative to the Internet
    // no public cache: allow-query-cache isn’t open to “any”

    // public authoritative zones (signed)
    zone "domainA" {
        type master;
        file "zones/ext/db.domainA";   // relative to /var/cache/bind
        notify yes;
        allow-transfer { key xfr-key; 203.0.113.53; };   // secondary
        also-notify   { 203.0.113.53 key xfr-key; };
        dnssec-policy default;
        inline-signing yes;
    };

    zone "domainB" {
        type master;
        file "zones/ext/db.domainB";
        notify yes;
        allow-transfer { key xfr-key; 203.0.113.53; };
        also-notify   { 203.0.113.53 key xfr-key; };
        dnssec-policy default;
        inline-signing yes;
    };
};

TSIG: generate with tsig-keygen -a hmac-sha256 xfr-key > /etc/bind/keys/xfr-key.key. Include on both primary and secondary. On the secondary, set masters { <PRIMARY_IP> key xfr-key; };.


Zone File Tips: TXT length & $ORIGIN

  • DKIM TXT often exceeds 255 bytes; split into multiple quoted chunks (BIND concatenates them): selector._domainkey IN TXT ( "v=DKIM1; k=rsa; p=MIIBI..." "...rest of base64..." )
  • With $ORIGIN domainA., a name without trailing dot becomes name.domainA.. For DMARC, use _dmarc IN TXT ... (relative) or _dmarc.domainA. IN TXT ... (absolute).

Common Pitfalls & Fixes

  • .private: file not found
    Keys aren’t in key-directory. Move all K* (.key/.private/.state) to /var/cache/bind/keys, chown bind:bind, chmod 640, then rndc loadkeys <zone>.
  • permission denied for .jnl/.signed/tmp-*
    External zone file points to read-only location (e.g., /etc/bind). Place external zones under /var/cache/bind/zones/ext.
  • dnssec-validation redefined
    Set it only once per scope. Prefer per-view and remove the global one.
  • KASP noise: Invalid NS RRset for 'tw' ... no valid RRSIG
    External view had recursion/validation disabled so checkds couldn’t validate parents. Fix by enabling recursion + dnssec-validation auto for 127.0.0.1 only, and use forward only;.

Verification Checklist (copy-paste ready)

Variables:
Primary (public) = 203.0.113.10, Secondary = 203.0.113.53

Internal (from an internal host)

dig @203.0.113.10 www.domainA A                 # internal authoritative
dig @203.0.113.10 example.com A                 # internal recursion (should answer)

External (from an Internet host)

dig @203.0.113.10 www.domainA A +norecurse      # flags: aa; no recursion
dig @203.0.113.10 example.com A                 # should be REFUSED (no recursion)
dig @203.0.113.10 www.domainA A +dnssec +norecurse +multiline  # expect RRSIG present

DNSSEC chain (requires DS at the registrar)

delv @1.1.1.1 www.domainA A                     # expect “secure” (or “insecure” before DS is live)
dig  @1.1.1.1 domainA DS +dnssec +multiline     # key tag/alg/digest should match your KSK

Primary/Secondary sync (TSIG)

# On the secondary (with TSIG)
dig @203.0.113.10 domainA AXFR -y hmac-sha256:xfr-key:<BASE64_SECRET>
# From anywhere without TSIG → should fail
dig @203.0.113.10 domainA AXFR
# SOA serial parity
dig @203.0.113.10 domainA SOA +short
dig @203.0.113.53 domainA SOA +short

RNDC & signing state

rndc -c /etc/bind/keys/rndc.key status
rndc -c /etc/bind/keys/rndc.key signing -list domainA
rndc -c /etc/bind/keys/rndc.key signing -list domainB

NAT/Forwarding Notes (optional)

  • If a gateway DNATs Internet 53 → primary 203.0.113.10:53, make sure both UDP and TCP 53 are forwarded.
  • For internal clients, prefer pointing directly to the primary DNS (fewer moving parts). If you must use a jump host, avoid SNAT on LAN→LAN DNS so BIND sees real client IPs.

Suggested Daily Health Checks

  • Run:
    • delv @1.1.1.1 www.domainA A and ensure it reports secure/insecure as expected.
    • dig @203.0.113.10 domainA DNSKEY +dnssec +short and confirm key tag matches the registrar DS.
    • SOA serial equality between primary and secondary.
  • Watch dnssec.log for next key event lines; after the localhost-only recursion fix, the Invalid NS RRset ... warnings should be gone.

Wrap-up

This layout emphasizes:

  • Config vs. state separation: /etc/bind holds configs; everything writable lives under /var/cache/bind (KASP state, signatures, journals, trust anchors).
  • Clear view policy: keep the internal resolver simple (no validation), keep the external authoritative strict (signed), and expose recursion/validation only to localhost for KASP’s checkds.
  • Tight boundaries: refuse public recursion, protect AXFR with TSIG, and disable IPv6 listening if not needed.

With these pieces in place, a single BIND instance can reliably serve distinct internal/external needs and maintain DNSSEC with sane, maintainable defaults.

Recent Posts

  • Postfix + Let’s Encrypt + BIND9 + DANE Fully Automated TLSA Update Guide
  • Postfix + Let’s Encrypt + BIND9 + DANE TLSA 指紋自動更新完整教學
  • Deploying DANE in Postfix
  • 如何在 Postfix 中部署 DANE
  • DANE: DNSSEC-Based TLS Protection

Recent Comments

  1. Building a Complete Enterprise-Grade Mail System (Overview) - Nuface Blog on High Availability Architecture, Failover, GeoDNS, Monitoring, and Email Abuse Automation (SOAR)
  2. Building a Complete Enterprise-Grade Mail System (Overview) - Nuface Blog on MariaDB + PostfixAdmin: The Core of Virtual Domain & Mailbox Management
  3. Building a Complete Enterprise-Grade Mail System (Overview) - Nuface Blog on Daily Operations, Monitoring, and Performance Tuning for an Enterprise Mail System
  4. Building a Complete Enterprise-Grade Mail System (Overview) - Nuface Blog on Final Chapter: Complete Troubleshooting Guide & Frequently Asked Questions (FAQ)
  5. Building a Complete Enterprise-Grade Mail System (Overview) - Nuface Blog on Network Architecture, DNS Configuration, TLS Design, and Postfix/Dovecot SNI Explained

Archives

  • December 2025
  • November 2025
  • October 2025

Categories

  • AI
  • Apache
  • Cybersecurity
  • Database
  • DNS
  • Docker
  • Fail2Ban
  • FileSystem
  • Firewall
  • Linux
  • LLM
  • Mail
  • N8N
  • OpenLdap
  • OPNsense
  • PHP
  • QoS
  • Samba
  • Switch
  • Virtualization
  • VPN
  • WordPress
© 2025 Nuface Blog | Powered by Superbs Personal Blog theme