Skip to content

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.

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.

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.

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.

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.

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.

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.

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.