Skip to content

Commit

Permalink
plugin/tsig: new plugin TSIG (coredns#4957)
Browse files Browse the repository at this point in the history
* expose tsig secrets via dnsserver.Config
* add tsig plugin

Signed-off-by: Chris O'Haver <[email protected]>
  • Loading branch information
chrisohaver authored Jun 27, 2022
1 parent 6488595 commit 68e141e
Show file tree
Hide file tree
Showing 14 changed files with 1,112 additions and 3 deletions.
3 changes: 3 additions & 0 deletions core/dnsserver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ type Config struct {
// TLSConfig when listening for encrypted connections (gRPC, DNS-over-TLS).
TLSConfig *tls.Config

// TSIG secrets, [name]key.
TsigSecret map[string]string

// Plugin stack.
Plugin []plugin.Plugin

Expand Down
1 change: 1 addition & 0 deletions core/dnsserver/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ func (h *dnsContext) MakeServers() ([]caddy.Server, error) {
c.Debug = c.firstConfigInBlock.Debug
c.Stacktrace = c.firstConfigInBlock.Stacktrace
c.TLSConfig = c.firstConfigInBlock.TLSConfig
c.TsigSecret = c.firstConfigInBlock.TsigSecret
}

// we must map (group) each config to a bind address
Expand Down
12 changes: 10 additions & 2 deletions core/dnsserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ type Server struct {
debug bool // disable recover()
stacktrace bool // enable stacktrace in recover error log
classChaos bool // allow non-INET class queries

tsigSecret map[string]string
}

// NewServer returns a new CoreDNS server and compiles all plugins in to it. By default CH class
Expand All @@ -54,6 +56,7 @@ func NewServer(addr string, group []*Config) (*Server, error) {
Addr: addr,
zones: make(map[string]*Config),
graceTimeout: 5 * time.Second,
tsigSecret: make(map[string]string),
}

// We have to bound our wg with one increment
Expand All @@ -73,6 +76,11 @@ func NewServer(addr string, group []*Config) (*Server, error) {
// set the config per zone
s.zones[site.Zone] = site

// copy tsig secrets
for key, secret := range site.TsigSecret {
s.tsigSecret[key] = secret
}

// compile custom plugin for everything
var stack plugin.Handler
for i := len(site.Plugin) - 1; i >= 0; i-- {
Expand Down Expand Up @@ -115,7 +123,7 @@ func (s *Server) Serve(l net.Listener) error {
ctx := context.WithValue(context.Background(), Key{}, s)
ctx = context.WithValue(ctx, LoopKey{}, 0)
s.ServeDNS(ctx, w, r)
})}
}), TsigSecret: s.tsigSecret}
s.m.Unlock()

return s.server[tcp].ActivateAndServe()
Expand All @@ -129,7 +137,7 @@ func (s *Server) ServePacket(p net.PacketConn) error {
ctx := context.WithValue(context.Background(), Key{}, s)
ctx = context.WithValue(ctx, LoopKey{}, 0)
s.ServeDNS(ctx, w, r)
})}
}), TsigSecret: s.tsigSecret}
s.m.Unlock()

return s.server[udp].ActivateAndServe()
Expand Down
1 change: 1 addition & 0 deletions core/dnsserver/zdirectives.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var Directives = []string{
"any",
"chaos",
"loadbalance",
"tsig",
"cache",
"rewrite",
"header",
Expand Down
1 change: 1 addition & 0 deletions core/plugin/zplugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@ import (
_ "github.com/coredns/coredns/plugin/tls"
_ "github.com/coredns/coredns/plugin/trace"
_ "github.com/coredns/coredns/plugin/transfer"
_ "github.com/coredns/coredns/plugin/tsig"
_ "github.com/coredns/coredns/plugin/whoami"
)
1 change: 1 addition & 0 deletions plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ acl:acl
any:any
chaos:chaos
loadbalance:loadbalance
tsig:tsig
cache:cache
rewrite:rewrite
header:header
Expand Down
4 changes: 3 additions & 1 deletion plugin/transfer/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ func setup(c *caddy.Controller) error {
})

c.OnStartup(func() error {
config := dnsserver.GetConfig(c)
t.tsigSecret = config.TsigSecret
// find all plugins that implement Transferer and add them to Transferers
plugins := dnsserver.GetConfig(c).Handlers()
plugins := config.Handlers()
for _, pl := range plugins {
tr, ok := pl.(Transferer)
if !ok {
Expand Down
4 changes: 4 additions & 0 deletions plugin/transfer/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var log = clog.NewWithPlugin("transfer")
type Transfer struct {
Transferers []Transferer // List of plugins that implement Transferer
xfrs []*xfr
tsigSecret map[string]string
Next plugin.Handler
}

Expand Down Expand Up @@ -110,6 +111,9 @@ func (t *Transfer) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms
// Send response to client
ch := make(chan *dns.Envelope)
tr := new(dns.Transfer)
if r.IsTsig() != nil {
tr.TsigSecret = t.tsigSecret
}
errCh := make(chan error)
go func() {
if err := tr.Out(w, r, ch); err != nil {
Expand Down
111 changes: 111 additions & 0 deletions plugin/tsig/README.md
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.
168 changes: 168 additions & 0 deletions plugin/tsig/setup.go
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{}
Loading

0 comments on commit 68e141e

Please sign in to comment.