dns
The dns role runs CoreDNS as a compose project in dns_dir (/opt/dns). It answers the names in dns_records for the clients in dns_allow, forwards every other name to dns_upstreams, and refuses all other clients. Use it for private names, for example to resolve internal host names to private addresses for a tailnet.
What it does
Section titled “What it does”- Writes a Corefile, a records file and a compose file, and starts the project with Docker Compose. CoreDNS runs with host networking, so it sees the real client addresses that
dns_allowis checked against. - Binds UDP and TCP port 53 only on the addresses in
dns_bind, plus, withdns_bind_tailscale, the node’s Tailscale IPv4 and IPv6 addresses. - Answers
dns_recordswith the TTLdns_ttl, from a managed hosts file indns_dir. The host’s own/etc/hostsis not served. Changing a record needs no restart: CoreDNS re-reads the file within a few seconds, and the cache may keep serving the old answer: up todns_ttlseconds for a changed record, and up todns_cacheseconds (capped by the upstream TTL) for a new record whose name was forwarded before. Names that are not listed are forwarded todns_upstreamsand cached fordns_cacheseconds (0turns the cache off). - Runs the container read-only, with all capabilities dropped except
NET_BIND_SERVICE, withno-new-privileges, with CPU and memory limits (dns_cpus,dns_memory) andrestart: unless-stopped. A change of the Corefile or the compose file recreates the container.
The role never runs as an open resolver. It stops when dns_allow is empty, and it refuses wildcard bind addresses (0.0.0.0, ::) and wildcard client networks (0.0.0.0/0, ::/0).
Key variables
Section titled “Key variables”| Variable | Default | Meaning |
|---|---|---|
dns_bind | [] | Addresses CoreDNS binds. Never a wildcard. Give at least one, or set dns_bind_tailscale. |
dns_bind_tailscale | false | Also bind the addresses printed by tailscale ip -4 and tailscale ip -6. |
dns_allow | [] | Client networks (CIDR) that get answers. Required. |
dns_upstreams | [1.1.1.1, 8.8.8.8] | Resolvers that unknown names are forwarded to. |
dns_records | [] | Names answered locally: name (lower case) and addresses (IPv4 and IPv6). |
dns_ttl | 60 | TTL of the local records in seconds. |
dns_cache | 300 | Seconds forwarded answers are cached. 0 disables the cache. |
dns_log_queries | false | Log every query. |
dns_image | pinned CoreDNS tag | CoreDNS image. |
All variables: dns in the reference.
Example
Section titled “Example”A resolver for private names on a private address and on loopback, for clients on a private network and for the host itself:
- hosts: resolver become: true roles: - noixdev.infra.docker - role: noixdev.infra.dns vars: dns_bind: [127.0.0.1, 192.0.2.53] dns_allow: [127.0.0.0/8, 192.0.2.0/24] dns_records: - { name: app.internal.example.com, addresses: [192.0.2.10] } - { name: dual.internal.example.com, addresses: [192.0.2.11, "2001:db8::11"] }app.internal.example.com answers with its IPv4 address, dual.internal.example.com with both. Any other name, such as example.org, is forwarded to the upstream resolvers.
Answers
Section titled “Answers”- A known name asked for a record type it has no address for gets an empty answer (NOERROR, no records). Asking
app.internal.example.comfor AAAA, when only an IPv4 address is listed, does not reach the upstream resolvers. The public record, if there is one, is never returned. - A client outside
dns_allowgets REFUSED. - An unknown name is forwarded to
dns_upstreams.
Use it as the nameserver of a tailnet
Section titled “Use it as the nameserver of a tailnet”-
Bind the node’s Tailscale addresses with
dns_bind_tailscale: trueand allow the tailnet ranges:- hosts: resolverbecome: trueroles:- role: noixdev.infra.tailscalevars:tailscale_auth_key: "{{ vault_tailscale_auth_key }}"tailscale_accept_dns: false- noixdev.infra.docker- role: noixdev.infra.dnsvars:dns_bind_tailscale: truedns_allow: [100.64.0.0/10, "fd7a:115c:a1e0::/48"]dns_records:- { name: app.internal.example.com, addresses: [100.64.0.10] } -
In the Tailscale admin console open DNS and add the node’s Tailscale addresses as nameservers, in one of two ways:
- Split DNS for the internal domain, with Override local DNS off. If the node or its tailscaled is down, only the internal names fail.
- Global nameservers with Override local DNS on. Every client then sends all its DNS through this node, so if the node or tailscaled is down, all name resolution on those clients fails.
With Split DNS only names in the internal domain reach this server; clients resolve all other names as before. With the global nameserver every name reaches it, and names without a record are forwarded to
dns_upstreams. If the tailnet policy (ACLs or grants) is not allow-all, it must allow UDP and TCP port 53 to this node for all members. If the node’s Tailscale addresses change (for example after it is registered again), run the play again and update the nameserver entries. -
Keep
tailscale_accept_dns: falseon this node, so it never resolves through itself.
Gotchas
Section titled “Gotchas”- A bind address must exist when CoreDNS starts. Tailscale IPv6 addresses come up after boot. Until the address exists CoreDNS cannot bind (it exits completely if one address is missing), so the container restarts (
restart: unless-stopped) until it can. This is expected, not an error. Withbase_nonlocal_bind: truein thebaserole, CoreDNS can bind addresses that are not up yet, so the restart loop does not happen. dns_bind_tailscaleneeds Tailscale up. The role runstailscale ipand fails when that fails. Applytailscalefirst and make sure the node is logged in.- There is no wildcard bind, and
dns_allowdefaults to an empty list, which stops the role. Both are deliberate, so the server cannot be used as an open resolver by accident. - Every record needs a lower-case name and at least one address. The role checks this before it changes anything.
- The role needs Docker. Apply
dockerfirst.