Skip to content

Configuration reference

The node reads a YAML file, by default /etc/opdns/node.yaml (optional unless you pass --config or set OPDNS_CONFIG), then applies environment variables. From the source:

Package config is the self-hosted node’s configuration: a YAML file (default /etc/opdns/node.yaml) with OPDNS_* environment overrides.

Every key can be overridden by the environment variable OPDNS_ followed by its upper-cased path joined with underscores: unbound.spawn is OPDNS_UNBOUND_SPAWN, data_dir is OPDNS_DATA_DIR, listen.udp is OPDNS_LISTEN_UDP (lists are comma separated; an empty value clears one). Precedence: flag > environment > file > default. Unknown file keys are errors.

The file carries a version (1 today; a file without one is version 1). An older file is migrated when it is loaded; a file from a newer release is refused, so a downgrade cannot misread it.

opdns-node config check validates a file without starting anything (unknown keys are errors) and prints the effective configuration, with secrets redacted and the source of every value as a comment: # default, # file, # env OPDNS_… or # flag --…. It exits with status 2 on an invalid file. opdns-node run --print-config prints the same for what run would use, and opdns-node config reference prints the tables below from the binary itself.

Terminal window
opdns-node config check /etc/opdns/node.yaml
opdns-node run --print-config

Command-line flags that override the file: --config FILE, --data-dir DIR, --log-level LEVEL, and for run only --profile-file FILE, --lists-url URL and --enrol-token CODE.

SIGHUP (systemctl reload opdns-node, or docker kill -s HUP opdns-node) reads the configuration again and applies these keys at once, without dropping queries: dns.allow_from, web.allow_from, web.hosts, web.password_hash, store.retention_days, local.hosts and unbound.ipv6 (the host’s IPv6 route is checked again too; a changed result restarts Unbound). Any other changed key keeps its running value and is logged as needing a restart. An invalid file changes nothing: the node logs the error and keeps its configuration. The same signal also pulls the profile and lists again and reloads the TLS certificate. A profile_file needs no signal: the node reloads it when it changes (Standalone mode).

With mode empty (the default) the node is enrolled when its data directory holds a node token and standalone when the configuration has a profile. With neither, and no enrol.token, run prints how to enrol and exits with status 2; it never serves unfiltered DNS for lack of setup. An invalid configuration also exits with status 2.

The node installs a list artifact only if it verifies with a trusted Ed25519 key: one of lists.public_keys, or, while lists.trust_cloud_key is on (the default), the key the list source publishes. An enrolled node needs neither setting.

Key Environment variable Type Default Description
version (file only) integer 1 The config file format version (CurrentVersion). A file without it is version 1; an older file is migrated on load, a newer one is refused.
mode OPDNS_MODE string “” (automatic), “enrolled” or “standalone”.
data_dir OPDNS_DATA_DIR string "/var/lib/opdns" Holds the identity, last good state, lists and the log database.
log_level OPDNS_LOG_LEVEL string "info" debug, info, warn, error.
log_format OPDNS_LOG_FORMAT string "json" json or text.
log_pii OPDNS_LOG_PII bool false Turns operational log redaction off: query names, client addresses, emails and tokens appear in the node’s own logs. Debugging sessions only.
name OPDNS_NAME string the machine’s host name Labels this node in its own records (logrec node field).
profile_file OPDNS_PROFILE_FILE string Loads the profile document from a JSON or YAML file instead of the cloud (tests, and standalone mode).
profile (file only) mapping The inline profile document for standalone mode, in the profile JSON schema written as YAML.
Key Environment variable Type Default Description
listen.udp OPDNS_LISTEN_UDP list of strings ["0.0.0.0:53", "[::]:53"] Do53 UDP listen addresses.
listen.tcp OPDNS_LISTEN_TCP list of strings ["0.0.0.0:53", "[::]:53"] Do53 TCP listen addresses.
listen.dot OPDNS_LISTEN_DOT list of strings DNS-over-TLS listen addresses (need tls_cert and tls_key).
listen.doh OPDNS_LISTEN_DOH list of strings DNS-over-HTTPS (HTTP/2) listen addresses.
listen.doq OPDNS_LISTEN_DOQ list of strings DNS-over-QUIC listen addresses.
listen.doh3 OPDNS_LISTEN_DOH3 list of strings DNS-over-HTTPS (HTTP/3) listen addresses.
listen.tls_cert OPDNS_LISTEN_TLS_CERT string The PEM certificate of the encrypted listeners, reloaded when the file changes.
listen.tls_key OPDNS_LISTEN_TLS_KEY string The PEM private key of TLSCert.
listen.dns_domain OPDNS_LISTEN_DNS_DOMAIN string "dns.opdns.net" The name clients use for DoT SNI and DoH (<profile>.<domain>).
listen.rate_do53 OPDNS_LISTEN_RATE_DO53 number 0 Per client queries/s on Do53 (0 disables limiting).
listen.burst_do53 OPDNS_LISTEN_BURST_DO53 number 0 The per-client burst of RateDo53.
listen.block_ttl OPDNS_LISTEN_BLOCK_TTL integer 300 The TTL of blocked answers, in seconds.
Key Environment variable Type Default Description
page.addr OPDNS_PAGE_ADDR string "0.0.0.0:8053" Serves /, /api/status, /healthz and, with metrics, /metrics (empty disables the page).
page.metrics OPDNS_PAGE_METRICS bool false Serves Prometheus metrics at /metrics on the page address: the edge metric set plus the node’s (link state, log backlog, SQLite size, writer queue depth, dropped records, shed queries). Off by default.
Key Environment variable Type Default Description
web.allow_from OPDNS_WEB_ALLOW_FROM list of strings CIDRs (or addresses) allowed to open the page and the API besides the defaults: RFC 1918, CGNAT 100.64/10, ULA, link-local and loopback. Every other source gets 403.
web.hosts OPDNS_WEB_HOSTS list of strings Names accepted in the Host header besides IP literals, localhost and the node’s name (with and without .local). Any other Host gets 421, which defeats DNS rebinding.
web.api_token OPDNS_WEB_API_TOKEN string The bearer token of the standalone local REST API. When empty, one is generated on first run, kept in <data_dir>/secrets/api_token and shown once on the local page.
web.password_hash OPDNS_WEB_PASSWORD_HASH string PasswordHash, when set, protects the page, /api/status and /metrics with a password (HTTP Basic, any user name): an argon2id hash from opdns-node hash-password. /healthz and the local REST API (bearer token) are not affected.
Key Environment variable Type Default Description
cloud.api_url OPDNS_CLOUD_API_URL string "https://api.opdns.io" The cloud API base (profile pulls).
cloud.link_url OPDNS_CLOUD_LINK_URL string "wss://link.opdns.net/link" The WebSocket link endpoint.
cloud.ca_file OPDNS_CLOUD_CA_FILE string Adds PEM roots to the system pool (dev CA).
cloud.profile_poll OPDNS_CLOUD_PROFILE_POLL duration (e.g. 30s, 15m, 6h) 15m The safety-net profile pull interval.
cloud.proxy OPDNS_CLOUD_PROXY string Overrides HTTPS_PROXY for the link and fetches.
cloud.spki_pins OPDNS_CLOUD_SPKI_PINS list of strings Optionally pins the TLS keys of the cloud API and link hosts: base64 SHA-256 hashes of a SubjectPublicKeyInfo (“sha256/<base64>” or bare base64), matched against every certificate of the verified chain. Off by default, since a pin breaks behind TLS-inspecting proxies and on CA changes; list integrity never depends on it (artifacts are signed).
cloud.profile_public_keys OPDNS_CLOUD_PROFILE_PUBLIC_KEYS list of strings Pins the hex Ed25519 keys the cloud signs profile responses with (threat model G-1). Enrolled nodes also trust the keys the cloud announces at enrolment and in verified responses (kept in profile_keys.json); with any key trusted, an unsigned or badly signed profile is refused and the last good one kept. Empty: learnt keys only.
Key Environment variable Type Default Description
link.max_inflight_queries OPDNS_LINK_MAX_INFLIGHT_QUERIES integer 4 How many relayed dashboard queries (tail, search and the analytics shapes) run against the log store at once. A query that finds every slot busy waits briefly, then is shed: it is answered with no rows, partial and partial_reason “overload”, so the dashboard shows a partial result instead of an error. Lower it on small hardware; default 4 (the cloud’s own per-node limit).
link.token_rotation OPDNS_LINK_TOKEN_ROTATION duration (e.g. 30s, 15m, 6h) 2160h How often the node rotates its node token over the link: the cloud issues a new token, the node stores it in node.json before switching to it, and the previous token keeps working for 10 minutes. 0 turns automatic rotation off (opdns-node rotate-token still works). Default 90 days.
Key Environment variable Type Default Description
enrol.token OPDNS_ENROL_TOKEN string A one-time enrolment code from the dashboard (or a node token), exchanged once on first start; ignored once enrolled.
enrol.node_id OPDNS_ENROL_NODE_ID string Enrols a node token offline, without asking the cloud.
Key Environment variable Type Default Description
lists.public_keys OPDNS_LISTS_PUBLIC_KEYS list of strings Hex Ed25519 keys; an artifact must verify with one.
lists.trust_cloud_key OPDNS_LISTS_TRUST_CLOUD_KEY bool true Also trusts the key the list source publishes: the cloud’s lists_pubkey on every profile pull (enrolled), or the pubkey of the latest.json next to (or at) lists.url (standalone). Learnt keys are kept in the data dir for offline boots. Turn it off to pin lists.public_keys only.
lists.url OPDNS_LISTS_URL string The initial (enrolled) or only (standalone) artifact URL. In standalone mode it may also name a latest.json pointer, which is followed to the newest artifact on every poll.
lists.sha256 OPDNS_LISTS_SHA256 string Optionally pins URL’s content (hex).
lists.poll OPDNS_LISTS_POLL duration (e.g. 30s, 15m, 6h) 6h Re-fetches URL in standalone mode.
Key Environment variable Type Default Description
unbound.spawn OPDNS_UNBOUND_SPAWN bool true Runs Unbound as a supervised child process.
unbound.binary OPDNS_UNBOUND_BINARY string "unbound" The unbound executable (looked up on PATH).
unbound.port OPDNS_UNBOUND_PORT integer 5353 Unbound’s loopback port.
unbound.threads OPDNS_UNBOUND_THREADS integer 0 (0: one per core, at most 4).
unbound.cache_mb OPDNS_UNBOUND_CACHE_MB integer 64 The total message and RRset cache (at least 16).
unbound.dnssec OPDNS_UNBOUND_DNSSEC bool true Off disables validation entirely (offline dev only).
unbound.ipv6 OPDNS_UNBOUND_IPV6 string "auto" How the spawned Unbound uses IPv6 for its upstream queries: “auto” probes the host at start and on SIGHUP (no IPv6 route: do-ip6 no; a route but no public IPv6 source address: prefer-ip4; else both families), “on” always uses both, “prefer-ip4” prefers IPv4, “off” never uses IPv6. Without IPv6 egress a cold lookup otherwise waits on IPv6 timeouts first and fails more often.
unbound.stubs OPDNS_UNBOUND_STUBS list of strings Zone=addr[@port][;addr] stub zones (offline dev).
unbound.upstream OPDNS_UNBOUND_UPSTREAM string Used instead of a spawned Unbound when Spawn is false.
Key Environment variable Type Default Description
clock.ntp_servers OPDNS_CLOCK_NTP_SERVERS list of strings ["162.159.200.1:123", "162.159.200.123:123"] Host:port IP literals queried while the clock is in doubt (default: time.cloudflare.com’s anycast addresses).
clock.ntp_name OPDNS_CLOCK_NTP_NAME string "time.cloudflare.com" Additionally resolved through the local Unbound, which runs without validation while the gate is closed.
clock.max_skew OPDNS_CLOCK_MAX_SKEW duration (e.g. 30s, 15m, 6h) 1h Beyond which the clock is implausible against a reference.
clock.check_interval OPDNS_CLOCK_CHECK_INTERVAL duration (e.g. 30s, 15m, 6h) 30s Between gate evaluations.
Key Environment variable Type Default Description
store.retention_days OPDNS_STORE_RETENTION_DAYS integer 30 Applies when the profile sets none.
store.max_size_mb OPDNS_STORE_MAX_SIZE_MB integer 0 Caps the database; 0 means no cap.
store.min_free_mb OPDNS_STORE_MIN_FREE_MB integer 500 The low-disk watermark: below this much free space on the data directory’s file system, query logging pauses (DNS keeps answering, the local page says so) until space is back. 0 disables. Reloaded on SIGHUP.
Key Environment variable Type Default Description
dns.stub_zones (file only) list of {name, addrs, insecure} Forward zones to fixed servers (file only; the unbound.stubs string form also accepts a trailing “!” for insecure).
dns.allow_from OPDNS_DNS_ALLOW_FROM list of strings CIDRs (or addresses) allowed to query the node besides the defaults (RFC 1918, CGNAT 100.64/10, ULA, link-local, loopback): the open-resolver guard. Other sources get REFUSED on Do53 (UDP and TCP); their DoT, DoH and DoQ connections are closed after the handshake. 0.0.0.0/0 or ::/0 turns the guard off, which the local page warns about.
dns.public_transports OPDNS_DNS_PUBLIC_TRANSPORTS bool false Lets any source use the encrypted listeners (DoT, DoH, DoQ, DoH3); Do53 stays guarded.
dns.root_zone OPDNS_DNS_ROOT_ZONE bool true Keeps a local copy of the root zone in the spawned Unbound (RFC 8806): its iterator answers root referrals from the copy instead of asking a root server, so a cold lookup starts at the TLD servers. The copy is ZONEMD-verified (and DNSSEC-validated when validation is on), lives in <data_dir>/unbound/root.zone and is refreshed from dns.root_zone_sources. When it is missing, invalid or expired Unbound queries the root servers as usual, so turning it off or losing it never stops resolution; the local page warns once the copy is two days old. Ignored with unbound.spawn false.
dns.root_zone_sources OPDNS_DNS_ROOT_ZONE_SOURCES list of strings ["170.247.170.2", "2801:1b8:10::b", "192.33.4.12", "2001:500:2::c", "199.7.91.13", "2001:500:2d::d", "192.5.5.241", "2001:500:2f::f", "192.112.36.4", "2001:500:12::d0d", "193.0.14.129", "2001:7fd::1", "192.0.32.132", "2620:0:2d0:202::132", "192.0.47.132", "2620:0:2830:202::132", "https://www.internic.net/domain/root.zone"] Where the root zone copy comes from: IP addresses (optionally addr@port) that serve it by AXFR, and http(s) URLs of the zone file, which Unbound prefers for downloads (probing the addresses’ SOA for changes). The default is RFC 8806’s list (b, c, d, f, g and k root, xfr.lax and xfr.cjr.dns.icann.org, IPv4 and IPv6) plus ICANN’s HTTPS copy; a node that cannot reach them (a firewall allowing only DNS to its upstream) can name a local server that serves the zone by AXFR.
Key Environment variable Type Default Description
local.domains OPDNS_LOCAL_DOMAINS list of strings ["home.arpa"] The local domains the node answers itself, never asking the internet: hosts below them from local.hosts and the lease files, NXDOMAIN for the others. The first names PTR answers. Default home.arpa (RFC 8375); a name outside home.arpa, .lan, .home, .internal and the other local suffixes may be caught by rebinding protection.
local.hosts OPDNS_LOCAL_HOSTS list of strings Static names, “name=address[,address]”: a label (answered under every local domain) or a name under one of them. They win over lease host names. Reloaded on SIGHUP.
local.lease_files OPDNS_LOCAL_LEASE_FILES list of strings DHCP lease files (dnsmasq, ISC dhcpd, Kea CSV; the format is detected) whose host names are answered under the local domains, with PTR records, and name the clients in the query logs. Polled every lease_poll; the list is reloaded on SIGHUP.
local.lease_poll OPDNS_LOCAL_LEASE_POLL duration (e.g. 30s, 15m, 6h) 30s How often the lease files are checked for changes.
local.forward OPDNS_LOCAL_FORWARD string The router (address or address:port) asked for local names and private addresses the node does not know (conditional forwarding); empty answers them NXDOMAIN. Private reverse lookups never leave the LAN either way.
local.device_names OPDNS_LOCAL_DEVICE_NAMES bool true Names clients without a device id (plain DNS) in the node’s query logs after their lease or static host name.

The container image sets these on top of the built-in defaults (cmd/opdns-node/Dockerfile):

Variable Value
OPDNS_DATA_DIR /var/lib/opdns
OPDNS_UNBOUND_SPAWN true
OPDNS_UNBOUND_BINARY /usr/sbin/unbound
OPDNS_PAGE_ADDR 0.0.0.0:8053