cdnsd

August 5, 2026 ยท View on GitHub

Resolver for Cardano-based second-level domains on Handshake top-level domains

Features

  • Blockchain-based DNS indexer and resolver:
    • Supports Cardano and Handshake network integration
    • Dynamic top-level domain (TLD) discovery and management
    • ICANN root hints for non-blockchain domain resolution
  • Multi-protocol DNS service:
    • Standard DNS over UDP and TCP
    • DNS over TLS (when enabled)
  • DNSSEC-aware recursive resolution:
    • Validates signed answers and authenticated denial of existence
    • Supports IANA and blockchain-authenticated roots
  • Real-time monitoring:
    • Prometheus metrics endpoint
    • Optional request-level query logging
  • Pluggable configuration:
    • YAML file and environment variable support
    • Profile-based settings for network/TLDs
  • Debugging and Observability:
    • Optional debug HTTP server (for pprof, etc.)
  • Persistence:
    • Local disk database to store sync state and discovered blockchain data

Configuration

cdnsd supports configuration via YAML config files, and all settings may be overridden with environment variables.

Production deployment

This repository currently ships only pre-production Cardano profiles. The built-in defaults (ada-preprod and auto-preprod) are for testing and must not be used as a production configuration. There are no mainnet profiles in this release; configure and independently verify the network, policy, script address, and sync intercept before serving any real traffic.

Before exposing the resolver, operators should:

  • Set indexer.verify: true (the default) and keep it enabled. Disabling it is suitable only for controlled development experiments.
  • Bind DNS to the intended interface explicitly. Set dns.recursionEnabled: false unless this instance is protected by network ACLs and is deliberately operated as a recursive resolver.
  • Keep metrics and debug/pprof on loopback or a protected management network. An empty metrics address is treated as loopback by the daemon; an explicit wildcard address exposes it broadly.
  • Put state.dir on persistent, private storage and include it in backups. The state contains the indexed data and DNSSEC root-anchor rollover state; do not share it between active instances.
  • Monitor logs, /healthz, /readyz, and Prometheus metrics. Readiness means the indexer and DNS listeners have started; it does not attest that the configured chain or upstream DNS sources are healthy.

The resolver has not yet implemented every operational property required for an internet-facing authoritative or recursive service, including complete chain reorganization handling and name expiration handling. Review the current issue tracker and test the selected deployment topology before using it for production traffic.

Top-level Config Options (YAML)

OptionTypeEnvironment VariableDescription
logging.debugboolLOGGING_DEBUGEnable debug logging (default: false)
logging.queryLogboolLOGGING_QUERY_LOGEnable DNS query logging (default: true)
metrics.addressstringMETRICS_LISTEN_ADDRESSIP/interface for Prometheus metrics listener
metrics.portuintMETRICS_LISTEN_PORTTCP port for Prometheus metrics
dns.addressstringDNS_LISTEN_ADDRESSDNS listener IP/interface (empty = all)
dns.portuintDNS_LISTEN_PORTDNS UDP/TCP port (default: 8053)
dns.tlsPortuintDNS_LISTEN_TLS_PORTDNS-over-TLS port (default: 8853)
dns.recursionEnabledboolDNS_RECURSIONAllow recursive DNS lookups
dns.rootHintsstringDNS_ROOT_HINTSDNS root hints (PEM string)
dns.rootHintsFilestringDNS_ROOT_HINTS_FILEFile path to DNS root hints
dns.dnssec.enabledboolDNSSEC_ENABLEDValidate recursive DNSSEC chains (default: false)
dns.dnssec.trustAnchorsstringDNSSEC_TRUST_ANCHORSDS or DNSKEY trust anchors in zone-file format
dns.dnssec.trustAnchorsFilestringDNSSEC_TRUST_ANCHORS_FILEFile containing DS or DNSKEY trust anchors
dns.dnssec.rootAnchorRefreshIntervaldurationDNSSEC_ROOT_ANCHOR_REFRESH_INTERVALRFC 5011 root DNSKEY refresh interval (default: 24h)
dns.dnssec.rootAnchorHoldDowndurationDNSSEC_ROOT_ANCHOR_HOLD_DOWNRFC 5011 add/remove hold-down (default: 720h)
debug.addressstringDEBUG_ADDRESSAddress for debug HTTP server (default: localhost)
debug.portuintDEBUG_PORTPort for debug HTTP server
indexer.networkstringINDEXER_NETWORKCardano network name (e.g. preprod, mainnet)
indexer.networkMagicuint32INDEXER_NETWORK_MAGICCardano network magic value
indexer.addressstringINDEXER_TCP_ADDRESSCardano node TCP address
indexer.socketPathstringINDEXER_SOCKET_PATHPath to Cardano node IPC socket
indexer.interceptHashstringINDEXER_INTERCEPT_HASHInitial sync block hash for chain
indexer.interceptSlotuint64INDEXER_INTERCEPT_SLOTInitial sync slot number
indexer.verifyboolINDEXER_VERIFYEnable indexer verification mode
indexer.handshakeAddressstringINDEXER_HANDSHAKE_ADDRESSHandshake peer address to connect
state.dirstringSTATE_DIRDirectory for persistent state (BadgerDB)
tls.certFilePathstringTLS_CERT_FILE_PATHPath to TLS certificate for DNS over TLS
tls.keyFilePathstringTLS_KEY_FILE_PATHPath to TLS key for DNS over TLS
profiles[]stringPROFILESList of enabled network profiles

