Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
plugin/sign: a plugin that signs zones
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
Showing
21 changed files
with
1,111 additions
and
1 deletion.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,4 +51,5 @@ var Directives = []string{ | |
"erratic", | ||
"whoami", | ||
"on", | ||
"sign", | ||
} |
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 |
---|---|---|
|
@@ -60,3 +60,4 @@ grpc:grpc | |
erratic:erratic | ||
whoami:whoami | ||
on:github.com/caddyserver/caddy/onevent | ||
sign:sign |
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,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. |
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,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 | ||
} |
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,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 | ||
} |
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,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)) | ||
} | ||
} |
Oops, something went wrong.