DNSSEC¶
DNS Security Extensions (DNSSEC) provides cryptographic authentication of DNS data.
Implementation Status¶
| Feature | Status | Notes |
|---|---|---|
| DNSSEC Validation | ✅ Implemented | Validate upstream responses via dnssec.validation |
| CRD Schema (Phase 1) | ✅ Implemented | DNSSECConfig, DNSSECSigningConfig, DNSSECKeySource types |
| Policy Configuration (Phase 2) | ✅ Implemented | dnssec-policy blocks generated in named.conf |
| Key Source Configuration (Phase 3) | ✅ Implemented | Secret-backed and auto-generated keys |
| Zone Signing Configuration (Phase 4) | ✅ Implemented | Per-zone dnssecPolicy field, inline signing via bindcar |
| DS Record Status Reporting (Phase 5) | ✅ Implemented | DS records published in DNSZone.status.dnssec (ADR-0006) |
| Integration Tests (Phase 6) | 🚧 Partial | Unit coverage complete; end-to-end DNSSEC suite tracked with the integration harness |
DNSSEC Validation¶
When DNSSEC validation is enabled, BIND9 verifies cryptographic signatures on DNS responses from upstream nameservers. This protects against cache poisoning, man-in-the-middle tampering, and spoofed DNS responses.
Requirements: - Valid DNSSEC trust anchors (managed automatically by BIND9 via RFC 5011) - Network connectivity to root DNS servers - Accurate system time (NTP synchronization)
Configuration¶
Configure validation in the Bind9Cluster global configuration:
apiVersion: bindy.firestoned.io/v1beta1
kind: Bind9Cluster
metadata:
name: production-dns
namespace: bindy-system
spec:
global:
dnssec:
validation: true # Enable DNSSEC validation of upstream responses
validation: true renders dnssec-validation auto;: BIND validates with
its built-in root trust anchor. (yes would require a trust-anchors
block, which bindy does not render.) validation: false renders
dnssec-validation no;.
Or override per-instance in Bind9Instance:
apiVersion: bindy.firestoned.io/v1beta1
kind: Bind9Instance
metadata:
name: primary-dns
namespace: bindy-system
spec:
clusterRef: production-dns
config:
dnssec:
validation: true
Verification¶
SERVICE_IP=$(kubectl get svc -n bindy-system production-dns-primary -o jsonpath='{.spec.clusterIP}')
# Query a DNSSEC-signed domain - look for 'ad' (authentic data) flag
dig @$SERVICE_IP cloudflare.com +dnssec
# flags: qr rd ra ad; QUERY: 1, ANSWER: 1, ...
# ^^-- successful DNSSEC validation
# Query a domain with broken DNSSEC - should FAIL with SERVFAIL
dig @$SERVICE_IP dnssec-failed.org
Troubleshooting Validation¶
# Check BIND9 logs for DNSSEC errors
kubectl logs -n bindy-system -l app.kubernetes.io/component=bind9 | grep -i dnssec
# Common errors:
# "broken trust chain" - Missing or invalid DS records in parent zone
# "no valid signature found" - Expired or missing RRSIG records
# "validation failed" - Signature verification failed
# Query without validation for debugging
dig @$SERVICE_IP example.com +cd # +cd = checking disabled
# Verify NTP sync - DNSSEC signatures have validity periods
kubectl exec -n bindy-system -l app.kubernetes.io/component=bind9 -- date
DNSSEC Zone Signing¶
Bindy supports declarative DNSSEC zone signing using BIND9's modern dnssec-policy mechanism. Configuration is via Bind9Cluster (global policy) and optionally overridden per DNSZone.
How It Works¶
- The operator generates a
dnssec-policyblock innamed.conffrom your CRD config - BIND9 handles key generation, signing, and automatic key rotation
- Each
DNSZonecan specify a policy viaspec.dnssecPolicy(or inherit the cluster default)
Cluster-Level Signing Configuration¶
apiVersion: bindy.firestoned.io/v1beta1
kind: Bind9Cluster
metadata:
name: production-dns
namespace: bindy-system
spec:
global:
dnssec:
validation: true
signing:
enabled: true
policy: "bindy" # Name of the policy bindy defines; zones inherit it
algorithm: "ECDSAP256SHA256" # Recommended: ECDSA P-256
kskLifetime: "365d" # Key Signing Key lifetime
zskLifetime: "90d" # Zone Signing Key lifetime
nsec3: true # Use NSEC3 (privacy-preserving)
nsec3Iterations: 0 # Per RFC 9276 recommendation
# Key management
autoGenerate: true # BIND9 generates keys automatically
exportToSecret: true # Back up generated keys to a Secret
policy names the dnssec-policy block bindy defines from these settings.
It defaults to "bindy". BIND reserves "default", "insecure" and "none"
for its built-in policies and refuses to load a configuration that redefines
one, so bindy refuses those names. A DNSZone with no dnssecPolicy of its
own is signed with this policy.
This generates a dnssec-policy block in named.conf similar to:
dnssec-policy "bindy" {
keys {
ksk lifetime 365d algorithm ECDSAP256SHA256;
zsk lifetime 90d algorithm ECDSAP256SHA256;
};
nsec3param iterations 0 optout no salt-length 16;
signatures-refresh 5d;
signatures-validity 30d;
signatures-validity-dnskey 30d;
zone-propagation-delay 300;
parent-propagation-delay 3600;
max-zone-ttl 86400;
};
Per-Zone Policy Override¶
Each DNSZone inherits the cluster's DNSSEC signing policy by default. Override it with spec.dnssecPolicy:
apiVersion: bindy.firestoned.io/v1beta1
kind: DNSZone
metadata:
name: example-com
namespace: bindy-system
spec:
zoneName: example.com
clusterRef: production-dns
soaRecord:
primaryNs: ns1.example.com.
adminEmail: admin.example.com.
serial: 2026012801
refresh: 3600
retry: 600
expire: 604800
negativeTtl: 86400
ttl: 3600
# Override cluster default or disable signing for this zone:
# dnssecPolicy: "high-security" # Use a different named policy
# dnssecPolicy: "none" # Disable signing for this zone
When dnssecPolicy is set, inline-signing yes; is automatically added to the zone configuration.
Key Source Options¶
Option 1: Auto-Generated Keys (Recommended for Development)¶
signing:
enabled: true
policy: "bindy"
autoGenerate: true
exportToSecret: true # Back up keys to a Kubernetes Secret
BIND9 generates KSK and ZSK keys automatically, in the key directory
(/var/cache/bind/keys, which bindy sets as key-directory whenever signing
is enabled).
Auto-generated keys do not survive a pod restart yet
The key directory is an emptyDir, so a restarted pod generates new keys
and the zone is re-signed with them. exportToSecret and
keysFrom.persistentVolume are accepted by the CRD but not yet
implemented: nothing is exported, and no volume is created. Until they
are, use user-supplied keys (Option 2) for any zone whose DS record is
published at its parent.
Option 2: User-Supplied Keys (Recommended for Production)¶
signing:
enabled: true
policy: "bindy"
# kskLifetime / zskLifetime: leave unset, or "unlimited"
keysFrom:
secretRef:
name: bindy-dnssec-keys-example-com
Supply pre-generated keys via a Kubernetes Secret. This is the recommended production approach: it gives full control over key material, the keys survive pod restarts, and every primary signs with the same keys, so a zone served by several primaries publishes one DNSKEY RRset and one set of DS records (ADR-0012).
How it works: the Secret is mounted read-only into an init container,
dnssec-keys-init, which copies each key file into BIND's writable key
directory (/var/cache/bind/keys) before named starts. named never mounts
the Secret itself, and writes its .state files beside the copies.
Rules for the Secret:
- It lives in the same namespace as the
Bind9Instancepods, and its name starts withbindy-. - Secret data keys cannot contain
+, so write each+of a BIND key file name as_:Kexample.com.+013+12345.keyis stored asKexample.com._013_12345.key. The init container restores the name. Other data keys are skipped. - A Secret with no key file fails the init container, and the pod does not
start: otherwise
namedwould quietly generate keys of its own.
Generate a KSK and a ZSK and store them:
dnssec-keygen -a ECDSAP256SHA256 -f KSK example.com
dnssec-keygen -a ECDSAP256SHA256 example.com
args=()
for f in Kexample.com.+*; do
args+=(--from-file="$(printf '%s' "$f" | tr '+' '_')=$f")
done
kubectl -n bindy-system create secret generic bindy-dnssec-keys-example-com "${args[@]}"
Shared keys do not roll automatically
kskLifetime and zskLifetime must be unset or unlimited with
keysFrom.secretRef; the API server rejects anything else. A finite
lifetime would make each primary roll a successor key on its own, and
the primaries would diverge. To rotate, generate the successor, add it
to the Secret, and restart the pods
(kubectl rollout restart deployment/<instance>): the copy happens at
pod start, so editing the Secret alone changes nothing.
Algorithm Selection¶
| Algorithm | OID | Recommended Use |
|---|---|---|
ECDSAP256SHA256 |
13 | Default: modern, fast, small keys |
ECDSAP384SHA384 |
14 | Higher security margin, slightly larger |
RSASHA256 |
8 | Legacy compatibility only |
Use ECDSAP256SHA256 unless you have a specific compatibility requirement. Per RFC 8624, ECDSA algorithms are MUST implement for modern resolvers.
NSEC vs NSEC3¶
| Setting | Privacy | Notes |
|---|---|---|
nsec3: false |
❌ Zone enumerable | Simpler, lower overhead |
nsec3: true |
✅ Hashed names | Recommended; use nsec3Iterations: 0 per RFC 9276 |
Completing the Chain of Trust¶
After zones are signed, publish DS records in the parent zone to complete the DNSSEC chain of trust. The DS record links the child zone's KSK to the parent zone's trust.
The operator extracts DS records automatically: after configuring a signed zone it queries the zone's DNSKEY RRset, derives one DS record per KSK (SHA-256 digest, RFC 8624), and publishes them in the zone's status.
# All DS records for the zone, ready to paste into the parent zone
kubectl get dnszone example-com -n bindy-system \
-o jsonpath='{.status.dnssec.dsRecords[*]}'
# Example output:
# example.com. IN DS 12345 13 2 4C3A9C2E...
# Signing state at a glance (kubectl get dnszones -o wide shows a DNSSEC column)
kubectl get dnszone example-com -n bindy-system \
-o jsonpath='{.status.dnssec.signed}'
The full status carries the key tag and algorithm as well:
status:
dnssec:
signed: true
dsRecords:
- "example.com. IN DS 12345 13 2 4C3A9C2E..."
keyTag: 12345
algorithm: ECDSAP256SHA256
nextKeyRollover: "2027-09-27T00:00:00" # requires bindcar 0.8.2+
nextKeyRollover is the next KSK/CSK rollover event BIND has scheduled
(server-local clock), read from the sidecar's rndc dnssec -status parsing.
It needs the bindcar v0.8.2+ sidecar and is omitted with older sidecars.
lastKeyRollover is always null today: bindcar exposes the next scheduled
event and current key states, not rollover history.
signed: false with empty dsRecords means the zone has a DNSSEC policy but
BIND9 is still generating its keys; the status refreshes on the next
reconcile. A zone with dnssecPolicy: "none" reports no dnssec status at
all.
Publish the dsRecords output at your domain registrar or parent zone
operator. To cross-check by hand:
kubectl exec -n bindy-system -l app.kubernetes.io/component=bind9 -- \
dig @localhost -p 5353 example.com DNSKEY | dnssec-dsfromkey -2 -f - example.com
The manual output must match .status.dnssec.dsRecords.
DNSSEC Record Types Reference¶
| Record | Purpose |
|---|---|
| DNSKEY | Public signing keys (KSK and ZSK) |
| RRSIG | Cryptographic signatures for each RRset |
| NSEC | Proof of non-existence (zone-enumerable) |
| NSEC3 | Privacy-preserving proof of non-existence (hashed names) |
| DS | Delegation signer, published in the parent zone |
Best Practices¶
- Test in staging first: Enable signing on non-critical zones before production
- Use NSEC3: Set
nsec3: trueandnsec3Iterations: 0per RFC 9276 - Use ECDSAP256SHA256: Modern, compact, widely supported
- Back up keys: Set
exportToSecret: trueor usekeysFrom.secretReffor user-managed keys - Publish DS records promptly: Signed zones without a parent DS record are signed but not validated by resolvers
- Monitor for expiry: Alert on RRSIG validity windows; BIND9 auto-renews but monitor for issues
- Plan DS rollovers: KSK rollovers require coordinating DS record updates with the parent zone
Reference¶
RFCs¶
- RFC 4033 - DNS Security Introduction and Requirements
- RFC 4034 - Resource Records for DNSSEC
- RFC 4035 - Protocol Modifications for DNSSEC
- RFC 5155 - NSEC3 (Hashed Authenticated Denial)
- RFC 8624 - Algorithm Requirements and Usage
- RFC 9276 - NSEC3 Parameter Guidance
BIND9 Documentation¶
External Tools¶
- DNSViz - DNSSEC chain visualization and debugging
- Verisign DNSSEC Analyzer - Full chain validation
- DNSSEC Debugger - Interactive DNSSEC testing