Example YAML Snippet

logging:
  debug: true
  queryLog: true
metrics:
  address: "127.0.0.1"
  port: 9000
dns:
  address: "0.0.0.0"
  port: 8053
  tlsPort: 8853
  recursionEnabled: false
  rootHintsFile: "/etc/cdnsd/named.root"
  dnssec:
    enabled: true
    # Optional: replaces the bundled IANA root anchors.
    trustAnchorsFile: "/etc/cdnsd/root.keys"
debug:
  address: "127.0.0.1"
  port: 6060
indexer:
  network: "preprod"
  networkMagic: 1
  address: "preprod-node.local:3001"
  socketPath: ""
  interceptHash: ""
  interceptSlot: 0
  verify: true
  handshakeAddress: ""
state:
  dir: "/var/lib/cdnsd"
tls:
  certFilePath: "/etc/cdnsd/cert.pem"
  keyFilePath: "/etc/cdnsd/key.pem"
profiles:
  - "cardano-preprod-testing"

DNSSEC and multiple roots

DNSSEC validation is opt-in. When enabled, cdnsd requests DNSSEC records from authoritative servers, validates each signed delegation, returns the Authenticated Data (AD) bit for secure answers, and returns SERVFAIL for bogus data. Securely proven unsigned delegations continue to resolve without the AD bit.

The bundled trust-anchor set contains the IANA KSK-2017 and KSK-2024 anchors. Supplying trustAnchors or trustAnchorsFile replaces that set. Each non-comment line must be a complete DS or DNSKEY record, and anchors for multiple zones may be listed together.

When DNSSEC is enabled, the root anchor set is refreshed from an authenticated root DNSKEY response. New SEP keys remain add_pending until the configured RFC 5011 hold-down has elapsed; missing keys enter remove_pending, and a revoked key is removed immediately. The state is stored in state.dir and is loaded before the first refresh, so a restart during hold-down does not reset the timer. Failed, unsigned, or otherwise unauthenticated responses are discarded and cannot replace the last known-good set. Refresh transitions are logged and exposed through dnssec_root_anchor_transition_total and dnssec_root_anchor_active.

The bundled anchors are the bootstrap. Operators should monitor the transition metric and logs during a rollover. For manual recovery, stop cdnsd, preserve a backup, remove the dnssec_root_anchor_state entry from the Badger state directory, and restart with a verified trustAnchorsFile.

Cardano and Handshake records do not need to descend from the ICANN root. When an on-chain delegation contains a DS record, cdnsd uses that record as the trust anchor for the delegated zone. This creates a separate, blockchain-authenticated DNSSEC island for each applicable root.

Profiles

Profiles predefine settings for specific TLDs and Cardano/Handshake networks. Enable profiles via the profiles YAML array or PROFILES environment variable.

Example profiles:

  • cardano-preprod-testing
  • ada-preprod
  • hydra-preprod

See the profile config file for the full list and details.

Running

Start cdnsd with:

cdnsd -config /etc/cdnsd/config.yaml

Or, override settings with environment variables, e.g.:

export DNS_LISTEN_PORT=5353
cdnsd

For the container image, mount a persistent volume at /var/lib/cdnsd. The image sets STATE_DIR to that path and runs as the unprivileged nonroot user. A typical deployment should also publish only the DNS ports needed by clients and keep the management listener private:

docker run --rm \
  -v cdnsd-state:/var/lib/cdnsd \
  -p 8053:8053/udp -p 8053:8053/tcp \
  ghcr.io/blinklabs-io/cdnsd:TAG

Metrics & Observability

  • Prometheus: Exposed at /metrics (port per config; empty address defaults to 127.0.0.1)
  • Health: /healthz reports that the HTTP process is responding; /readyz returns 503 until the indexer and DNS listeners have started
  • Debug HTTP/pprof: If the debug port is set, accessible for diagnostics; protect this endpoint because pprof can expose sensitive runtime details

These HTTP endpoints are unauthenticated. Use loopback, firewall rules, or a separate authenticated reverse proxy for all management traffic. Do not bind metrics or debug to 0.0.0.0 on an untrusted network.