COREDNS-TEMPLATE(7)
πΈ COREDNS-TEMPLATE(7) πΈ
okay bestie so this is a page from the CoreDNS Plugins manual, and it's about the template plugin β literally one of the most fun ones. grab a snack, we're doing this π
β¨ NAME β¨
template β lets you build dynamic responses based on the incoming query. yes, actually. you just write a template and boom, answers. π
π DESCRIPTION π
The template plugin lets you dynamically respond to queries by just writing a (Go) template. that's it. that's the whole vibe. you write a template, it answers. iconic behavior fr fr β¨
π¦ SYNTAX π¦
template CLASS TYPE [ZONE...] {
match REGEX...
var NAME EXPRESSION
expr EXPRESSION
answer RR
additional RR
authority RR
rcode CODE
ederror EXTENDED_ERROR_CODE [EXTRA_REASON]
fallthrough [FALLTHROUGH-ZONE...]
}
and now the breakdown, one by one, no skips π
CLASSβ the query class. usuallyINorANY. πTYPEβ the query type (A,PTR, ... and it can beANY, which matches all types).ZONEβ the zone scope(s) for this template. defaults to the server zones. πmatchREGEX... β Go regexp patterns (https://golang.org/pkg/regexp/) that get matched against the incoming question name. if you specify no regex at all, it matches everything (default:.*). first matching regex wins. and heads up β regex patterns must not exceed10000characters. πanswer|additional|authorityRRβ an RFC 1035 (https://www.rfc-editor.org/info/rfc1035/#section-5) style resource record fragment, built by a Go template (https://golang.org/pkg/text/template/) that contains the reply. if you specify no answer, you get a response with an empty answer section. that's the deal. πvarNAMEEXPRESSIONβ sets the variableNAMEto the result ofEXPRESSION, evaluated for each matching query and available to the templates as.Var.NAME. you can set multiple variables, and each one may use the variables declared before it. see the Expressions section below, iykyk πexprEXPRESSIONβ a condition that has to evaluate to true for the query to match. all conditions must be true for a complete match; otherwise the query is subject tofallthrough, as if no regex had matched. conditions may use the variables declared withvar. again, Expressions section, we got you π«ΆrcodeCODEβ a response code (NXDOMAIN,SERVFAIL, ...). the default isNOERROR. valid response code values are per theRcodeToStringmap defined by themiekg/dnspackage inmsg.go. βederrorEXTENDEDERRORCODEβ an extended DNS error code as a number defined in RFC8914 (0,1,2,...,24). andEXTRA_REASONis an additional string explaining the reason for returning the error. πfallthroughβ continue with the next template instance if the template'sZONEmatches a query name but no regex match. if there's no next template, continue resolution with the next plugin. if[FALLTHROUGH-ZONE...]are listed (for examplein-addr.arpaandip6.arpa), then only queries for those zones will be subject to fallthrough. and listen β withoutfallthrough, when the template'sZONEmatches a query but no regex match, then aSERVFAILresponse is returned. dramatic but true π
also see (#also-see) contains an additional reading list, if you're the kind of girl who reads the whole syllabus. respect. π
π§ TEMPLATES π§
every single resource record is a full-featured Go template (https://golang.org/pkg/text/template/) with the following predefined data β memorize these, they're your new besties:
.Zoneβ the matched zone string (e.g.example.). πΈ.Nameβ the query name, as a string (lowercased)..Classβ the query class (usuallyIN)..Typeβ the RR type requested (e.g.PTR)..Matchβ an array of all matches.index .Match 0refers to the whole match. π₯.Groupβ a map of the named capture groups..Messageβ the complete incoming DNS message..Questionβ the matched question section..Remoteβ client's IP address. π.Varβ the variables defined withvar(e.g..Var.myvariable)..Metaβ a function that takes a metadata name and returns the value, if themetadataplugin is enabled. for example,.Meta "kubernetes/client-namespace"π«
and the following predefined template functions (https://golang.org/pkg/text/template#hdr-Functions):
parseIntβ interprets a string in the given base and bit size. equivalent tostrconv.ParseUint(https://golang.org/pkg/strconv#ParseUint). π
the output of the template must be an RFC 1035 (https://www.rfc-editor.org/info/rfc1035/) style resource record β commonly referred to as a "zone file". periodt.
WARNING β οΈ there's a syntactical problem with Go templates and CoreDNS config files. expressions like{{$var}}will be interpreted as a reference to an environment variable by CoreDNS (and Caddy), while{{ $var }}will work. see Bugs (#bugs) andcorefile(5). no cap, put the spaces in. π
π EXPRESSIONS π
the EXPRESSION of a var or expr is written in the expr language β the same one used by the view plugin. see https://expr-lang.org/docs/language-definition as a detailed reference for valid syntax. go off, read the docs, they're giving thorough. β¨
expressions can reference the DNS query functions and utility functions listed in the view plugin's documentation, the variables declared by preceding var options, and:
group(name string) stringβ the capture group namednameof the matching regex, or""if there's no such group. πgroup(index int) stringβ theindex-th capture group of the matching regex,group(0)being the entire match, or""if there's no such group. π¦
and a variable name must be a valid identifier, and it must not be the name of an existing function, or of a keyword or literal of the expr language β like len or true. don't do it. it will not slay. π
π METRICS π
if monitoring is enabled (via the prometheus plugin) then the following metrics are exported, bestie:
coredns_template_matches_total{server, zone, view, class, type}β the total number of matched requests by regex. π₯coredns_template_template_failures_total{server, zone, view, class, type, section, template}β the number of times the Go templating failed. the Regex, section and template label values can be used to map the error back to the config file. πcoredns_template_rr_failures_total{server, zone, view, class, type, section, template}β the number of times the templated resource record was invalid and could not be parsed. Regex, section and template label values can be used to map the error back to the config file. π
both failure cases indicate a problem with the template configuration. the server label indicates the server incrementing the metric β see the metrics plugin for details. π
(part 2 is coming, don't go anywhere π«Ά)
β¨ EXAMPLES β¨
Bestie, this is the fun part β real configs you can literally copy-paste. Each one comes with a lil breakdown of what's happening so nothing feels like β¨magicβ¨ you don't understand.
π« RESOLVE EVERYTHING TO NXDOMAIN
The most babygirl-simple template there is, no cap:
. {
template ANY ANY {
rcode NXDOMAIN
}
}
- This template uses the default zone (
.or all queries) - All queries will be answered (no
fallthrough) - The answer is always
NXDOMAIN
π RESOLVE .INVALID AS NXDOMAIN
The .invalid domain is a reserved TLD (see RFC 2606 Reserved Top Level DNS Names https://www.rfc-editor.org/info/rfc2606/#section-2) β it literally exists to mark domains as invalid.
. {
forward . 8.8.8.8
template ANY ANY invalid {
rcode NXDOMAIN
authority "invalid. 60 {{ .Class }} SOA ns.invalid. hostmaster.invalid. (1 60 60 60 60)"
ederror 21 "Blocked according to RFC2606"
}
}
- A query to
.invalidwill result inNXDOMAIN(rcode) - A dummy SOA record is sent to hand out a TTL of 60s for caching purposes
- Querying
.invalidin the CH class will also cause aNXDOMAIN/SOA response - The default regex is
.*
π BLOCK INVALID SEARCH DOMAIN COMPLETIONS
Okay so picture this: you run example.com with a datacenter dc1.example.com. The datacenter domain is part of the DNS search domain. But something.example.com.dc1.example.com would indicate a fully qualified domain name (something.example.com) that inadvertently has the default domain or search path (dc1.example.com) tacked on. Rude. We block it:
. {
forward . 8.8.8.8
template IN ANY example.com.dc1.example.com {
rcode NXDOMAIN
authority "{{ .Zone }} 60 IN SOA ns.example.com hostmaster.example.com (1 60 60 60 60)"
}
}
A more verbose regex-based equivalent would be:
. {
forward . 8.8.8.8
template IN ANY example.com {
match "example\.com\.(dc1\.example\.com\.)$"
rcode NXDOMAIN
authority "{{ index .Match 1 }} 60 IN SOA ns.{{ index .Match 1 }} hostmaster.{{ index .Match 1 }} (1 60 60 60 60)"
fallthrough
}
}
The regex-based version can do more complex matching/templating while zone-based templating is easier to read and use. Trade-offs, bestie π
π RESOLVE A/PTR FOR .EXAMPLE
. {
forward . 8.8.8.8
# ip-a-b-c-d.example A a.b.c.d
template IN A example {
match (^|[.])ip-(?P<a>[0-9]*)-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$
answer "{{ .Name }} 60 IN A {{ .Group.a }}.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
fallthrough
}
# d.c.b.a.in-addr.arpa PTR ip-a-b-c-d.example
template IN PTR in-addr.arpa {
match ^(?P<d>[0-9]*)[.](?P<c>[0-9]*)[.](?P<b>[0-9]*)[.](?P<a>[0-9]*)[.]in-addr[.]arpa[.]$
answer "{{ .Name }} 60 IN PTR ip-{{ .Group.a }}-{{ .Group.b }}-{{ .Group.c }}-{{ .Group.d }}.example."
}
}
An IPv4 address consists of 4 bytes, a.b.c.d. Named groups make it less error-prone to reverse the IP address in the PTR case. Try to use named groups to explain what your regex and template are doing β future you will thank you, fr fr.
Note that the A record is actually a wildcard: any subdomain of the IP address will resolve to the IP address.
Having templates to map certain PTR/A pairs is a common pattern.
fallthrough is needed for mixed domains where only some responses are templated.
π RESOLVE DEVICE ADDRESSES FOR HTTPS ON LAN
For an embedded device on a local network to be trusted, it needs to have a certificate signed by a CA and the CA needs to verify that the device controls the name for which it is issued. As the local address may change, a certificate is issued for the wildcard subdomain *.<id>.example.com instead, where <id> is a unique id for the given device. The same trick as in the example above can be used to find out the actual IP address.
Resolving to public IP addresses would allow for this to be used in phishing attacks, so an additional expression must be satisfied to limit the allowed IP range. Safety first, bestie π«Ά
. {
forward . 8.8.8.8
template IN A example.com {
match ^(?P<ip>[0-9]{1,3}(-[0-9]{1,3}){3})[.](?P<id>[a-z2-7]{26})[.]example[.]com[.]$
var ip replace(group('ip'), '-', '.')
expr "any(['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '169.254.0.0/16'], incidr(ip, #))"
answer "{{ .Name }} 60 IN A {{ .Var.ip }}"
fallthrough
}
}
The regular expression for the unique device id matches a base32 encoded string of a 128-bit device id, with the padding removed.
Note that an expression using # must be quoted, or it will be interpreted as a comment. Yeah. That's a whole thing. Don't get got π
π’ RESOLVE HEXADECIMAL IP PATTERN USING PARSEINT
. {
forward . 8.8.8.8
template IN A example {
match "^ip0a(?P<b>[a-f0-9]{2})(?P<c>[a-f0-9]{2})(?P<d>[a-f0-9]{2})[.]example[.]$"
answer "{{ .Name }} 60 IN A 10.{{ parseInt .Group.b 16 8 }}.{{ parseInt .Group.c 16 8 }}.{{ parseInt .Group.d 16 8 }}"
fallthrough
}
}
An IPv4 address can be expressed in a more compact form using its hexadecimal encoding. For example ip-10-123-123.example. can instead be expressed as ip0a7b7b7b.example. β compact and kinda iconic ngl.
π― RESOLVE MULTIPLE IP PATTERNS
. {
forward . 8.8.8.8
template IN A example {
match "^ip-(?P<a>10)-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]dc[.]example[.]$"
match "^(?P<a>[0-9]*)[.](?P<b>[0-9]*)[.](?P<c>[0-9]*)[.](?P<d>[0-9]*)[.]ext[.]example[.]$"
answer "{{ .Name }} 60 IN A {{ .Group.a}}.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
fallthrough
}
}
Named capture groups can be used to template one response for multiple patterns. One template, many fits. Efficiency queen π
π¬ RESOLVE A AND MX RECORDS FOR IP TEMPLATES IN .EXAMPLE
. {
forward . 8.8.8.8
template IN A example {
match ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$
answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
fallthrough
}
template IN MX example {
match ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$
answer "{{ .Name }} 60 IN MX 10 {{ .Name }}"
additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
fallthrough
}
}
Two templates, same match, different record types. It's giving coordination β¨
π ADDING AUTHORITATIVE NAMESERVERS TO THE RESPONSE
. {
forward . 8.8.8.8
template IN A example {
match ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$
answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
authority "example. 60 IN NS ns0.example."
authority "example. 60 IN NS ns1.example."
additional "ns0.example. 60 IN A 203.0.113.8"
additional "ns1.example. 60 IN A 198.51.100.8"
fallthrough
}
template IN MX example {
match ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$
answer "{{ .Name }} 60 IN MX 10 {{ .Name }}"
additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
authority "example. 60 IN NS ns0.example."
authority "example. 60 IN NS ns1.example."
additional "ns0.example. 60 IN A 203.0.113.8"
additional "ns1.example. 60 IN A 198.51.100.8"
fallthrough
}
}
Look at all those authority and additional lines β serving the full ensemble, not just the main character answer π
π¦ FABRICATE A CNAME
This example responds with a CNAME to google.com for any DNS query made exactly for foogle.com. The answer will also contain a record for google.com if the upstream nameserver can return a record for it of the requested type.
. {
template IN ANY foogle.com {
match "^foogle\.com\.$"
answer "foogle.com 60 IN CNAME google.com"
}
forward . 8.8.8.8
}
foogle.com β google.com. It's giving legally distinct ππ
π ALSO SEE
- Go regexp (
https://golang.org/pkg/regexp/) for details about the regex implementation - RE2 syntax reference (
https://github.com/google/re2/wiki/Syntax) for details about the regex syntax - RFC 1034 (
https://www.rfc-editor.org/info/rfc1034/#section-3.6.1) and RFC 1035 (https://www.rfc-editor.org/info/rfc1035/#section-5) for the resource record format - Go template (
https://golang.org/pkg/text/template/) for the template language reference
π BUGS
CoreDNS supports caddyfile environment variables (https://caddyserver.com/docs/caddyfile#env) with notion of {$ENV_VAR}. This parser feature will break Go template variables (https://golang.org/pkg/text/template/#hdr-Variables) notations like {{$variable}}. The equivalent notation {{ $variable }} will work. Try to avoid Go template variables in the context of this plugin.
So:{{$variable}}= broken,{{ $variable }}= works. The space is doing SO much heavy lifting here, bestie. Respect it π
And that's the whole page, babes! This was COREDNS-TEMPLATE(7) from CoreDNS, dated October 2026 β go forth and template responsibly, and remember: named capture groups are your besties π«Άβ¨π
π¦ where this page comes from
- opensuse-leap 16.0: coredns-extras 1.15.0-bp160.1.1 Β· Apache-2.0 AND MPL-2.0
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms