Skip to content

Commit

Permalink
plugin/sign: a plugin that signs zones
Browse files Browse the repository at this point in the history
Sign is a plugin that signs zone data (on disk). The README.md details
what exactly happens to should be accurate related to the code.

Signs are signed with a CSK, resigning and first time signing is all
handled by *sign* plugin.

Signed-off-by: Miek Gieben <miek@miek.nl>
  • Loading branch information
miekg committed Jul 19, 2019
1 parent 1d5095c commit 895c2d3
Show file tree
Hide file tree
Showing 22 changed files with 948 additions and 1 deletion.
1 change: 1 addition & 0 deletions core/dnsserver/zdirectives.go
Expand Up @@ -49,4 +49,5 @@ var Directives = []string{
"erratic",
"whoami",
"on",
"sign",
}
1 change: 1 addition & 0 deletions core/plugin/zplugin.go
Expand Up @@ -39,6 +39,7 @@ import (
_ "github.com/coredns/coredns/plugin/root"
_ "github.com/coredns/coredns/plugin/route53"
_ "github.com/coredns/coredns/plugin/secondary"
_ "github.com/coredns/coredns/plugin/sign"
_ "github.com/coredns/coredns/plugin/template"
_ "github.com/coredns/coredns/plugin/tls"
_ "github.com/coredns/coredns/plugin/trace"
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Expand Up @@ -51,9 +51,10 @@ require (
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.9.1 // indirect
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect
google.golang.org/genproto v0.0.0-20190701230453-710ae3a149df // indirect
google.golang.org/grpc v1.22.0
gopkg.in/DataDog/dd-trace-go.v1 v1.16.0
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Expand Up @@ -242,6 +242,8 @@ golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 h1:IcSOAf4PyMp3U3XbIEj1/xJ2BjNN2jWv7JoyOsMxXUU=
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
Expand Down Expand Up @@ -269,6 +271,8 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
Expand Down Expand Up @@ -297,6 +301,8 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -319,6 +325,8 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
Expand All @@ -331,6 +339,8 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190611190212-a7e196e89fd3/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190626174449-989357319d63 h1:UsSJe9fhWNSz6emfIGPpH5DF23t7ALo2Pf3sC+/hsdg=
google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190701230453-710ae3a149df h1:k3DT34vxk64+4bD5x+fRy6U0SXxZehzUHRSYUJcKfII=
google.golang.org/genproto v0.0.0-20190701230453-710ae3a149df/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
Expand Down
1 change: 1 addition & 0 deletions plugin.cfg
Expand Up @@ -58,3 +58,4 @@ grpc:grpc
erratic:erratic
whoami:whoami
on:github.com/caddyserver/caddy/onevent
sign:sign
116 changes: 116 additions & 0 deletions plugin/sign/README.md
@@ -0,0 +1,116 @@
# sign

## Name

*sign* - add DNSSEC records to zone files.

## Description

The *sign* plugin is used to sign (see RFC 6781) zones. In this process DNSSEC resource records
are added. The signatures have a expiration date, so the signing process must be repeated every so
often, otherwise the zone's data will go BAD (RFC 4035, Section 5.5). The *sign* plugin takes care
of this.

Only NSEC is supported, *sign* does not support NSEC3.

*Signs* works in conjunction with the *file* and *auto* plugins; this plugin **signs** the zones
*files*, *auto* and *file* **serve** the zones *data*.

For this plugin to work at least one Common Signing Key, (see coredns-keygen(1)) is needed. This key
(or keys) will be used to sign the entire zone. *Sign* does not support the ZSK/KSK split, nor will
it do key rollovers - it just signs.

*Sign* will:

* (Re)-sign the zone with the CSK(s) every Thursday at 15:00 UTC (- generous jitter).
The jitter will be applied to avoid a stampeding herd of zones waiting to be signed.
This default to random amount between -5 to 0 days.
* Create signatures that have an inception of -3H and expiration of +3W for every key given.
* Add or replace *all* apex CDS/CDNSKEY records with the ones derived from the given keys. For each
key two CDS are created one with SHA1 and another with SHA256.
* Update the SOA's serial number to the *Unix epoch* of when the signing happens. This will
overwrite *any* previous serial number.

Keys are named (following BIND9): `K<name>+<alg>+<id>.key` and `K<name>+<alg>+<id>.private`.
The keys **must not** be included in your zone; they will be added by *sign*. These keys can be
generated with `coredns-keygen` or BIND9's `dnssec-keygen`. You don't have to adhere to this naming
scheme, but then you need to name your keys explicitly, see the `keys files` directive.

A generated zone is written out in a file named `db.<name>.signed`.

When CoreDNS starts up (or is reloaded) a quick scan is done to see if the zone needs to be
resigned; this happens by checking SOA's RRSIG expiration time. If within 2 weeks, the zone will be
resigned.

## Syntax

~~~
sign DBFILE [ZONES...] {
keys file|directory KEY...|DIR...
directory DIR
}
~~~

* **DBFILE** the database file to read and parse. If the path is relative, the path from the
*root* directive will be prepended to it.
* **ZONES** zones it should be sign for. If empty, the zones from the configuration block are
used.
* `keys` specifies the keys (there can be multiple) to sign the zone. If `file` is
used the **KEY**'s filenames are used as is. If `directory` is used, *sign* will look in **DIR**
for `K<name>+<alg>+<id>` files. Any metadata in these files (Activate, Publish, etc.) is
*ignored*.
* `directory` specifies the **DIR** where CoreDNS should save zones that have been signed.
If not given this defaults to `/var/lib/coredns`. The zones are saved under the name
`db.<name>.signed`. If the path is relative the path from the *root* directive will be prepended
to it.

## Examples

Sign the `example.org` zone contained in the file `db.example.org` and write to result to
`/var/lib/db.example.org.signed` to let the *file* plugin pick it up and serve it.

~~~ corefile
example.org {
file /var/lib/coredns/db.example.org.signed
sign db.example.org {
key file /etc/coredns/keys/Kexample.org
}
}
~~~

Or use a single zone file for *multiple* zones, note that the **ZONES** are repeated for both plugins.
Also note this outputs *multiple* signed output files. Here we use the default output directory
`/var/lib/coredns`.

~~~ corefile
. {
file /var/lib/coredns/db.example.org.signed example.org
file /var/lib/coredns/db.example.net.signed example.net
sign db.example.org example.org example.net {
key directory /etc/coredns/keys
}
}
~~~

This is the same configuration, but the zones are put in the server block, but note that you still
need to specify what file is served for what zone in the *file* plugin:

~~~ corefile
example.org example.net {
file var/lib/coredns/db.example.org.signed example.org
file var/lib/coredns/db.example.net.signed example.net
sign db.example.org {
key directory /etc/coredns/keys
}
}
~~~

## Also See

The DNSSEC RFCs: RFC 4033, RFC 4034 and RFC 4035. And the BCP on DNSSEC, RFC 6781. Further more the
manual pages coredns-keygen(1) and dnssec-keygen(8). And the *file* plugin's documentation.

## Bugs

`keys directory` is not implemented, nor is coredns-keygen.
5 changes: 5 additions & 0 deletions plugin/sign/TODO
@@ -0,0 +1,5 @@
* glue, delegation
* more tests, test nsec sorting, test zone signing apex only zone etc.
resign test, key parse test
* discard bad keys on default
* empty-non-terminals
20 changes: 20 additions & 0 deletions plugin/sign/dnssec.go
@@ -0,0 +1,20 @@
package sign

import (
"github.com/miekg/dns"
)

func (p Pair) signRRs(rrs []dns.RR, signerName string, ttl, incep, expir uint32) (*dns.RRSIG, error) {
rrsig := &dns.RRSIG{
Hdr: dns.RR_Header{Rrtype: dns.TypeRRSIG, Ttl: ttl},
Algorithm: p.Public.Algorithm,
SignerName: signerName,
KeyTag: p.KeyTag,
OrigTtl: ttl,
Inception: incep,
Expiration: expir,
}

e := rrsig.Sign(p.Private, rrs)
return rrsig, e
}
83 changes: 83 additions & 0 deletions plugin/sign/file.go
@@ -0,0 +1,83 @@
package sign

import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"

"github.com/coredns/coredns/plugin/file"
"github.com/coredns/coredns/plugin/file/tree"

"github.com/miekg/dns"
)

// write writes out the zone file to a temporary file which is then move into the correct place.
func (s Signer) write(z *file.Zone) error {
f, err := ioutil.TempFile(s.directory, "signed-")
if err != nil {
return err
}

// need better sorting of these records, to make it slightly nicer
fmt.Fprintln(f, z.Apex.SOA.String())
for _, rr := range z.Apex.SIGSOA {
fmt.Fprintln(f, rr.String())
}
for _, rr := range z.Apex.NS {
fmt.Fprintln(f, rr.String())
}
for _, rr := range z.Apex.SIGNS {
fmt.Fprintln(f, rr.String())
}
err = z.Walk(func(e *tree.Elem, _ map[uint16][]dns.RR) error {
for _, r := range e.All() {
fmt.Fprintln(f, r.String())
}
return nil
})
if err != nil {
return err
}

f.Close()
return os.Rename(f.Name(), filepath.Join(s.directory, s.signedfile))
}

// Parse parses the zone in filename and returns a new Zone or an error. This
// is similar to the Parse function in the *file* plugin. However when parsing the
// record type RRSIG, DNSKEY, CDNSKEY and CDS are *not* included in the
// returned zone (if encountered).
func Parse(f io.Reader, origin, fileName string) (*file.Zone, error) {
// make work on signer?
zp := dns.NewZoneParser(f, dns.Fqdn(origin), fileName)
zp.SetIncludeAllowed(true)
z := file.NewZone(origin, fileName)
seenSOA := false

for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
if err := zp.Err(); err != nil {
return nil, err
}

switch rr.(type) {
case *dns.RRSIG, *dns.DNSKEY, *dns.CDNSKEY, *dns.CDS:
// drop
case *dns.SOA:
seenSOA = true
if err := z.Insert(rr); err != nil {
return nil, err
}
default:
if err := z.Insert(rr); err != nil {
return nil, err
}
}
}
if !seenSOA {
return nil, fmt.Errorf("file %q has no SOA record", fileName)
}

return z, nil
}

0 comments on commit 895c2d3

Please sign in to comment.