forked from coredns/coredns
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
plugin/tsig: new plugin TSIG (coredns#4957)
* expose tsig secrets via dnsserver.Config * add tsig plugin Signed-off-by: Chris O'Haver <[email protected]>
- Loading branch information
1 parent
6488595
commit 68e141e
Showing
14 changed files
with
1,112 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,7 @@ var Directives = []string{ | |
"any", | ||
"chaos", | ||
"loadbalance", | ||
"tsig", | ||
"cache", | ||
"rewrite", | ||
"header", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
# tsig | ||
|
||
## Name | ||
|
||
*tsig* - validate TSIG requests and sign responses. | ||
|
||
## Description | ||
|
||
With *tsig*, you can define a set of TSIG secret keys for validating incoming TSIG requests and signing | ||
responses. It can also require TSIG for certain query types, refusing requests that do not comply. | ||
|
||
## Syntax | ||
|
||
~~~ | ||
tsig [ZONE...] { | ||
secret NAME KEY | ||
secrets FILE | ||
require [QTYPE...] | ||
} | ||
~~~ | ||
|
||
* **ZONE** - the zones *tsig* will TSIG. By default, the zones from the server block are used. | ||
|
||
* `secret` **NAME** **KEY** - specifies a TSIG secret for **NAME** with **KEY**. Use this option more than once | ||
to define multiple secrets. Secrets are global to the server instance, not just for the enclosing **ZONE**. | ||
|
||
* `secrets` **FILE** - same as `secret`, but load the secrets from a file. The file may define any number | ||
of unique keys, each in the following `named.conf` format: | ||
```cgo | ||
key "example." { | ||
secret "X28hl0BOfAL5G0jsmJWSacrwn7YRm2f6U5brnzwWEus="; | ||
}; | ||
``` | ||
Each key may also specify an `algorithm` e.g. `algorithm hmac-sha256;`, but this is currently ignored by the plugin. | ||
* `require` **QTYPE...** - the query types that must be TSIG'd. Requests of the specified types | ||
will be `REFUSED` if they are not signed.`require all` will require requests of all types to be | ||
signed. `require none` will not require requests any types to be signed. Default behavior is to not require. | ||
## Examples | ||
Require TSIG signed transactions for transfer requests to `example.zone`. | ||
``` | ||
example.zone { | ||
tsig { | ||
secret example.zone.key. NoTCJU+DMqFWywaPyxSijrDEA/eC3nK0xi3AMEZuPVk= | ||
require AXFR IXFR | ||
} | ||
transfer { | ||
to * | ||
} | ||
} | ||
``` | ||
Require TSIG signed transactions for all requests to `auth.zone`. | ||
``` | ||
auth.zone { | ||
tsig { | ||
secret auth.zone.key. NoTCJU+DMqFWywaPyxSijrDEA/eC3nK0xi3AMEZuPVk= | ||
require all | ||
} | ||
forward . 10.1.0.2 | ||
} | ||
``` | ||
## Bugs | ||
### Zone Transfer Notifies | ||
With the transfer plugin, zone transfer notifications from CoreDNS are not TSIG signed. | ||
### Special Considerations for Forwarding Servers (RFC 8945 5.5) | ||
https://datatracker.ietf.org/doc/html/rfc8945#section-5.5 | ||
CoreDNS does not implement this section as follows ... | ||
* RFC requirement: | ||
> If the name on the TSIG is not | ||
of a secret that the server shares with the originator, the server | ||
MUST forward the message unchanged including the TSIG. | ||
CoreDNS behavior: | ||
If ths zone of the request matches the _tsig_ plugin zones, then the TSIG record | ||
is always stripped. But even when the _tsig_ plugin is not involved, the _forward_ plugin | ||
may alter the message with compression, which would cause validation failure | ||
at the destination. | ||
* RFC requirement: | ||
> If the TSIG passes all checks, the forwarding | ||
server MUST, if possible, include a TSIG of its own to the | ||
destination or the next forwarder. | ||
CoreDNS behavior: | ||
If ths zone of the request matches the _tsig_ plugin zones, _forward_ plugin will | ||
proxy the request upstream without TSIG. | ||
* RFC requirement: | ||
> If no transaction security is | ||
available to the destination and the message is a query, and if the | ||
corresponding response has the AD flag (see RFC4035) set, the | ||
forwarder MUST clear the AD flag before adding the TSIG to the | ||
response and returning the result to the system from which it | ||
received the query. | ||
CoreDNS behavior: | ||
The AD flag is not cleared. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
package tsig | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"io" | ||
"os" | ||
"strings" | ||
|
||
"github.com/coredns/caddy" | ||
"github.com/coredns/coredns/core/dnsserver" | ||
"github.com/coredns/coredns/plugin" | ||
|
||
"github.com/miekg/dns" | ||
) | ||
|
||
func init() { | ||
caddy.RegisterPlugin(pluginName, caddy.Plugin{ | ||
ServerType: "dns", | ||
Action: setup, | ||
}) | ||
} | ||
|
||
func setup(c *caddy.Controller) error { | ||
t, err := parse(c) | ||
if err != nil { | ||
return plugin.Error(pluginName, c.ArgErr()) | ||
} | ||
|
||
config := dnsserver.GetConfig(c) | ||
|
||
config.TsigSecret = t.secrets | ||
|
||
config.AddPlugin(func(next plugin.Handler) plugin.Handler { | ||
t.Next = next | ||
return t | ||
}) | ||
|
||
return nil | ||
} | ||
|
||
func parse(c *caddy.Controller) (*TSIGServer, error) { | ||
t := &TSIGServer{ | ||
secrets: make(map[string]string), | ||
types: defaultQTypes, | ||
} | ||
|
||
for i := 0; c.Next(); i++ { | ||
if i > 0 { | ||
return nil, plugin.ErrOnce | ||
} | ||
|
||
t.Zones = plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) | ||
for c.NextBlock() { | ||
switch c.Val() { | ||
case "secret": | ||
args := c.RemainingArgs() | ||
if len(args) != 2 { | ||
return nil, c.ArgErr() | ||
} | ||
k := plugin.Name(args[0]).Normalize() | ||
if _, exists := t.secrets[k]; exists { | ||
return nil, fmt.Errorf("key %q redefined", k) | ||
} | ||
t.secrets[k] = args[1] | ||
case "secrets": | ||
args := c.RemainingArgs() | ||
if len(args) != 1 { | ||
return nil, c.ArgErr() | ||
} | ||
f, err := os.Open(args[0]) | ||
if err != nil { | ||
return nil, err | ||
} | ||
secrets, err := parseKeyFile(f) | ||
if err != nil { | ||
return nil, err | ||
} | ||
for k, s := range secrets { | ||
if _, exists := t.secrets[k]; exists { | ||
return nil, fmt.Errorf("key %q redefined", k) | ||
} | ||
t.secrets[k] = s | ||
} | ||
case "require": | ||
t.types = qTypes{} | ||
args := c.RemainingArgs() | ||
if len(args) == 0 { | ||
return nil, c.ArgErr() | ||
} | ||
if args[0] == "all" { | ||
t.all = true | ||
continue | ||
} | ||
if args[0] == "none" { | ||
continue | ||
} | ||
for _, str := range args { | ||
qt, ok := dns.StringToType[str] | ||
if !ok { | ||
return nil, c.Errf("unknown query type '%s'", str) | ||
} | ||
t.types[qt] = struct{}{} | ||
} | ||
default: | ||
return nil, c.Errf("unknown property '%s'", c.Val()) | ||
} | ||
} | ||
} | ||
return t, nil | ||
} | ||
|
||
func parseKeyFile(f io.Reader) (map[string]string, error) { | ||
secrets := make(map[string]string) | ||
s := bufio.NewScanner(f) | ||
for s.Scan() { | ||
fields := strings.Fields(s.Text()) | ||
if len(fields) == 0 { | ||
continue | ||
} | ||
if fields[0] != "key" { | ||
return nil, fmt.Errorf("unexpected token %q", fields[0]) | ||
} | ||
if len(fields) < 2 { | ||
return nil, fmt.Errorf("expected key name %q", s.Text()) | ||
} | ||
key := strings.Trim(fields[1], "\"{") | ||
if len(key) == 0 { | ||
return nil, fmt.Errorf("expected key name %q", s.Text()) | ||
} | ||
key = plugin.Name(key).Normalize() | ||
if _, ok := secrets[key]; ok { | ||
return nil, fmt.Errorf("key %q redefined", key) | ||
} | ||
key: | ||
for s.Scan() { | ||
fields := strings.Fields(s.Text()) | ||
if len(fields) == 0 { | ||
continue | ||
} | ||
switch fields[0] { | ||
case "algorithm": | ||
continue | ||
case "secret": | ||
if len(fields) < 2 { | ||
return nil, fmt.Errorf("expected secret key %q", s.Text()) | ||
} | ||
secret := strings.Trim(fields[1], "\";") | ||
if len(secret) == 0 { | ||
return nil, fmt.Errorf("expected secret key %q", s.Text()) | ||
} | ||
secrets[key] = secret | ||
case "}": | ||
fallthrough | ||
case "};": | ||
break key | ||
default: | ||
return nil, fmt.Errorf("unexpected token %q", fields[0]) | ||
} | ||
} | ||
if _, ok := secrets[key]; !ok { | ||
return nil, fmt.Errorf("expected secret for key %q", key) | ||
} | ||
} | ||
return secrets, nil | ||
} | ||
|
||
var defaultQTypes = qTypes{} |
Oops, something went wrong.