Skip to content

Configuration reference

This page is the reference for the server configuration file, /etc/dtm/dtm-server.yaml on a deployed node. The deployment writes a working file for you at first boot; you only edit it for the boot-time settings below that have no API surface.

  • Strict validation. An unknown or misspelled key anywhere in the file stops startup with an error naming the offending field, so a typo fails loudly instead of being silently ignored (see troubleshooting). An empty or comments-only file is fine: the server starts with defaults.
  • Restart to apply. File edits take effect at the next service restart. Restart one node at a time: a node stops answering DNS while it restarts.
  • Environment overrides. Keys marked with a DTM_* name below can be set as environment variables, which win over the file. List values are comma-separated. A malformed value stops startup rather than being ignored. The deployment uses this convention itself to deliver secrets (the cluster encryption key, the Entra client secret) from Key Vault into a root-only environment file, so secret values never appear in the YAML.
  • The file is per node. Apply the same edit on every node unless a setting is genuinely node-specific (listen addresses, node identity).

Most day-to-day settings are not in this file. Zones, records, health checks, forwarders, the blocklist, subnet mappings, and TSIG keys are managed through the API, Terraform, and the UI, and replicate cluster-wide. Several config sections below also have a runtime endpoint that is the preferred surface; for those, the YAML is only the initial seed, and once a runtime change has been made the persisted value wins over the YAML at the next boot:

YAML section Preferred runtime surface Takes effect
forwarders PUT /api/v1/forwarders/default Live
forwarder_hardening PUT /api/v1/forwarders/hardening Live
api_rate_limit (except trusted proxies) PUT /api/v1/config/rate-limits Live
axfr PUT /api/v1/config/axfr Live
dynamic_dns (enable and TSIG gate) PUT /api/v1/config/dynamic-dns Live
cache PUT /api/v1/config/cache Live
listen.dot / listen.doh PUT /api/v1/transport-config (guide) Live
replication timing PUT /api/v1/config/replication On next restart
azure.region_discovery_mode PUT /api/v1/config/region-discovery Live
azure.tag_registration_* (except _client_id) PUT /api/v1/config/tag-registration (guide) Live (interval: on next restart)
observability.otlp PUT /api/v1/observability/otlp Live
observability.query_log_* PUT /api/v1/config/query-log Live
backup PUT /api/v1/config/backup On next restart
azure.monitor_*, azure.log_analytics_* PUT /api/v1/config/azure-monitor (admin, reads too) On next restart
entra PUT /api/v1/auth/entra/config Live
Key Default Env override What it does
listen.dns_udp ":53" DTM_LISTEN_DNS_UDP UDP DNS bind address. The deployment binds the VM’s private IP, not all interfaces.
listen.dns_tcp ":53" DTM_LISTEN_DNS_TCP TCP DNS bind address.
listen.api ":8443" DTM_LISTEN_API REST API (HTTPS) bind address.
listen.dot.enabled false none Serve DNS-over-TLS on listen.dot.listen_addr. Boot default only; the runtime toggle wins.
listen.dot.listen_addr ":853" none DoT bind address (host:port).
listen.doh.enabled false none Serve DNS-over-HTTPS (/dns-query) on a dedicated listener at listen.doh.listen_addr. Boot default only; the runtime toggle wins. Unauthenticated by spec: gate port 443 at the NSG first.
listen.doh.listen_addr ":443" none DoH bind address (host:port).
listen.tls.cert_file empty DTM_TLS_CERT_FILE TLS certificate for the API (and DoT/DoH). Deployed nodes set /var/lib/dtm/api-tls.crt with managed: true. Empty with managed off: a fresh self-signed certificate at every start.
listen.tls.key_file empty DTM_TLS_KEY_FILE Private key matching cert_file (/var/lib/dtm/api-tls.key as deployed). Owner-readable only; set both keys together. A missing file stops startup, unless managed is on and both are missing.
listen.tls.managed false DTM_TLS_MANAGED true as deployed: if both files are missing, writes a two-year self-signed pair once, and renews it in its last 30 days; serves the uploaded certificate.
listen.tls.ca_file empty DTM_TLS_CA_FILE Advanced: enables mutual TLS. Every API client (including each node’s own UI service) must then present a certificate signed by this CA.
listen.dns_listeners 1 none DNS listener socket replicas for high query rates. Worth raising toward the vCPU count on 8+ vCPU nodes.
Key Default Env override What it does
node.id the hostname DTM_NODE_ID Cluster-unique node identity.
node.region empty DTM_NODE_REGION Region label for region-aware routing.
storage.type bbolt DTM_STORAGE_TYPE Local data store type. Production deployments use the default.
storage.path /var/lib/dtm/dtm.db as deployed DTM_STORAGE_PATH Data store path (on the dedicated data disk).
storage.auto_reseed_on_corruption on DTM_STORAGE_AUTO_RESEED_ON_CORRUPTION Boot-time self-heal: a corrupt data store is quarantined and re-seeded from a confirmed-healthy peer, never wiped without one.
forwarders 168.63.129.16 DTM_FORWARDERS Default recursive upstreams. Runtime-managed via the forwarders API; treat the YAML as the seed.

