Forwarding, blocklist and dynamic DNS with Terraform
These resources control how DTM resolves names it is not authoritative for, plus the DNS blocklist and dynamic-update settings. See forwarders and stub zones for the concepts.
Default forwarder
Section titled “Default forwarder”dtm_default_forwarder (role: editor or admin) is a cluster-wide
singleton: where DTM sends queries that match no authoritative zone,
conditional forwarder, or stub zone.
resource "dtm_default_forwarder" "this" { upstreams = ["168.63.129.16"] enabled = true}Set enabled = false to answer non-matching queries with REFUSED instead of
forwarding them.
Conditional forwarders
Section titled “Conditional forwarders”dtm_forwarder (role: editor or admin) forwards one domain to specific
upstreams.
resource "dtm_forwarder" "ad" { domain = "corp.contoso.com." upstreams = ["10.0.0.1:53"] comment = "Active Directory DNS"}| Attribute | Required | Description |
|---|---|---|
domain |
Yes | FQDN with trailing dot. Changing it replaces the resource. |
upstreams |
Yes | Resolvers as ip:port or bare IP. |
comment |
No | Free-form note. |
enabled |
No | Defaults to true. |
Stub zones
Section titled “Stub zones”dtm_stub_zone (role: editor or admin) tracks a delegated domain’s
nameservers from a set of hints.
resource "dtm_stub_zone" "partner" { domain = "partner.local." hints = ["10.5.0.1:53"]}| Attribute | Required | Description |
|---|---|---|
domain |
Yes | FQDN with trailing dot. Changing it replaces the resource. |
hints |
Yes | Bootstrap nameserver IPs. |
comment |
No | Free-form note. |
enabled |
No | Defaults to true. |
refresh_interval_seconds |
No | How often to refresh the NS list (0 = server default). |
Encrypted upstreams and upstream health checks
Section titled “Encrypted upstreams and upstream health checks”dtm_forwarder, dtm_stub_zone and dtm_default_forwarder all take the same
six optional attributes for
encrypted upstreams (DoT/DoH)
and
health-aware upstream failover.
One transport applies to all of that resource’s upstreams.
resource "dtm_forwarder" "partner" { domain = "partner.example." upstreams = ["10.20.0.53"] transport = "dot" tls_server_name = "dns.partner.example" health_check = true}| Attribute | Description |
|---|---|
transport |
udp (plaintext, the default), tcp, dot (DNS over TLS) or doh (DNS over HTTPS). A bare upstream IP gets port 853 for dot, 443 for doh. |
tls_server_name |
For dot and doh: the name the upstream’s certificate is verified against. Upstreams are IP addresses, so this is the only name DTM checks. |
ca_pem |
For dot and doh: a PEM CA certificate (or self-signed leaf) the upstream’s certificate must chain to. A certificate, not a secret. |
tls_skip_verify |
For dot and doh: encrypt without verifying the certificate. Prefer tls_server_name or ca_pem. |
doh_path |
For doh: the endpoint path, default /dns-query. Must start with /. |
health_check |
Probe the upstreams and try healthy ones first. |
dot and doh need at least one of tls_server_name, ca_pem or
tls_skip_verify, set in the configuration or already held by DTM. Without
one, the plan or the apply fails and names the three fields.
Leave an attribute out and DTM keeps the value it holds, whether it was set by an earlier apply, the API or the UI. An update never changes it, it causes no plan diff, and the Terraform state shows what DTM holds. So you can manage the upstreams in Terraform and the transport elsewhere without either side undoing the other.
A replacement is different. Changing the domain of a dtm_forwarder or
dtm_stub_zone, or running terraform apply -replace, destroys it and
creates a new one, and the new one starts from the defaults for every
attribute the configuration leaves out: plaintext DNS (transport empty,
which DTM treats as udp), no TLS settings, health checks off. The plan shows those attributes as (known after apply),
not as a change. If a forwarder’s transport must survive a replacement, set
it in the configuration.
This also means that removing an attribute from your configuration does not
clear it. To go back to plaintext DNS, set transport = "udp" explicitly.
Destroying dtm_default_forwarder clears its upstreams and disables it, and
leaves its transport as DTM holds it.
Blocklist
Section titled “Blocklist”dtm_blocklist (role: editor or admin) is a cluster-wide singleton that
sinks matching names to NXDOMAIN (suffix match).
resource "dtm_blocklist" "this" { domains = [ "doubleclick.net", "trackers.example", ]}The computed entry_count reports the deduplicated number of entries.
TSIG keys (dynamic updates)
Section titled “TSIG keys (dynamic updates)”dtm_tsig_key (role: editor or admin) authorises RFC 2136 dynamic
updates to specific zones. Only name and algorithm force replacement:
zone_ids and the secret change in place, so clients keep working across the
change. To rotate, set a new secret_wo and bump secret_wo_version; the key
keeps its name. Do not add create_before_destroy: the API refuses a second
key with the same name, so a same-name replacement would fail.
resource "dtm_tsig_key" "dhcp" { name = "dhcp-update." algorithm = "hmac-sha256" secret_wo = var.tsig_secret # sensitive, base64; >=128 bits decoded for hmac-sha256 (192/256 for sha384/sha512) secret_wo_version = "1" zone_ids = [dtm_zone.example.id]}| Attribute | Required | Description |
|---|---|---|
name |
Yes | Key name, e.g. dhcp-update. (trailing dot optional). Used as the API key. Changing it replaces the key. |
algorithm |
Yes | hmac-sha256 (recommended), hmac-sha384, or hmac-sha512. Changing it replaces the key. |
secret_wo |
To create | Base64 shared secret, write-only (Terraform 1.11 or newer): never stored in state or plan. Minimum decoded length is per algorithm: 128 bits for hmac-sha256, 192 for hmac-sha384, 256 for hmac-sha512 (a 32-byte secret satisfies all three). Create needs it (or the deprecated secret). |
secret_wo_version |
No | Rotation trigger for secret_wo (any string). Change it together with a new secret_wo to rotate the secret in place; a secret_wo change without a version bump is ignored. |
secret |
No (deprecated) | The older form of the secret. Stored in Terraform state, so prefer secret_wo; conflicts with it. A change rotates in place. |
zone_ids |
Yes | IDs of the zones this key may update. Changes apply in place. |
When a signed dynamic update or zone transfer arrives, DTM matches the wire key
name against the stored name case-insensitively and with or without the
trailing dot. A key stored bare (the form nsupdate and dnssec-keygen use)
therefore authorises the same operations as its dotted equivalent.
Import a TSIG key by its name (not ID): terraform import dtm_tsig_key.dhcp dhcp-update.
The secret is never readable, so after an import configure secret_wo without
a secret_wo_version to adopt the key as it is, and set the version when you
next rotate.
Dynamic DNS and AXFR
Section titled “Dynamic DNS and AXFR”Both are admin singletons, with one scope difference: the AXFR record applies
live and replicates cluster-wide, while the dynamic-DNS record configures
only the node the provider’s endpoint targets, so declare one
dtm_dynamic_dns_config per provider endpoint for each node that should
accept RFC 2136 updates (see dynamic DNS).
# Enable RFC 2136 dynamic updates on the endpoint's node, requiring TSIG.resource "dtm_dynamic_dns_config" "this" { enabled = true require_tsig = true}
# Allow zone transfers (AXFR) from a CIDR, requiring TSIG, and send# RFC 1996 DNS NOTIFY to a mirroring secondary on every zone change.resource "dtm_axfr_config" "this" { enabled = true allowed_cidrs = ["10.0.0.0/8"] require_tsig = true
notify_targets = ["10.20.0.10:53"] notify_tsig_key_name = "mirror-key" notify_debounce_seconds = 5}The three notify_* attributes drive outbound DNS NOTIFY: each target gets
one NOTIFY per zone per quiet window (notify_debounce_seconds, so a bulk
import does not storm the mirror), optionally signed with a stored TSIG key
(a missing key fails closed rather than sending unsigned). Like the rest of
the record, they apply live and replicate cluster-wide.
Forwarder hardening
Section titled “Forwarder hardening”dtm_forwarder_hardening_config (role: admin, per-node singleton: it
hardens only the node the provider endpoint targets, like
dtm_dynamic_dns_config above; apply it via each node’s endpoint to cover the
cluster) constrains the default forwarder with an optional allowlist and
per-client rate cap.
resource "dtm_forwarder_hardening_config" "this" { qname_allowlist = ["microsoft.com.", "azure.net."] per_client_qps = 50 per_client_burst = 100}An empty qname_allowlist forwards everything; per_client_qps = 0 means no
per-client cap.