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.

Logging with a test zone looks something like this:

~~~ txt
2019-07-20T15:29:08.719Z [INFO] plugin/sign: Signing "miek.nl." because open plugin/sign/testdata/db.miek.nl.signed: no such file or directory
2019-07-20T15:29:08.719Z [INFO] plugin/sign: Signed "miek.nl." with key tags "59725" in 11.670985ms, saved in "plugin/sign/testdata/db.miek.nl.signed". Next: 2019-07-20T15:49:06.560Z
2019-07-20T15:30:08.711Z [INFO] plugin/file: Successfully reloaded zone "miek.nl." in "plugin/sign/testdata/db.miek.nl.signed" with serial 1563636548
2019-07-20T15:49:08.709Z [INFO] plugin/sign: Signing "miek.nl." because resign was: 10m0s ago
2019-07-20T15:49:08.709Z [INFO] plugin/sign: Signed "miek.nl." with key tags "59725" in 2.055895ms, saved in "plugin/sign/testdata/db.miek.nl.signed". Next: 2019-07-20T16:09:06.560Z
2019-07-20T15:50:08.708Z [INFO] plugin/file: Successfully reloaded zone "miek.nl." in "plugin/sign/testdata/db.miek.nl.signed" with serial 1563637748
~~~

Signed-off-by: Miek Gieben <miek@miek.nl>
  • Loading branch information
miekg committed Aug 27, 2019
1 parent 87bd9de commit 2795789
Show file tree
Hide file tree
Showing 21 changed files with 1,111 additions and 1 deletion.
1 change: 1 addition & 0 deletions core/dnsserver/zdirectives.go
Expand Up @@ -51,4 +51,5 @@ var Directives = []string{
"erratic",
"whoami",
"on",
"sign",
}
1 change: 1 addition & 0 deletions core/plugin/zplugin.go
Expand Up @@ -40,6 +40,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
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -54,7 +54,7 @@ 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-20190801041406-cbf593c0f2f3
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Expand Up @@ -306,6 +306,8 @@ golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
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 @@ -335,6 +337,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 @@ -366,6 +370,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-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down Expand Up @@ -407,6 +413,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 @@ -60,3 +60,4 @@ grpc:grpc
erratic:erratic
whoami:whoami
on:github.com/caddyserver/caddy/onevent
sign:sign
161 changes: 161 additions & 0 deletions plugin/sign/README.md
@@ -0,0 +1,161 @@
# 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 that sign the resource records sets have an expiration date, this means the
signing process must be repeated before this expiration data is reached. Otherwise the zone's data
will go BAD (RFC 4035, Section 5.5). The *sign* plugin takes care of this. *Sign* works, but has
a couple of limitations, see the "Bugs" section.

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

*Sign* 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 or algorithm rollovers - it just signs.

*Sign* will:

* (Re)-sign the zone with the CSK(s) when:

- the last time it was signed is more than a 6 days ago. Each zone will have some jitter
applied to the inception date.

- the signature only has 14 days left before expiring.

Both these dates are only checked on the SOA's signature(s).

* Create signatures that have an inception of -3 hours (minus a jitter between 0 and 18 hours)
and a expiration of +32 days for every given DNSKEY.

* 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.

Thus there are two ways that dictate when a zone is signed. Normally every 6 days (plus jitter) it
will be resigned. If for some reason we fail this check, the 14 days before expiring kicks in.

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 file` directive.

A generated zone is written out in a file named `db.<name>.signed` in the directory named by the
`directory` directive (which defaults to `/var/lib/coredns`).

## Syntax

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

* **DBFILE** the zone 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.
* `key` specifies the key(s) (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*. These keys must also be Key Signing Keys (KSK).
* `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.

Keys can be generated with `coredns-keygen`, to create one for use in the *sign* plugin, use:
`coredns-keygen example.org` or `dnssec-keygen -a ECDSAP256SHA256 -f KSK example.org`.

## Examples

Sign the `example.org` zone contained in the file `db.example.org` and write to result to
`./db.example.org.signed` to let the *file* plugin pick it up and serve it. The keys used
are read from `/etc/coredns/keys/Kexample.org.key` and `/etc/coredns/keys/Kexample.org.private`.

~~~ txt
example.org {
file db.example.org.signed
sign db.example.org {
key file /etc/coredns/keys/Kexample.org
directory .
}
}
~~~

Running this leads to the following log output (note the timers in this example have been set to
shorter intervals).