The cluster section. The deployment writes this for you; the main reason to touch it is the readiness threshold.

Key Default Env override What it does
replication.enabled false (true as deployed) DTM_REPLICATION_ENABLED Enable clustering.
replication.bind_addr the node’s private IP as deployed none Cluster traffic bind address.
replication.bind_port 7946 DTM_REPLICATION_BIND_PORT The cluster port (allow it node-to-node in the NSG).
replication.seeds empty DTM_REPLICATION_SEEDS host:port of existing nodes a joining node contacts.
replication.encrypt_key required DTM_REPLICATION_ENCRYPT_KEY The cluster encryption key. The deployment delivers it from Key Vault via the environment; every node must hold the same value.
replication.push_pull_interval_seconds 15 DTM_REPLICATION_PUSH_PULL_INTERVAL_SECONDS Full-state sync cadence between nodes. Also settable via PUT /api/v1/config/replication (applied on restart).
replication.readyz_queue_threshold 5000 DTM_REPLICATION_READYZ_QUEUE_THRESHOLD Outbound update-queue depth at which /readyz names the backlog. From v1.0.4 it is reported, not a reason to turn not-ready; see known limitations.
replication.gossip_max_streams_per_source 4 DTM_REPLICATION_GOSSIP_MAX_STREAMS_PER_SOURCE Concurrent inbound cluster-port connections accepted from one source address (added in v1.0.4). A connection over the cap is closed before any of it is read, and counted in dtm_replication_gossip_stream_refused_total.
replication.gossip_max_streams 32 DTM_REPLICATION_GOSSIP_MAX_STREAMS Concurrent inbound cluster-port connections accepted in total (added in v1.0.4). Normal cluster traffic stays well under both caps; a refused connection costs one sync, retried on the next interval.

See the runtime API (/api/v1/config/dynamic-dns) for the enable and TSIG gate; the scavenger tunables are file-or-environment only.

Key Default Env override What it does
dynamic_dns.enabled false DTM_DYNAMIC_DNS_ENABLED Accept RFC 2136 dynamic updates.
dynamic_dns.require_tsig false DTM_DYNAMIC_DNS_REQUIRE_TSIG Reject unsigned updates. Turn this on wherever untrusted clients can reach port 53.
dynamic_dns.scavenge_max_age 0 (disabled) DTM_SCAVENGE_MAX_AGE Seconds after which un-refreshed dynamic records are removed.
dynamic_dns.scavenge_interval 3600 DTM_SCAVENGE_INTERVAL How often the scavenger sweeps.

The built-in backup scheduler; the workflow lives on backup and restore. These keys are the boot default: a setting saved per node with PUT /api/v1/config/backup, the Settings page, or dtm_backup_config overrides them at the next boot. The one exception is the passphrase: when the saved setting has none, backup.passphrase or DTM_BACKUP_PASSPHRASE still applies.

Key Default Env override What it does
backup.schedule_enabled false DTM_BACKUP_SCHEDULE_ENABLED Take snapshots on a schedule, per node.
backup.interval_minutes 0 = 1440 (daily) DTM_BACKUP_INTERVAL_MINUTES Snapshot cadence.
backup.retention_count 7 DTM_BACKUP_RETENTION_COUNT Local snapshots kept; oldest pruned first. Pruning matches by filename: any dtm-backup-*.dtmb in backup.dir counts, including a downloaded snapshot saved there under its default name, so rename manual copies or store them outside backup.dir.
backup.dir a backups directory beside the data store DTM_BACKUP_DIR Where local snapshots land.
backup.blob_container_url empty (local-only) DTM_BACKUP_BLOB_CONTAINER_URL Optional off-node copy via the VM’s managed identity (Storage Blob Data Contributor). A URL carrying a query string (SAS) is rejected at startup.

Outbound event notifications; contracts and event types live on notifications.

