Skip to content

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

  1. The operator generates a dnssec-policy block in named.conf from your CRD config
  2. BIND9 handles key generation, signing, and automatic key rotation
  3. Each DNSZone can specify a policy via spec.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

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.

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 Bind9Instance pods, and its name starts with bindy-.
  • Secret data keys cannot contain +, so write each + of a BIND key file name as _: Kexample.com.+013+12345.key is stored as Kexample.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 named would 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

  1. Test in staging first: Enable signing on non-critical zones before production
  2. Use NSEC3: Set nsec3: true and nsec3Iterations: 0 per RFC 9276
  3. Use ECDSAP256SHA256: Modern, compact, widely supported
  4. Back up keys: Set exportToSecret: true or use keysFrom.secretRef for user-managed keys
  5. Publish DS records promptly: Signed zones without a parent DS record are signed but not validated by resolvers
  6. Monitor for expiry: Alert on RRSIG validity windows; BIND9 auto-renews but monitor for issues
  7. 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


See Also