Make intra-cluster NOTIFY loop-free (TSIG-keyed, no pod IPs in restart config)
v0.2.5 (PR #14) added an options-scope allow-notify enumerating the primary pod IP on secondaries. Options-scope config feeds the config-hash annotation that rolls the StatefulSet, so any config change rolled the pods, the primary came back on a new pod IP, the operator re-rendered with the new IP, the hash changed, the pods rolled again — an infinite roll loop across every BindCluster. The prod deployment was reverted to v0.2.4. Replace the pod-IP allow-notify with TSIG-authenticated NOTIFY: - Secondaries render `allow-notify { key "<name>"; };` — a static key element with NO IPs. It depends only on the key name, so pod-IP churn can never change the render, the config-hash, or trigger a restart. - The primary signs its outgoing NOTIFYs: the zone-scope also-notify entries (already enumerating replica pod IPs, applied via rndc addzone/modzone with NO restart) now carry `key "<name>"`. - Key choice: reuse the cluster's catalog transfer TSIG key (TransferKeyRef). Secondaries already present it for AXFR and it is in keys.conf on every pod, so no new key plumbing is needed. Add a permanent regression guard for the loop class: - controller: reconcile the ConfigMap with the primary pod on two different IPs and assert the config-hash is byte-identical. - render: render restart-scoped input and assert no pod IP appears in allow-notify; RenderInput no longer has any pod-IP field. Zone-scope also-notify (rndc, no restart) legitimately still lists pod IPs; only restart-scoped config must be pod-IP-independent.
This commit is contained in:
@@ -2,6 +2,7 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -58,12 +59,19 @@ func recordsToUpdates(zone string, records []bindv1alpha1.Record, defaultTTL int
|
||||
// updateKeyName returns the TSIG key name (as used in named.conf) for a zone's
|
||||
// update key, falling back to the object name.
|
||||
func updateKeyName(ctx context.Context, c client.Client, zone *bindv1alpha1.BindZone) string {
|
||||
ref := zone.Spec.UpdateKeyRef
|
||||
return tsigKeyName(ctx, c, zone.Namespace, zone.Spec.UpdateKeyRef)
|
||||
}
|
||||
|
||||
// tsigKeyName resolves a BindTSIGKey object reference to the TSIG key name used
|
||||
// in named.conf (the KeyName override when set, otherwise the object name).
|
||||
// Returns "" for an empty ref, and falls back to the ref if the object cannot be
|
||||
// read.
|
||||
func tsigKeyName(ctx context.Context, c client.Client, namespace, ref string) string {
|
||||
if ref == "" {
|
||||
return ""
|
||||
}
|
||||
var key bindv1alpha1.BindTSIGKey
|
||||
if err := c.Get(ctx, client.ObjectKey{Namespace: zone.Namespace, Name: ref}, &key); err != nil {
|
||||
if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: ref}, &key); err != nil {
|
||||
return ref
|
||||
}
|
||||
if key.Spec.KeyName != "" {
|
||||
@@ -86,3 +94,23 @@ func terminateInline(entries []string) string {
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// alsoNotifyList renders also-notify entries, each optionally annotated with a
|
||||
// TSIG key so the primary signs its NOTIFYs and secondaries can accept them by
|
||||
// key (`allow-notify { key ... }`) rather than by pod IP. An entry that already
|
||||
// carries a `key` clause is left untouched.
|
||||
func alsoNotifyList(addrs []string, key string) string {
|
||||
key = strings.TrimSpace(key)
|
||||
var parts []string
|
||||
for _, a := range addrs {
|
||||
a = strings.TrimSpace(strings.TrimRight(a, ";"))
|
||||
if a == "" {
|
||||
continue
|
||||
}
|
||||
if key != "" && !strings.Contains(a, " key ") {
|
||||
a = fmt.Sprintf("%s key \"%s\"", a, key)
|
||||
}
|
||||
parts = append(parts, a+";")
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user