Key Default Env override What it does
notifications.enabled false DTM_NOTIFICATIONS_ENABLED Enable event delivery. At least one sink must be set, or startup fails (never silently un-covered).
notifications.webhook_url empty DTM_NOTIFICATIONS_WEBHOOK_URL HTTPS webhook sink (https:// required except loopback).
notifications.webhook_secret empty (unsigned) DTM_NOTIFICATIONS_WEBHOOK_SECRET HMAC-SHA256 signing key for the webhook; deliver via the environment. Empty sends unsigned (startup WARN).
notifications.eventgrid_topic_endpoint empty DTM_NOTIFICATIONS_EVENTGRID_TOPIC_ENDPOINT Azure Event Grid custom-topic sink, published with the managed identity (EventGrid Data Sender). A query string (key) in the URL is rejected.
notifications.event_types empty = all none Allowlist of event types to deliver.
notifications.min_interval_seconds 60 DTM_NOTIFICATIONS_MIN_INTERVAL_SECONDS Per-source flap-dedupe window; repeats inside it are suppressed (and counted).

The observability guide and tag-based VM registration cover these in context. The Azure Monitor and Logs Ingestion keys are boot defaults: a setting saved per node with PUT /api/v1/config/azure-monitor, the Settings page, or dtm_azure_monitor_config overrides them at the next boot. azure.subscription_id has no runtime surface and stays a file setting.

Key Default Env override What it does
observability.query_log_enabled false DTM_QUERY_LOG_ENABLED Per-query logging. Boot seed only; prefer PUT /api/v1/config/query-log (live, cluster-wide).
observability.query_log_sample_rate log every query none Keep 1-in-N queries on high-QPS nodes. Runtime-settable on the same endpoint.
observability.audit_max_entries 10000 none Audit log entry cap, oldest pruned first; minimum 1000. Changed in v1.0.7: the default becomes 2000000.
observability.audit_retention none (no time limit) none Audit log time cap as a duration, for example "2160h" for 90 days. Changed in v1.0.7: the default becomes 2160h (90 days); see audit log retention.
observability.otlp.* disabled none OTLP metrics export (endpoint, headers, interval_seconds, service_name, insecure); prefer PUT /api/v1/observability/otlp.
azure.subscription_id empty DTM_AZURE_SUBSCRIPTION_ID Subscription for the Azure integrations.
azure.monitor_enabled (+ monitor_resource_id, monitor_region) false none Master switch for the Azure Monitor exporter: custom metrics and query-log shipping. Leave monitor_resource_id and monitor_region blank to publish to this VM, read from the Azure Instance Metadata Service; set them only to send the metrics to a different resource.
azure.monitor_metrics_enabled true DTM_AZURE_MONITOR_METRICS_ENABLED Push custom metrics. false is the logs-only setup: no metric is pushed and no target is resolved, while the query log still ships.
azure.logs_ingestion_endpoint empty none Added in v1.0.6. The logs ingestion URI of the data collection endpoint (or Direct DCR) the query log ships to: an https:// origin on a host under .monitor.azure.com. Set with logs_ingestion_dcr_immutable_id; requires azure.monitor_enabled. See Ship the query log to Log Analytics.
azure.logs_ingestion_dcr_immutable_id empty none Added in v1.0.6. The data collection rule’s immutable ID, dcr- and 32 lower-case hex digits. The node’s managed identity needs Monitoring Metrics Publisher on that rule.
azure.logs_ingestion_stream Custom-DTMLogs none Added in v1.0.6. The stream the rule declares, Custom-<name>.
azure.log_analytics_workspace_id / _key removed none Removed in v1.0.6. A value is logged as an ERROR at start-up that names the replacement, and is otherwise ignored; nothing ships through it. See upgrading from 1.0.5.
azure.region_discovery_enabled false (true as deployed) DTM_AZURE_REGION_DISCOVERY_ENABLED Discover subnet-to-region mappings from your VNets.
azure.region_discovery_resource_group empty DTM_AZURE_REGION_DISCOVERY_RESOURCE_GROUP Scope discovery to one resource group.
azure.region_discovery_mode arm-list DTM_AZURE_REGION_DISCOVERY_MODE Discovery backend (arm-list or resource-graph); runtime-switchable.
azure.region_discovery_auto_apply false none Apply discovered mappings on the timer, as earlier releases did. Default is detect-only: the loop reports drift and an admin applies it.
azure.region_discovery_interval_seconds 3600 none Detection cadence. An explicit 0 disables the periodic detector (detect on request only); omitting the key keeps the default.
azure.tag_registration_* disabled DTM_AZURE_TAG_REGISTRATION_* Tag-based VM registration. Boot seed only, except tag_registration_client_id; prefer PUT /api/v1/config/tag-registration (cluster-wide). Keys and workflow in that guide.

Entra ID single sign-on covers setup end to end. These keys are the boot default: a configuration saved with PUT /api/v1/auth/entra/config, the Settings page, or dtm_sso_config applies live and overrides them at every later boot: on the node that served it, or on every node once the configuration is cluster-wide (v1.0.5).

Key Default Env override What it does
entra.tenant_id empty none Your directory (tenant) ID, a GUID. SSO activates when tenant, client, secret and redirect URI are all set. The meta-tenants common, organizations and consumers are not accepted: from v1.0.4 a node configured with one keeps Entra sign-in disabled and logs a warning. allow_meta_tenant has no effect.
entra.client_id empty none The app registration’s client ID.
entra.client_secret empty DTM_ENTRA_CLIENT_SECRET The client secret; deliver it via the environment, never in the YAML.
entra.redirect_uri empty none The OAuth redirect URI registered on the app.
Key Default Env override What it does
allow_script_health_checks false none Allow script health-check probes. See the caution below before enabling.
script_signing_keys empty none Public keys that may approve managed scripts: a list of {id, public_key}. Read at startup; keep it the same on every node.
script_revoked_hashes empty none Content hashes (sha256:<hex>) of managed scripts that must never run, whoever signed them. Read at startup; keep it the same on every node.
api_allowed_hosts empty (no enforcement) none Host-header allowlist for the API, a DNS-rebinding defence. The server warns at startup when unset.
api_rate_limit.* built-in defaults none Per-IP API rate limits by class (auth_rps/auth_burst, write_rps/write_burst, read_rps/read_burst, upload_rps/upload_burst); the upload class covers the disk-staging endpoints (/api/v1/restore, /api/v1/admin/compact) with a deliberately severe built-in default (1 request per 600 s). Prefer PUT /api/v1/config/rate-limits.
api_rate_limit.trusted_proxies loopback (127.0.0.1/32, ::1/128) none CIDRs allowed to set X-Forwarded-For; from a trusted peer the rate limiter keys on the address that proxy saw. Deliberately file-only. Omitted, it trusts loopback only, so the web console on the same node gives each browser its own rate-limit bucket. [] trusts nobody: every console user then shares one bucket. A list replaces the default, so keep loopback in it when you add a load balancer’s range. Changed in v1.0.4 (the default was trust nobody).
managed_identity_probe_audiences empty (built-in denylist) none Token audiences a managed_identity health probe may use. Empty: any audience except Key Vault and Managed HSM, Resource Manager, Microsoft Graph and Azure AD Graph, and Storage. Set: only the audiences listed. File-only, added in v1.0.4; see authenticated health probes.
axfr.enabled false none Allow zone transfers (AXFR).
axfr.allowed_cidrs empty (all, when enabled) none Restrict transfers to these networks. Write each entry as a CIDR with its prefix length (10.0.0.5/32, not 10.0.0.5). After you set the list, attempt a zone transfer from a host outside it, and confirm the transfer is refused.
axfr.require_tsig false none Require TSIG-signed, per-zone-authorized transfer requests.
axfr.notify_targets empty (no NOTIFY) DTM_AXFR_NOTIFY_TARGETS host:port list sent an RFC 1996 DNS NOTIFY on every zone-content change, so mirrors stop polling SOA serials.
axfr.notify_tsig_key_name empty (unsigned) DTM_AXFR_NOTIFY_TSIG_KEY_NAME Sign NOTIFYs with this stored TSIG key. Key rotation applies without a restart; a missing key fails closed rather than sending unsigned.
axfr.notify_debounce_seconds 0 = 5 DTM_AXFR_NOTIFY_DEBOUNCE_SECONDS Per-zone quiet window: a bulk import produces one NOTIFY, not hundreds. Like the rest of axfr, the NOTIFY keys are boot seeds for the runtime record (PUT /api/v1/config/axfr, live, cluster-wide).
forwarder_hardening.qname_allowlist empty (no restriction) none Suffix allowlist for names the default forwarder will resolve.
forwarder_hardening.per_client_qps / per_client_burst off none Per-source-IP cap on forwarded queries.
cors_origin empty (no CORS) DTM_CORS_ORIGIN Access-Control-Allow-Origin value for the API.
Key Default Env override What it does
cache.enabled false (true as deployed) DTM_CACHE_ENABLED Cache forwarded responses.
cache.max_size 10000 (50000 as deployed) DTM_CACHE_MAX_SIZE Maximum cached entries.
cache.ttl_seconds 0 (use record TTL) none Cap cached-answer TTLs.
health_workers 16 DTM_HEALTH_WORKERS Health-probe worker pool size; raise it if very many checks saturate probing.
cross_region_poll_interval_seconds 30 DTM_CROSS_REGION_POLL_INTERVAL_SECONDS How often cross-region failover state is re-evaluated.
readyz.sync_timeout_action block none Whether a joining node that misses its first full sync keeps waiting (block) or serves anyway (proceed).
readyz.sync_timeout_seconds 10 none How long a joining node waits for that first sync.
readyz.drain_grace_seconds 10 none Pause after readiness flips to draining, so load balancers evict the node first.
memory.self_restart_bytes 0 (disabled) none Memory ceiling at which the service restarts itself cleanly rather than risking the OOM killer.

The file can seed zones and records at first boot (zones: with name, default_ttl, soa, records), but in production zones are owned by the API, Terraform, and the UI once the cluster is running. Do not manage ongoing DNS data through the file.