~~~ txt
2019-08-02T17:27:45.270Z [WARNING] plugin/file: Failed to open "open /tmp/db.example.org.signed: no such file or directory": trying again in 1m0s
2019-08-02T17:27:45.279Z [INFO] plugin/sign: Signing "example.org." because open /tmp/db.example.org.signed: no such file or directory
2019-08-02T17:27:45.279Z [INFO] plugin/sign: Successfully signed zone "example.org." in "/tmp/db.example.org.signed" with key tags "59725" and 1564766865 SOA serial, elapsed 9.357933ms, next: 2019-08-02T22:27:45.270Z
2019-08-02T17:28:45.271Z [INFO] plugin/file: Successfully reloaded zone "example.org." in "/tmp/db.example.org.signed" with serial 1564766865
~~~

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`.

~~~ txt
. {
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:

~~~ txt
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
}
}
~~~

Be careful to fully list the origins you want to sign, if you don't:

~~~ txt
example.org example.net {
sign plugin/sign/testdata/db.example.org miek.org {
key file /etc/coredns/keys/Kexample.org
}
}
~~~

This will lead to `db.example.org` be signed *twice*, as this entire section is parsed twice because
you have specified the origins `example.org` and `example.net` in the server block.

Forcibly resigning a zone can be accomplished by removing the signed zone file (CoreDNS will keep on
serving it from memory), and sending SIGUSR1 to the process to make it reload and resign the zone
file.

## 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.

Coredns-keygen can be found at <https://github.com/coredns-utils> in the coredns-keygen directory.

## Bugs

`keys directory` is not implemented. Glue records are currently signed, and no DS records are added
for child zones.
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
}
93 changes: 93 additions & 0 deletions plugin/sign/file.go
@@ -0,0 +1,93 @@
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 moved into the correct place.
func (s *Signer) write(z *file.Zone) error {
f, err := ioutil.TempFile(s.directory, "signed-")
if err != nil {
return err
}

if err := write(f, z); err != nil {
f.Close()
return err
}

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

func write(w io.Writer, z *file.Zone) error {
if _, err := io.WriteString(w, z.Apex.SOA.String()); err != nil {
return err
}
w.Write([]byte("\n"))
for _, rr := range z.Apex.SIGSOA {
io.WriteString(w, rr.String())
w.Write([]byte("\n"))
}
for _, rr := range z.Apex.NS {
io.WriteString(w, rr.String())
w.Write([]byte("\n"))
}
for _, rr := range z.Apex.SIGNS {
io.WriteString(w, rr.String())
w.Write([]byte("\n"))
}
err := z.Walk(func(e *tree.Elem, _ map[uint16][]dns.RR) error {
for _, r := range e.All() {
io.WriteString(w, r.String())
w.Write([]byte("\n"))
}
return nil
})
return err
}

// 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 types DNSKEY, RRSIG, CDNSKEY and CDS are *not* included in the returned
// zone (if encountered).
func Parse(f io.Reader, origin, fileName string) (*file.Zone, error) {
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.DNSKEY, *dns.RRSIG, *dns.CDNSKEY, *dns.CDS:
continue
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
}
43 changes: 43 additions & 0 deletions plugin/sign/file_test.go
@@ -0,0 +1,43 @@
package sign

import (
"os"
"testing"

"github.com/miekg/dns"
)

func TestFileParse(t *testing.T) {
f, err := os.Open("testdata/db.miek.nl")
if err != nil {
t.Fatal(err)
}
z, err := Parse(f, "miek.nl.", "testdata/db.miek.nl")
if err != nil {
t.Fatal(err)
}
s := &Signer{
directory: ".",
signedfile: "db.miek.nl.test",
}

s.write(z)
defer os.Remove("db.miek.nl.test")

f, err = os.Open("db.miek.nl.test")
if err != nil {
t.Fatal(err)
}
z, err = Parse(f, "miek.nl.", "db.miek.nl.test")
if err != nil {
t.Fatal(err)
}
if x := z.Apex.SOA.Header().Name; x != "miek.nl." {
t.Errorf("Expected SOA name to be %s, got %s", x, "miek.nl.")
}
apex, _ := z.Search("miek.nl.")
key := apex.Type(dns.TypeDNSKEY)
if key != nil {
t.Errorf("Expected no DNSKEYs, but got %d", len(key))
}
}

0 comments on commit 2795789

Please sign in to comment.