Inspect zone file and journal before seeding a zone #20
Reference in New Issue
Block a user
Delete Branch "benvin/zone-seed-journal-safe"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Seeding a zone overwrote its database file at serial 1 unconditionally. With a BIND journal present the skeleton fell outside the journal's serial range, so named failed the load with "out of range"; where it did load, secondaries holding a higher serial refused the transfer under RFC 1982.
WriteSeedZone(..., 1)with noPlanSeed, on atype primary; allow-updatezone under the samezones/directory, so the exact bug this PR fixes still bites RPZ and catalog zones → route both throughZoneDiskState/PlanSeed/Quarantine, or the newWriteSeedZonecontract ("callers must clear it with PlanSeed first") is violated by 2 of its 3 callers.parseZoneDiskState— empty or truncated probe output parses to{ZoneFile:false, Journal:false}, whichPlanSeedturns intoWriteSeedat serial 1 andWriteSeedZonethen clobbers whatever is on the PVC. The probe exits 0 whatever happens, so a degraded exec is indistinguishable from an empty directory → emit a terminating sentinel line from the probe and return an error when it is missing, instead of defaulting to the seed-and-clobber state.!st.ZoneFilebranch ignoresJournalOK: a journal present but unreadable (noodin the image, short read, EACCES, unknown format) is moved aside and the zone reseeded at serial 1 with its real serial unknown.parseZoneDiskState("zonefile=0\njournal=1\njnl=\n")yields{WriteSeed:true Serial:1 QuarantineJournal:true QuarantineSuffix:".orphaned-0"}→ returnSeedPlan{}whenst.Journal && !st.JournalOK.highestSerial— seedshwith 0 and then compares it in RFC 1982 space, so 0 is treated as a serial. With no zone file andJournalEnd >= 2^31,serialLT(0, JournalEnd)is false,hstays 0 and the seed serial drops to 1. Measured:JournalEnd=2147483648→Serial=1,serialLT(2147483648, 1) == false, i.e. a serial regression, and suffix.orphaned-0→ takeJournalEnddirectly when there is no usable zone serial rather than folding it through a zero sentinel.TestPlanSeedSerialWrap— passes for the wrong reason:highestSerialreturns 0 here, sonextSerial'snext == 0branch is never executed and the wrap is untested. The test assertsSerial == 1but never that the seed is newer thanJournalEnd→ assertserialLT(st.JournalEnd, p.Serial)and it will fail as written.api/v1alpha1/bindzone_types.go:51puts nokubebuilder:validation:PatternonzoneName, so a name containing'yields arbitrary shell — now includingmv→ validatezoneNameat the CRD, or pass paths assh -c '...' _ "$1"positional args.moveAside— plainmvoverwrites an existing<path>.orphaned-<serial>, so a second incident computing the same suffix replaces the copy that preserved the original data →mv -n, or make the suffix unique.;BIND LOG V9accepts any futureV9.x; BIND memcmps the full 16 bytes against";BIND LOG V9\n"/";BIND LOG V9.2\n"(lib/dns/journal.c) → match both exactly so a format that moves the begin/end offsets is not silently misparsed.odandhead -cto the BIND image requirements;api/v1alpha1/bindcluster_types.go:61still documents only "named, rndc and nsupdate".internal/bind/seed.go:68-74 —
QuarantineandwriteSeedZoneare two separate execs, so a failure of the second leaves the PVC with no zone file and no journal. The next reconcile probes a clean directory and seeds at serial 1, below the serial the quarantined journal recorded (and below what secondaries already hold) — the serial floor only survives in the.orphaned-<n>filename, which nothing reads. Reachable on any transient exec error/pod eviction between the two calls, and it hits the exact production case the PR fixes (quarantine both, reseed at 17, write fails, reseed at 1). → do the rename and the seed write in onesh -c(seed content on stdin), or recover the floor from*.orphaned-*.internal/bind/seed.go:82, internal/bind/zonestate.go:60-62 —
cat > pathtruncates in place, so an interrupted seed leaves a 0-byte/partial zone file;PlanSeedthen returns the empty plan forever forZoneFile=true, ZoneSerialOK=false(verified: probe on an empty file yields{ZoneFile:true ZoneSerialOK:false},ok=true, plan{}).AddZonefails every reconcile and nothing repairs it without a manual exec. →cat > path.tmp && mv -- path.tmp path.nit: internal/controller/bindcatalogzone_controller.go:57, internal/controller/bindpolicy_controller.go:70 — preserving the on-disk file changes semantics for these two. Catalog members and RPZ rules are add-only via nsupdate with no pruning reconcile; the old seed-at-1 was the only thing that dropped stale member PTRs / stale CNAME rules when named forgot the zone. After this change they persist indefinitely. Decide explicitly whether these two should preserve or reset.
nit: internal/controller/bindzone_controller.go:82 + rndc.go:54 —
DelZoneis plainrndc delzone, leaving db+jnl on the PVC. Delete-then-recreate of a BindZone now resurrects the previous incarnation's records instead of starting from a skeleton.nit: internal/bind/zonestate.go:161-164 —
journalFormatsis pinned to V9/V9.2. A future BIND journal magic makes every journal unjudgeable and hard-blocks any zone whose file is missing; at minimum log the unrecognised magic in theBlockedmessage.internal/bind/seed.go:87-101—seedScriptjoins its steps with\nand never setsset -e, so a failed step does not abort. If eithermoveAsiderename fails, the finalmvstill installs the skeleton over the still-live zone file and the script exits 0, soEnsureSeedZonereports success. Reproduced with a failingmv: the zone file ends up as the skeleton with the old.jnlstill beside it — the exact "out of range" state this PR exists to prevent. → prependset -e(or chain the steps with&&); the explicitrm -f; exit 1guard still works under-e.internal/bind/seed.go:93— the short-read guard fails open. When the command substitution yields nothing (wcnot in the image, or$tmpmissing becausemkdir/catfailed),[ "" -ne <size> ]exits 2, theiftakes the false branch, and the script proceeds to quarantine and install. Reproduced with awcthat exits 127: a 4-byte stdin was installed as the zone file, script exit 0.set -edoes not fix this — the test is anifcondition. → capture first and fail closed, e.g.sz=$(wc -c < tmp) || { rm -f tmp; exit 1; }plus a string compare[ "$sz" != "<size>" ].internal/bind/zonestate.go:253— "quarantineMarker joins a preserved file to the serial it was holding" is no longer true:quarantineSuffixuseshighestSerial, which now folds inOrphanSerialandJournalEnd, so the number can exceed the renamed file's own serial. → reword to "the high-water serial observed when it was preserved".internal/bind/zonestate.go:91—highestSerialfolds three inputs with pairwiseserialLT, which is not a total order; if any two of zone serial / journal end / orphan floor are ~2^31 apart the fold can settle on a value thatnextSerialdoes not put strictly after all three. Unreachable with realistic serials, but nothing documents or rejects the case. → either note the assumption or reject a spread > 2^31 asBlocked.internal/bind/zonestate.go:65— a zone file whose SOA will not parse (0-byte, truncated, unknown layout) returns an empty plan, soEnsureSeedZonereports success and the zone then wedges atAddZonewith an opaque BIND error → returnBlockedwith that reason, as the unreadable-journal case already does.internal/bind/zonestate.go:81— a freshly created BIND journal carriesbegin.serial == end.serial == 0, so this branch quarantines it for any zone serial >= 1; BIND treats such a journal as empty (it compares offsets, not serials) and rolls forward fine → skip the quarantine whenJournalBegin == JournalEnd == 0.internal/bind/zonestate.go:364—Quarantine's generated script is never executed by a test;TestMoveAsidePreservesEarlierQuarantinerunsmoveAsidealone, withoutshellScript'sset -e, so the quarantine-only (!WriteSeed) path has no end-to-end coverage → add a filesystem test for it beside the seed-script ones.