A copied DNS zone is not ready for a cPanel change window merely because named-checkzone exits with status 0. In the reproduced BIND 9.20.26 run for this article, a zone containing leak.other.test. produced ignoring out-of-zone data and still returned 0. BIND excluded the foreign owner name; the process status alone did not say that every intended record survived.
Use a fail-closed admission rule instead: preserve and hash the source, reject any serious diagnostic, compile only a clean canonical copy, serve that copy on a high loopback port, and query the records the handoff promises. This workflow touches neither cPanel nor authoritative DNS. It creates an evidence package for the person who owns the later change.
The procedure is aimed at agencies and migration operators. It assumes root access to a disposable Debian-style lab with BIND utilities installed, not to the production cPanel server. If the wider move also includes account data, inspect the cPanel backup before its restore window as a separate admission problem; a clean zone cannot validate a broken cpmove archive.
ISC’s named-checkzone performs the same zone checks used when named loads a zone, and its manual documents success and failure status. That makes it essential, but not sufficient for this handoff. The current BIND named-checkzone manual exposes strict policy levels for integrity, MX, NS, reverse, SRV and other checks. A migration gate must also inspect diagnostic text when BIND deliberately ignores data outside the named zone.
cPanel’s first-party guidance tells operators to run named-checkzone against the zone file, while its out-of-zone support article identifies foreign owner names, duplicate data and CNAME coexistence as real causes of invalid zones. Those pages explain repair on a cPanel host. Here, the earlier question is narrower: should this copied artifact enter the change window at all?
| Evidence surface | Admit when | Reject when | Reader decision |
|---|---|---|---|
| Source identity | hash matches the reviewed copy | source changes during rehearsal | stop and re-review the file |
| Strict parser | exit 0 and no reject-class diagnostic | nonzero exit or ignored/warning/error text | repair the source copy |
| Negative controls | all malformed fixtures lose | any bad fixture is admitted | fix the wrapper before trusting it |
| Local authority | expected SOA, NS, A, MX and CNAME answer | value, count or status differs | reconcile intended inventory |
| Scope and cleanup | only loopback/owned path used; both disappear | public DNS, cPanel, foreign PID or leftover path involved | abort the handoff |
This table is a contract, not a claim that syntax proves the entire DNS system. Delegation, DNSSEC, firewall behavior, transport fallback, caching and the eventual cPanel import remain separate stages.
Do not begin under /var/named. cPanel’s named-tools article uses the live cPanel path because it diagnoses an existing server. A preflight should work on a copied file in a disposable scope, so a parser experiment cannot change a live zone, serial or reload state.
The example uses handoff.test, a reserved testing name, and documentation-only addresses from 192.0.2.0/24. Replace it with a copied customer zone only after the fixture passes. Keep the original outside the lab as the review source of truth.
Place all nine tested-input blocks in one root-owned script, in order. The first block refuses an existing path and a busy TCP or UDP port. Its cleanup function will delete only the exact marker-owned directory; broad paths and globs are intentionally absent.
set -Eeuo pipefail
LAB=/var/lib/bind/voxfor-zone-reader-189
MARKER="$LAB/.voxfor-zone-reader-189"
ZONE_NAME=handoff.test
PORT=15389
ZONE="$LAB/handoff.test.zone"
CANONICAL="$LAB/handoff.test.canonical.zone"
NAMED_PID=""
fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
owned_cleanup() {
if [[ -n ${NAMED_PID:-} ]] && kill -0 "$NAMED_PID" 2>/dev/null; then
kill "$NAMED_PID"
wait "$NAMED_PID" 2>/dev/null || true
fi
NAMED_PID=""
if [[ -d "$LAB" ]]; then
[[ "$LAB" == /var/lib/bind/voxfor-zone-reader-189 ]] || fail "unexpected lab path"
[[ -f "$MARKER" ]] || fail "refusing cleanup without marker"
rm -rf -- "$LAB"
fi
}
trap 'rc=$?; if [[ $rc -ne 0 ]]; then owned_cleanup || true; fi; exit $rc' EXIT
[[ "$(id -u)" -eq 0 ]] || fail "run as root so named can drop to bind"
for tool in named-checkzone named-compilezone named dig sha256sum ss awk grep sed install chown; do
command -v "$tool" >/dev/null || fail "missing tool: $tool"
done
[[ ! -e "$LAB" ]] || fail "lab path already exists"
! ss -H -ltn "( sport = :$PORT )" | grep -q . || fail "TCP port is busy"
! ss -H -lun "( sport = :$PORT )" | grep -q . || fail "UDP port is busy"
Production zones may contain far more record types, includes or generated data than this fixture. Preserve those semantics when copying. If an agency is still deciding which panel owns the destination workflow, settle the cPanel and DirectAdmin operating-model decision before adapting panel-specific handoff steps.
Now create the reserved source and record its hash. The SOA serial is evidence carried by the source; the lab does not increment it because no publication occurs.
install -d -m 0750 -o bind -g bind "$LAB"
install -m 0640 -o bind -g bind /dev/null "$MARKER"
cat > "$ZONE" <<'ZONEFILE'
$ORIGIN handoff.test.
$TTL 300
@ IN SOA ns1.handoff.test. hostmaster.handoff.test. (
2026081601 3600 900 1209600 300
)
@ IN NS ns1.handoff.test.
@ IN NS ns2.handoff.test.
ns1 IN A 192.0.2.53
ns2 IN A 192.0.2.54
@ IN A 192.0.2.10
www IN CNAME handoff.test.
mail IN A 192.0.2.20
@ IN MX 10 mail.handoff.test.
_acme-challenge IN TXT "placeholder-token"
ZONEFILE
chown bind:bind "$ZONE"
chmod 0640 "$ZONE"
SOURCE_SHA="$(sha256sum "$ZONE" | awk '{print $1}')"
printf 'source_sha256=%s\n' "$SOURCE_SHA"
A hash proves file identity, not DNS correctness. Its job is to show that the exact copy reviewed before the run remains unchanged after parsing and local queries.
Strict checking and canonical compilation answer different questions. named-checkzone decides whether the source is admissible under the selected policies. named-compilezone can emit a normalized text representation, but the current named-compilezone manual warns that its default integrity behavior differs. The wrapper therefore runs the strict checker first and passes the same levels to the compiler.
The diagnostic policy is intentionally conservative for a handoff artifact. It rejects nonzero status plus ignoring out-of-zone data, warnings, load failures and error text. A real zone that relies on a legitimate warning needs an explicit reviewed exception; do not weaken the generic wrapper until it passes.
STRICT_ARGS=(-i local -k fail -m fail -M fail -n fail -r fail -S fail)
admit_zone() {
local input=$1 output=$2 log=$3 rc=0
if named-checkzone "${STRICT_ARGS[@]}" "$ZONE_NAME" "$input" >"$log" 2>&1; then
rc=0
else
rc=$?
fi
if [[ "$rc" -ne 0 ]] || grep -Eiq 'ignoring out-of-zone data|warning|not loaded|failed|error' "$log"; then
return 1
fi
named-compilezone "${STRICT_ARGS[@]}" -F text -o "$output" "$ZONE_NAME" "$input" >>"$log" 2>&1
}
admit_zone "$ZONE" "$CANONICAL" "$LAB/check.log"
CANONICAL_SHA="$(sha256sum "$CANONICAL" | awk '{print $1}')"
printf 'canonical_sha256=%s\n' "$CANONICAL_SHA"
Canonical output is a candidate to serve locally, not a replacement silently written over the source. Retaining both hashes lets the change owner identify which file was reviewed and which normalized copy produced the query receipt.
Clean-fixture success establishes only the happy path. The gate becomes credible when malformed controls fail for the reasons that matter. These are not one operation divided into filler: out-of-zone data can escape the zone while returning 0, a missing apex NS removes delegation data, and CNAME coexistence violates a different owner-name rule.
First append a fully qualified owner outside handoff.test. Record the native return code, require the diagnostic, then require the wrapper to reject it.
OUT_OF_ZONE="$LAB/out-of-zone.zone"
cp "$ZONE" "$OUT_OF_ZONE"
printf '%s\n' 'leak.other.test. IN A 192.0.2.90' >> "$OUT_OF_ZONE"
if named-checkzone "${STRICT_ARGS[@]}" "$ZONE_NAME" "$OUT_OF_ZONE" >"$LAB/out-of-zone-native.log" 2>&1; then
OUT_OF_ZONE_NATIVE_RC=0
else
OUT_OF_ZONE_NATIVE_RC=$?
fi
grep -q 'ignoring out-of-zone data' "$LAB/out-of-zone-native.log"
[[ "$OUT_OF_ZONE_NATIVE_RC" -eq 0 ]]
! admit_zone "$OUT_OF_ZONE" "$LAB/out-of-zone.canonical" "$LAB/out-of-zone-wrapper.log"
That negative control is the central reason to avoid a status-only gate. The repair is usually an owner-name or origin correction, not permission to ignore the message.
Next remove both apex NS rows. The strict checker must refuse a zone that cannot name its own authoritative servers.
MISSING_NS="$LAB/missing-ns.zone"
awk '!/ IN NS /' "$ZONE" > "$MISSING_NS"
! admit_zone "$MISSING_NS" "$LAB/missing-ns.canonical" "$LAB/missing-ns.log"
grep -q 'has no NS records' "$LAB/missing-ns.log"
Finally add an A record beside the existing www CNAME. DNS owner names cannot carry CNAME and unrelated record data together.
CNAME_CONFLICT="$LAB/cname-conflict.zone"
cp "$ZONE" "$CNAME_CONFLICT"
printf '%s\n' 'www IN A 192.0.2.99' >> "$CNAME_CONFLICT"
! admit_zone "$CNAME_CONFLICT" "$LAB/cname-conflict.canonical" "$LAB/cname-conflict.log"
grep -Eq 'CNAME and other data|CNAME.*other data' "$LAB/cname-conflict.log"
Each failure sends the artifact back to source review. Do not “fix” a customer zone by commenting out unfamiliar records merely to obtain a green command. Establish the intended record inventory with the zone owner, correct the copy, rerun all controls, and generate a new receipt.
Parser acceptance covers syntax and structural relationships. It does not demonstrate that the normalized copy returns the values the migration owner expects. Start one authoritative named process on 127.0.0.1:15389, disable recursion and allow only loopback queries. The configuration serves the canonical file, never the original and never a public interface.
CONF="$LAB/named.conf"
cat > "$CONF" <<EOF
options {
directory "$LAB";
listen-on port $PORT { 127.0.0.1; };
listen-on-v6 { none; };
recursion no;
allow-query { 127.0.0.1; };
pid-file "$LAB/named.pid";
session-keyfile "$LAB/session.key";
};
zone "$ZONE_NAME" {
type primary;
file "$(basename "$CANONICAL")";
};
EOF
chown bind:bind "$CONF" "$CANONICAL"
chmod 0640 "$CONF" "$CANONICAL"
named -g -c "$CONF" -u bind >"$LAB/named.log" 2>&1 &
NAMED_PID=$!
for attempt in $(seq 1 20); do
dig @127.0.0.1 -p "$PORT" "$ZONE_NAME" SOA +short >"$LAB/soa.out" 2>/dev/null && [[ -s "$LAB/soa.out" ]] && break
sleep 0.2
done
[[ -s "$LAB/soa.out" ]] || fail "loopback named did not become ready"
Query the apex and each record shape that defines the handoff. A missing owner provides a negative control for the local authority. The checks compare exact values rather than treating any response as success.
SOA_SERIAL="$(awk '{print $3}' "$LAB/soa.out")"
NS_COUNT="$(dig @127.0.0.1 -p "$PORT" "$ZONE_NAME" NS +short | sed '/^$/d' | wc -l)"
APEX_A="$(dig @127.0.0.1 -p "$PORT" "$ZONE_NAME" A +short)"
MX_VALUE="$(dig @127.0.0.1 -p "$PORT" "$ZONE_NAME" MX +short)"
WWW_CNAME="$(dig @127.0.0.1 -p "$PORT" "www.$ZONE_NAME" CNAME +short)"
NX_STATUS="$(dig @127.0.0.1 -p "$PORT" "missing.$ZONE_NAME" A +noall +comments | sed -n 's/.*status: \([^,]*\),.*/\1/p')"
[[ "$SOA_SERIAL" == 2026081601 ]]
[[ "$NS_COUNT" == 2 ]]
[[ "$APEX_A" == 192.0.2.10 ]]
[[ "$MX_VALUE" == '10 mail.handoff.test.' ]]
[[ "$WWW_CNAME" == 'handoff.test.' ]]
[[ "$NX_STATUS" == NXDOMAIN ]]
[[ "$(sha256sum "$ZONE" | awk '{print $1}')" == "$SOURCE_SHA" ]]
NXDOMAIN here only proves that the admitted local copy lacks that name. After a real cutover, resolvers may continue returning an earlier negative result until its cache expires; DNS negative-cache timing is a different post-change check.
Likewise, a small loopback answer cannot prove real network transport behavior. Large DNSSEC or TXT responses may trigger truncation and TCP retry, so test DNS UDP truncation and TCP fallback after the eventual authoritative service exists.
The representative lab emitted this receipt:
bind_version=9.20.26-1~deb13u1-Debian
zone=handoff.test
source_sha256=9d01321907ed13d63249fc2fefe22f8f9f8918c042aca24682f5cf246059de41
canonical_sha256=bf81f10b38cafa4f5f1fab275921d245c95ee846d349624be0fae4e0b1690c98
native_out_of_zone_exit=0
wrapper_out_of_zone=rejected
wrapper_missing_ns=rejected
wrapper_cname_conflict=rejected
soa_serial=2026081601
ns_count=2
apex_a=192.0.2.10
mx=10 mail.handoff.test.
www_cname=handoff.test.
missing_status=NXDOMAIN
source_unchanged=yes
cleanup=absent
These values prove one reserved fixture under one BIND build. Reproduce the gate with the destination’s utility version and compare a real zone against its approved inventory; do not reuse the hashes as if they certified another file.
The later cPanel or WHM action is intentionally outside this article. Current WHM DNS Zone Manager documentation covers the product surface, including raw-zone management, but a material click path would require original current screenshots from the actual destination. This non-interface workflow stops before that boundary.
Give the change owner the source file name and hash, canonical file name and hash, BIND version, complete check log, three negative-control results, record query matrix, NXDOMAIN result, timestamp and operator identity. Add the intended destination and rollback owner in the change ticket. Do not hand over only a screenshot of a green terminal.
Google Cloud’s DNS migration guidance similarly verifies target name servers directly before changing registrar delegation. DNSControl’s migration workflow advises duplicating first and avoiding unrelated cleanup during the move. Apply that discipline here: one artifact change, one controlled import, then direct verification against the new authority before delegation or cache-dependent conclusions.
Signed zones need another explicit boundary. An unsigned copy can pass this BIND gate while the eventual DNSSEC chain still fails, so use the DNSSEC chain-of-trust repair workflow when DS, DNSKEY, RRSIG or validating-resolver behavior is in scope. Multi-provider designs also need secondary DNS convergence checks after every authority has received the admitted data.
Accept the handoff only when the strict checker and fail-closed diagnostic policy admit the copied source, all three malformed controls are rejected, the canonical copy answers every approved SOA, NS, A, MX and CNAME value on loopback, the negative query returns NXDOMAIN, the source hash is unchanged, and both the owned path and port are absent after cleanup. Any missing fact is a rejection, not a warning to carry into the cPanel window.
named-checkzone return success while ignoring a DNS record?Yes. In BIND 9.20.26, the reproduced out-of-zone fixture printed ignoring out-of-zone data and returned exit status 0. A cPanel DNS admission gate should inspect diagnostics as well as status and reject the artifact until the foreign owner name is understood.
/var/named?No for this preflight. Copy the zone into a marker-owned lab and hash the reviewed source so testing cannot alter a live cPanel file, serial or reload state. Diagnose /var/named only inside a separately approved production incident or change procedure.
Canonical text gives the local authoritative rehearsal a normalized artifact produced by the same BIND toolchain. Keep the original source and both hashes; canonical output does not replace source review or prove every intended answer by itself.
It does not. Offline admission proves parser policy and local answers for one copy. Delegation, glue, firewall rules, DNSSEC, secondary convergence, UDP/TCP behavior and resolver caches still require verification against the eventual authoritative system.
An offline rehearsal does not require a serial change. Reading, hashing, compiling and locally querying a copy do not publish a new zone version, so this workflow preserves the source serial. Increment the serial only in the controlled change that actually modifies or publishes zone content, following the destination’s policy.
Provide the source and canonical hashes, BIND version, strict-check log, negative-control outcomes, exact local query receipt, cleanup proof, intended destination, operator timestamp and rollback owner. That package identifies what was tested and what remains for the change owner to verify.
Reject it when the source hash changes unexpectedly, any parser diagnostic is unresolved, a malformed control passes, an intended answer differs, NXDOMAIN is not observed for the negative name, scope reaches production, or cleanup leaves the owned listener or directory behind.
Cleanup is part of the evidence, not housekeeping postponed until after approval. Stop only the process started by this script, require the marker before removing the exact directory, then prove the listener and path are gone.
kill "$NAMED_PID"
wait "$NAMED_PID" 2>/dev/null || true
NAMED_PID=""
[[ "$LAB" == /var/lib/bind/voxfor-zone-reader-189 ]]
[[ -f "$MARKER" ]]
rm -rf -- "$LAB"
trap - EXIT
[[ ! -e "$LAB" ]]
! ss -H -ltn "( sport = :$PORT )" | grep -q .
! ss -H -lun "( sport = :$PORT )" | grep -q .
printf 'cleanup=absent\n'
If any admission or query criterion fails, do not import the zone and do not alter public delegation. Stop only the PID created by the marker-owned lab, remove only /var/lib/bind/voxfor-zone-reader-189, preserve the reviewed source outside that directory, attach the failed diagnostic to the change record and return the copy to its owner for correction. Rerun the complete gate after every source change.
A useful handoff fits one sentence: this exact source hash produced this canonical hash, rejected all three bad controls, answered this approved record inventory locally and left no lab state behind. Until the receipt supports every clause, the zone is still a candidate—not a production artifact.