Skip to content

shadowdns.yaml

shadowdns.yaml is ShadowDNS's own unified configuration file (specified with --config): a single YAML document containing three optional top-level sections, aliases (backup domain → root mapping), ephemeral_api (an HTTP API for short-lived TXT records), and doh (DNS-over-HTTPS, RFC 8484). Any other top-level key is rejected at startup (strict decoding).

# shadowdns.yaml

aliases:
  example.com:
    members:
      - backup.example.com
      - mirror.example.com
  example.org:
    members:
      - backup.example.org
    rewrite_rdata_labels: true
    collapse_cname_chain: true

ephemeral_api:
  listen: "127.0.0.1:8053"
  allow:
    - "127.0.0.1"
    - "10.0.0.0/8"
  # token: "optional-bearer-token"

doh:
  listen: "203.0.113.10:443"
  acme:
    directory_url: "https://acme-v02.api.letsencrypt.org/directory"
    ip: "203.0.113.10"
    http01_listen: "203.0.113.10:80"
    account_key_file: "/var/lib/shadowdns/acme/account.key"

aliases fields

Each key under aliases is a root domain; the value is an object:

Field Required Description
members Yes (must be non-empty) List of backup domains served by rewriting queries to this root
rewrite_rdata_labels No (default false) When true, RDATA name fields (CNAME/SRV targets, NS, MX, PTR, SOA names) get a label-anywhere rewrite — every root-label sequence inside the value is replaced with the backup origin, not just the in-bailiwick suffix. For zones using templated CDN-style targets that embed the root origin as a middle label
collapse_cname_chain No (default false) When true, in-zone CNAME chains are collapsed in responses for this root and all of its members — see CNAME Chain Collapsing

rewrite_rdata_labels: false vs true in practice

CDN-fronted zones often return a templated CNAME whose target embeds the zone's own origin as a middle label before the CDN provider's suffix. Suppose example.com is the root and example.net is one of its backup members, and the root's authoritative record for assets.example.com is:

assets.example.com.  300  IN  CNAME  assets.example.com.c.cdn.example.org.

A query then arrives for the backup name — assets.example.net A. ShadowDNS rewrites the owner name back into the backup namespace either way; the two settings differ only in how the CNAME target is rewritten:

rewrite_rdata_labels CNAME target returned for assets.example.net Correct?
false (default) assets.example.com.c.cdn.example.org. ✗ — the target ends in .example.org, so the root origin example.com sits as a middle label, not an in-bailiwick suffix. The conservative rule leaves it untouched, and the backup leaks the root's name.
true assets.example.net.c.cdn.example.org. ✓ — the label-anywhere rule replaces the embedded example.com with the backup origin example.net, exactly what a natively-served example.net zone would return.

Set rewrite_rdata_labels: true whenever a root's records point at templated CDN targets that carry the root origin as an embedded label. Leave it false (the default) for ordinary zones whose RDATA names are either in-bailiwick or genuinely external, where a label that coincidentally equals the root origin should not be rewritten.

aliases rules

  • A given backup domain may appear at most once across all roots (after normalization).
  • A backup domain must not equal its root (self-aliases are rejected).
  • Domains not listed here are treated as independent root zones, fully loaded into memory.
  • A backup zone may optionally provide its own zone file containing TXT, MX, and SRV override records. A, AAAA, CNAME, NS, and SOA records in a backup zone file are discarded with a WARN — these types are always inherited from the root.

For the query-handling details of zone aliasing, see Zone Aliasing Internals.

ephemeral_api fields

Field Required Description
listen Yes The host:port the API server binds to
allow Yes (must be non-empty) List of source IPs or CIDRs allowed to access the API; an empty list is rejected
token No Pre-shared bearer token. When set, every request must carry Authorization: Bearer <token>; when omitted, token verification is skipped (the IP ACL still applies)

When the ephemeral_api section is absent, no HTTP API server is started. For endpoint details, request/response schemas, and curl examples, see Ephemeral TXT API.

doh fields

All fields are required; loading fails naming the first missing field.

Field Required Description
listen Yes The host:port the DoH HTTPS service binds to, e.g. 203.0.113.10:443
acme.directory_url Yes ACME directory URL of the issuing CA (must be an absolute https:// URL)
acme.ip Yes The IP address the certificate is issued for (RFC 8738 IP-identifier certificate)
acme.http01_listen Yes The host:port the ACME HTTP-01 challenge responder binds to; MUST be reachable from the public Internet as port 80
acme.account_key_file Yes Absolute path to the persisted ACME account private key (PKCS#8 PEM, mode 0600). Generated on first use when absent and reused across restarts so re-registration is idempotent and does not exhaust the ACME new-account rate limit. Use a path under the systemd StateDirectory (/var/lib/shadowdns). This file is a secret — keep it 0600 and owned by the service user. Changing it requires a restart to take effect

The ACME account is registered without a contact email, so doh.acme accepts no email field; including one fails the load as an unknown field. (Contact email is optional under RFC 8555, and the short-lived auto-renewed certificate makes expiry notifications moot.)

When the doh section is absent, no DoH server, ACME client, or HTTP-01 listener is started.

DoH reuses the authoritative query path and is non-recursive: it only answers zones ShadowDNS hosts, and out-of-zone queries get REFUSED. It is not a general recursive DoH resolver.

The TLS certificate is obtained for the IP via ACME HTTP-01 using the Let's Encrypt short-lived profile (~6 days) and is auto-renewed and hot-swapped without a restart.

For deployment walkthrough and operational details, see DNS-over-HTTPS.

SIGHUP hot reload

SIGHUP re-reads shadowdns.yaml and atomically replaces the in-memory alias map:

  • If validation of either section fails, the running server keeps its previous state and ephemeral records are unaffected.
  • On a successful reload, the ephemeral record store is cleared.
  • The doh section is re-validated on reload (validation errors keep the running server). However, changes to doh.listen or any doh.acme.* field are not applied live — they require a process restart and are logged as an advisory. Certificate rotation is independent and automatic.
  • Every reload attempt is observable via Prometheus:
    • shadowdns_reload_total{result="success"|"failure"} counts reload outcomes
    • shadowdns_config_last_reload_success_timestamp_seconds records the Unix time of the last successful configuration load (initialized at startup); use time() - <gauge> for configuration-staleness alerting

Breaking change as of v0.x

The legacy --aliases CLI flag and the aliases.yaml file have been removed. Migration is mechanical: move the entries from the old aliases.yaml (root → [backups] format) under the aliases: section of the new shadowdns.yaml.