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
- 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.
- 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).
- 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 +.statein/var/cache/bind/keys; keepmanaged-keys.bind*and*.mkeys*in the/var/cache/bindroot; 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
rndcfrom the host, ensure it can read the samerndc.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, setmasters { <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 becomesname.domainA.. For DMARC, use_dmarc IN TXT ...(relative) or_dmarc.domainA. IN TXT ...(absolute).
Common Pitfalls & Fixes
.private: file not found
Keys aren’t inkey-directory. Move allK*(.key/.private/.state) to/var/cache/bind/keys,chown bind:bind,chmod 640, thenrndc loadkeys <zone>.permission deniedfor.jnl/.signed/tmp-*
External zonefilepoints 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 socheckdscouldn’t validate parents. Fix by enabling recursion +dnssec-validation autofor 127.0.0.1 only, and useforward 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 Aand ensure it reportssecure/insecureas expected.dig @203.0.113.10 domainA DNSKEY +dnssec +shortand confirm key tag matches the registrar DS.- SOA serial equality between primary and secondary.
- Watch
dnssec.logfornext key eventlines; after the localhost-only recursion fix, theInvalid NS RRset ...warnings should be gone.
Wrap-up
This layout emphasizes:
- Config vs. state separation:
/etc/bindholds 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.