mirror of
https://github.com/coredns/coredns.git
synced 2025-10-27 08:14:18 -04:00
plugin/sign: a plugin that signs zone (#2993)
* 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 [INFO] plugin/sign: Signing "miek.nl." because open plugin/sign/testdata/db.miek.nl.signed: no such file or directory [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 [INFO] plugin/file: Successfully reloaded zone "miek.nl." in "plugin/sign/testdata/db.miek.nl.signed" with serial 1563636548 [INFO] plugin/sign: Signing "miek.nl." because resign was: 10m0s ago [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 [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> * Adjust readme and remove timestamps Signed-off-by: Miek Gieben <miek@miek.nl> * Comment on the newline Signed-off-by: Miek Gieben <miek@miek.nl> * Update plugin/sign/README.md Co-Authored-By: Michael Grosser <development@stp-ip.net>
This commit is contained in:
@@ -51,4 +51,5 @@ var Directives = []string{
|
|||||||
"erratic",
|
"erratic",
|
||||||
"whoami",
|
"whoami",
|
||||||
"on",
|
"on",
|
||||||
|
"sign",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import (
|
|||||||
_ "github.com/coredns/coredns/plugin/root"
|
_ "github.com/coredns/coredns/plugin/root"
|
||||||
_ "github.com/coredns/coredns/plugin/route53"
|
_ "github.com/coredns/coredns/plugin/route53"
|
||||||
_ "github.com/coredns/coredns/plugin/secondary"
|
_ "github.com/coredns/coredns/plugin/secondary"
|
||||||
|
_ "github.com/coredns/coredns/plugin/sign"
|
||||||
_ "github.com/coredns/coredns/plugin/template"
|
_ "github.com/coredns/coredns/plugin/template"
|
||||||
_ "github.com/coredns/coredns/plugin/tls"
|
_ "github.com/coredns/coredns/plugin/tls"
|
||||||
_ "github.com/coredns/coredns/plugin/trace"
|
_ "github.com/coredns/coredns/plugin/trace"
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -42,7 +42,7 @@ require (
|
|||||||
github.com/tinylib/msgp v1.1.0 // indirect
|
github.com/tinylib/msgp v1.1.0 // indirect
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect
|
||||||
go.etcd.io/etcd v0.0.0-20190823073701-67d0c21bb04c
|
go.etcd.io/etcd v0.0.0-20190823073701-67d0c21bb04c
|
||||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
|
||||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3
|
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect
|
||||||
google.golang.org/api v0.9.0
|
google.golang.org/api v0.9.0
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -353,6 +353,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-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-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-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 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
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=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@@ -382,6 +384,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 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-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-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-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 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
|
||||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -416,6 +420,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-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-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-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-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 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
|
||||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -458,6 +464,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-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-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-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-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 h1:k3DT34vxk64+4bD5x+fRy6U0SXxZehzUHRSYUJcKfII=
|
||||||
google.golang.org/genproto v0.0.0-20190701230453-710ae3a149df/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
google.golang.org/genproto v0.0.0-20190701230453-710ae3a149df/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||||
|
|||||||
@@ -60,3 +60,4 @@ grpc:grpc
|
|||||||
erratic:erratic
|
erratic:erratic
|
||||||
whoami:whoami
|
whoami:whoami
|
||||||
on:github.com/caddyserver/caddy/onevent
|
on:github.com/caddyserver/caddy/onevent
|
||||||
|
sign:sign
|
||||||
|
|||||||
161
plugin/sign/README.md
Normal file
161
plugin/sign/README.md
Normal file
@@ -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 the 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
|
||||||
|
[WARNING] plugin/file: Failed to open "open /tmp/db.example.org.signed: no such file or directory": trying again in 1m0s
|
||||||
|
[INFO] plugin/sign: Signing "example.org." because open /tmp/db.example.org.signed: no such file or directory
|
||||||
|
[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
|
||||||
|
[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/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
plugin/sign/dnssec.go
Normal file
20
plugin/sign/dnssec.go
Normal file
@@ -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
plugin/sign/file.go
Normal file
93
plugin/sign/file.go
Normal file
@@ -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")) // RR Stringer() method doesn't include newline, which ends the RR in a zone file, write that here.
|
||||||
|
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
plugin/sign/file_test.go
Normal file
43
plugin/sign/file_test.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
119
plugin/sign/keys.go
Normal file
119
plugin/sign/keys.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coredns/coredns/core/dnsserver"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pair holds DNSSEC key information, both the public and private components are stored here.
|
||||||
|
type Pair struct {
|
||||||
|
Public *dns.DNSKEY
|
||||||
|
KeyTag uint16
|
||||||
|
Private crypto.Signer
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyParse reads the public and private key from disk.
|
||||||
|
func keyParse(c *caddy.Controller) ([]Pair, error) {
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
pairs := []Pair{}
|
||||||
|
config := dnsserver.GetConfig(c)
|
||||||
|
|
||||||
|
switch c.Val() {
|
||||||
|
case "file":
|
||||||
|
ks := c.RemainingArgs()
|
||||||
|
if len(ks) == 0 {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
for _, k := range ks {
|
||||||
|
base := k
|
||||||
|
// Kmiek.nl.+013+26205.key, handle .private or without extension: Kmiek.nl.+013+26205
|
||||||
|
if strings.HasSuffix(k, ".key") {
|
||||||
|
base = k[:len(k)-4]
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(k, ".private") {
|
||||||
|
base = k[:len(k)-8]
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(base) && config.Root != "" {
|
||||||
|
base = filepath.Join(config.Root, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
pair, err := readKeyPair(base+".key", base+".private")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pairs = append(pairs, pair)
|
||||||
|
}
|
||||||
|
case "directory":
|
||||||
|
return nil, fmt.Errorf("directory: not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
return pairs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readKeyPair(public, private string) (Pair, error) {
|
||||||
|
rk, err := os.Open(public)
|
||||||
|
if err != nil {
|
||||||
|
return Pair{}, err
|
||||||
|
}
|
||||||
|
b, err := ioutil.ReadAll(rk)
|
||||||
|
if err != nil {
|
||||||
|
return Pair{}, err
|
||||||
|
}
|
||||||
|
dnskey, err := dns.NewRR(string(b))
|
||||||
|
if err != nil {
|
||||||
|
return Pair{}, err
|
||||||
|
}
|
||||||
|
if _, ok := dnskey.(*dns.DNSKEY); !ok {
|
||||||
|
return Pair{}, fmt.Errorf("RR in %q is not a DNSKEY: %d", public, dnskey.Header().Rrtype)
|
||||||
|
}
|
||||||
|
ksk := dnskey.(*dns.DNSKEY).Flags&(1<<8) == (1<<8) && dnskey.(*dns.DNSKEY).Flags&1 == 1
|
||||||
|
if !ksk {
|
||||||
|
return Pair{}, fmt.Errorf("DNSKEY in %q is not a CSK/KSK", public)
|
||||||
|
}
|
||||||
|
|
||||||
|
rp, err := os.Open(private)
|
||||||
|
if err != nil {
|
||||||
|
return Pair{}, err
|
||||||
|
}
|
||||||
|
privkey, err := dnskey.(*dns.DNSKEY).ReadPrivateKey(rp, private)
|
||||||
|
if err != nil {
|
||||||
|
return Pair{}, err
|
||||||
|
}
|
||||||
|
switch signer := privkey.(type) {
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
return Pair{Public: dnskey.(*dns.DNSKEY), KeyTag: dnskey.(*dns.DNSKEY).KeyTag(), Private: signer}, nil
|
||||||
|
case *ed25519.PrivateKey:
|
||||||
|
return Pair{Public: dnskey.(*dns.DNSKEY), KeyTag: dnskey.(*dns.DNSKEY).KeyTag(), Private: signer}, nil
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
return Pair{Public: dnskey.(*dns.DNSKEY), KeyTag: dnskey.(*dns.DNSKEY).KeyTag(), Private: signer}, nil
|
||||||
|
default:
|
||||||
|
return Pair{}, fmt.Errorf("unsupported algorithm %s", signer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyTag returns the key tags of the keys in ps as a formatted string.
|
||||||
|
func keyTag(ps []Pair) string {
|
||||||
|
if len(ps) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
s := ""
|
||||||
|
for _, p := range ps {
|
||||||
|
s += strconv.Itoa(int(p.KeyTag)) + ","
|
||||||
|
}
|
||||||
|
return s[:len(s)-1]
|
||||||
|
}
|
||||||
5
plugin/sign/log_test.go
Normal file
5
plugin/sign/log_test.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package sign
|
||||||
|
|
||||||
|
import clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||||
|
|
||||||
|
func init() { clog.Discard() }
|
||||||
41
plugin/sign/nsec.go
Normal file
41
plugin/sign/nsec.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/coredns/coredns/plugin/file"
|
||||||
|
"github.com/coredns/coredns/plugin/file/tree"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// names returns the elements of the zone in nsec order. If the returned boolean is true there were
|
||||||
|
// no other apex records than SOA and NS, which are stored separately.
|
||||||
|
func names(origin string, z *file.Zone) ([]string, bool) {
|
||||||
|
// if there are no apex records other than NS and SOA we'll miss the origin
|
||||||
|
// in this list. Check the first element and if not origin prepend it.
|
||||||
|
n := []string{}
|
||||||
|
z.Walk(func(e *tree.Elem, _ map[uint16][]dns.RR) error {
|
||||||
|
n = append(n, e.Name())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if len(n) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if n[0] != origin {
|
||||||
|
n = append([]string{origin}, n...)
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
return n, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// NSEC returns an NSEC record according to name, next, ttl and bitmap. Note that the bitmap is sorted before use.
|
||||||
|
func NSEC(name, next string, ttl uint32, bitmap []uint16) *dns.NSEC {
|
||||||
|
sort.Slice(bitmap, func(i, j int) bool { return bitmap[i] < bitmap[j] })
|
||||||
|
|
||||||
|
return &dns.NSEC{
|
||||||
|
Hdr: dns.RR_Header{Name: name, Ttl: ttl, Rrtype: dns.TypeNSEC, Class: dns.ClassINET},
|
||||||
|
NextDomain: next,
|
||||||
|
TypeBitMap: bitmap,
|
||||||
|
}
|
||||||
|
}
|
||||||
42
plugin/sign/resign_test.go
Normal file
42
plugin/sign/resign_test.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResignInception(t *testing.T) {
|
||||||
|
then := time.Date(2019, 7, 18, 22, 50, 0, 0, time.UTC)
|
||||||
|
// signed yesterday
|
||||||
|
zr := strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190808191936 20190717161936 59725 miek.nl. eU6gI1OkSEbyt`)
|
||||||
|
if x := resign(zr, then); x != nil {
|
||||||
|
t.Errorf("Expected RRSIG to be valid for %s, got invalid: %s", then.Format(timeFmt), x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inception starts after this date.
|
||||||
|
zr = strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190808191936 20190731161936 59725 miek.nl. eU6gI1OkSEbyt`)
|
||||||
|
if x := resign(zr, then); x == nil {
|
||||||
|
t.Errorf("Expected RRSIG to be invalid for %s, got valid", then.Format(timeFmt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResignExpire(t *testing.T) {
|
||||||
|
then := time.Date(2019, 7, 18, 22, 50, 0, 0, time.UTC)
|
||||||
|
// expires tomorrow
|
||||||
|
zr := strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190717191936 20190717161936 59725 miek.nl. eU6gI1OkSEbyt`)
|
||||||
|
if x := resign(zr, then); x == nil {
|
||||||
|
t.Errorf("Expected RRSIG to be invalid for %s, got valid", then.Format(timeFmt))
|
||||||
|
}
|
||||||
|
// expire too far away
|
||||||
|
zr = strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190731191936 20190717161936 59725 miek.nl. eU6gI1OkSEbyt`)
|
||||||
|
if x := resign(zr, then); x != nil {
|
||||||
|
t.Errorf("Expected RRSIG to be valid for %s, got invalid: %s", then.Format(timeFmt), x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// expired yesterday
|
||||||
|
zr = strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190721191936 20190717161936 59725 miek.nl. eU6gI1OkSEbyt`)
|
||||||
|
if x := resign(zr, then); x == nil {
|
||||||
|
t.Errorf("Expected RRSIG to be invalid for %s, got valid", then.Format(timeFmt))
|
||||||
|
}
|
||||||
|
}
|
||||||
114
plugin/sign/setup.go
Normal file
114
plugin/sign/setup.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coredns/coredns/core/dnsserver"
|
||||||
|
"github.com/coredns/coredns/plugin"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
caddy.RegisterPlugin("sign", caddy.Plugin{
|
||||||
|
ServerType: "dns",
|
||||||
|
Action: setup,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup(c *caddy.Controller) error {
|
||||||
|
sign, err := parse(c)
|
||||||
|
if err != nil {
|
||||||
|
return plugin.Error("sign", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.OnStartup(sign.OnStartup)
|
||||||
|
c.OnStartup(func() error {
|
||||||
|
for _, signer := range sign.signers {
|
||||||
|
go signer.refresh(DurationRefreshHours)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
c.OnShutdown(func() error {
|
||||||
|
for _, signer := range sign.signers {
|
||||||
|
close(signer.stop)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Don't call AddPlugin, *sign* is not a plugin.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(c *caddy.Controller) (*Sign, error) {
|
||||||
|
sign := &Sign{}
|
||||||
|
config := dnsserver.GetConfig(c)
|
||||||
|
|
||||||
|
for c.Next() {
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
dbfile := c.Val()
|
||||||
|
if !filepath.IsAbs(dbfile) && config.Root != "" {
|
||||||
|
dbfile = filepath.Join(config.Root, dbfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
origins := make([]string, len(c.ServerBlockKeys))
|
||||||
|
copy(origins, c.ServerBlockKeys)
|
||||||
|
args := c.RemainingArgs()
|
||||||
|
if len(args) > 0 {
|
||||||
|
origins = args
|
||||||
|
}
|
||||||
|
for i := range origins {
|
||||||
|
origins[i] = plugin.Host(origins[i]).Normalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
signers := make([]*Signer, len(origins))
|
||||||
|
for i := range origins {
|
||||||
|
signers[i] = &Signer{
|
||||||
|
dbfile: dbfile,
|
||||||
|
origin: plugin.Host(origins[i]).Normalize(),
|
||||||
|
jitter: time.Duration(float32(DurationJitter) * rand.Float32()),
|
||||||
|
directory: "/var/lib/coredns",
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
signedfile: fmt.Sprintf("db.%ssigned", origins[i]), // origins[i] is a fqdn, so it ends with a dot, hence %ssigned.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for c.NextBlock() {
|
||||||
|
switch c.Val() {
|
||||||
|
case "key":
|
||||||
|
pairs, err := keyParse(c)
|
||||||
|
if err != nil {
|
||||||
|
return sign, err
|
||||||
|
}
|
||||||
|
for i := range signers {
|
||||||
|
for _, p := range pairs {
|
||||||
|
p.Public.Header().Name = signers[i].origin
|
||||||
|
}
|
||||||
|
signers[i].keys = append(signers[i].keys, pairs...)
|
||||||
|
}
|
||||||
|
case "directory":
|
||||||
|
dir := c.RemainingArgs()
|
||||||
|
if len(dir) == 0 || len(dir) > 1 {
|
||||||
|
return sign, fmt.Errorf("can only be one argument after %q", "directory")
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(dir[0]) && config.Root != "" {
|
||||||
|
dir[0] = filepath.Join(config.Root, dir[0])
|
||||||
|
}
|
||||||
|
for i := range signers {
|
||||||
|
signers[i].directory = dir[0]
|
||||||
|
signers[i].signedfile = fmt.Sprintf("db.%ssigned", signers[i].origin)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, c.Errf("unknown property '%s'", c.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sign.signers = append(sign.signers, signers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sign, nil
|
||||||
|
}
|
||||||
75
plugin/sign/setup_test.go
Normal file
75
plugin/sign/setup_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
shouldErr bool
|
||||||
|
exp *Signer
|
||||||
|
}{
|
||||||
|
{`sign testdata/db.miek.nl miek.nl {
|
||||||
|
key file testdata/Kmiek.nl.+013+59725
|
||||||
|
}`,
|
||||||
|
false,
|
||||||
|
&Signer{
|
||||||
|
keys: []Pair{},
|
||||||
|
origin: "miek.nl.",
|
||||||
|
dbfile: "testdata/db.miek.nl",
|
||||||
|
directory: "/var/lib/coredns",
|
||||||
|
signedfile: "db.miek.nl.signed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{`sign testdata/db.miek.nl example.org {
|
||||||
|
key file testdata/Kmiek.nl.+013+59725
|
||||||
|
directory testdata
|
||||||
|
}`,
|
||||||
|
false,
|
||||||
|
&Signer{
|
||||||
|
keys: []Pair{},
|
||||||
|
origin: "example.org.",
|
||||||
|
dbfile: "testdata/db.miek.nl",
|
||||||
|
directory: "testdata",
|
||||||
|
signedfile: "db.example.org.signed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// errors
|
||||||
|
{`sign db.example.org {
|
||||||
|
key file /etc/coredns/keys/Kexample.org
|
||||||
|
}`,
|
||||||
|
true,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, tc := range tests {
|
||||||
|
c := caddy.NewTestController("dns", tc.input)
|
||||||
|
sign, err := parse(c)
|
||||||
|
|
||||||
|
if err == nil && tc.shouldErr {
|
||||||
|
t.Fatalf("Test %d expected errors, but got no error", i)
|
||||||
|
}
|
||||||
|
if err != nil && !tc.shouldErr {
|
||||||
|
t.Fatalf("Test %d expected no errors, but got '%v'", i, err)
|
||||||
|
}
|
||||||
|
if tc.shouldErr {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
signer := sign.signers[0]
|
||||||
|
if x := signer.origin; x != tc.exp.origin {
|
||||||
|
t.Errorf("Test %d expected %s as origin, got %s", i, tc.exp.origin, x)
|
||||||
|
}
|
||||||
|
if x := signer.dbfile; x != tc.exp.dbfile {
|
||||||
|
t.Errorf("Test %d expected %s as dbfile, got %s", i, tc.exp.dbfile, x)
|
||||||
|
}
|
||||||
|
if x := signer.directory; x != tc.exp.directory {
|
||||||
|
t.Errorf("Test %d expected %s as directory, got %s", i, tc.exp.directory, x)
|
||||||
|
}
|
||||||
|
if x := signer.signedfile; x != tc.exp.signedfile {
|
||||||
|
t.Errorf("Test %d expected %s as signedfile, got %s", i, tc.exp.signedfile, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
plugin/sign/sign.go
Normal file
37
plugin/sign/sign.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Package sign implements a zone signer as a plugin.
|
||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sign contains signers that sign the zones files.
|
||||||
|
type Sign struct {
|
||||||
|
signers []*Signer
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnStartup scans all signers and signs or resigns zones if needed.
|
||||||
|
func (s *Sign) OnStartup() error {
|
||||||
|
for _, signer := range s.signers {
|
||||||
|
why := signer.resign()
|
||||||
|
if why == nil {
|
||||||
|
log.Infof("Skipping signing zone %q in %q: signatures are valid", signer.origin, filepath.Join(signer.directory, signer.signedfile))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go signAndLog(signer, why)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Various duration constants for signing of the zones.
|
||||||
|
const (
|
||||||
|
DurationExpireDays = 7 * 24 * time.Hour // max time allowed before expiration
|
||||||
|
DurationResignDays = 6 * 24 * time.Hour // if the last sign happenend this long ago, sign again
|
||||||
|
DurationSignatureExpireDays = 32 * 24 * time.Hour // sign for 32 days
|
||||||
|
DurationRefreshHours = 5 * time.Hour // check zones every 5 hours
|
||||||
|
DurationJitter = -18 * time.Hour // default max jitter
|
||||||
|
DurationSignatureInceptionHours = -3 * time.Hour // -(2+1) hours, be sure to catch daylight saving time and such, jitter is substracted
|
||||||
|
)
|
||||||
|
|
||||||
|
const timeFmt = "2006-01-02T15:04:05.000Z07:00"
|
||||||
222
plugin/sign/signer.go
Normal file
222
plugin/sign/signer.go
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coredns/coredns/plugin/file"
|
||||||
|
"github.com/coredns/coredns/plugin/file/tree"
|
||||||
|
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log = clog.NewWithPlugin("sign")
|
||||||
|
|
||||||
|
// Signer holds the data needed to sign a zone file.
|
||||||
|
type Signer struct {
|
||||||
|
keys []Pair
|
||||||
|
origin string
|
||||||
|
dbfile string
|
||||||
|
directory string
|
||||||
|
jitter time.Duration
|
||||||
|
|
||||||
|
signedfile string
|
||||||
|
stop chan struct{}
|
||||||
|
|
||||||
|
expiration uint32
|
||||||
|
inception uint32
|
||||||
|
ttl uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs a zone file according to the parameters in s.
|
||||||
|
func (s *Signer) Sign(now time.Time) (*file.Zone, error) {
|
||||||
|
rd, err := os.Open(s.dbfile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
z, err := Parse(rd, s.origin, s.dbfile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.inception, s.expiration = lifetime(now, s.jitter)
|
||||||
|
|
||||||
|
s.ttl = z.Apex.SOA.Header().Ttl
|
||||||
|
z.Apex.SOA.Serial = uint32(now.Unix())
|
||||||
|
|
||||||
|
for _, pair := range s.keys {
|
||||||
|
pair.Public.Header().Ttl = s.ttl // set TTL on key so it matches the RRSIG.
|
||||||
|
z.Insert(pair.Public)
|
||||||
|
z.Insert(pair.Public.ToDS(dns.SHA1))
|
||||||
|
z.Insert(pair.Public.ToDS(dns.SHA256))
|
||||||
|
z.Insert(pair.Public.ToDS(dns.SHA1).ToCDS())
|
||||||
|
z.Insert(pair.Public.ToDS(dns.SHA256).ToCDS())
|
||||||
|
z.Insert(pair.Public.ToCDNSKEY())
|
||||||
|
}
|
||||||
|
|
||||||
|
names, apex := names(s.origin, z)
|
||||||
|
ln := len(names)
|
||||||
|
|
||||||
|
var nsec *dns.NSEC
|
||||||
|
if apex {
|
||||||
|
nsec = NSEC(s.origin, names[(ln+1)%ln], s.ttl, []uint16{dns.TypeSOA, dns.TypeNS, dns.TypeRRSIG, dns.TypeNSEC})
|
||||||
|
z.Insert(nsec)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pair := range s.keys {
|
||||||
|
rrsig, err := pair.signRRs([]dns.RR{z.Apex.SOA}, s.origin, s.ttl, s.inception, s.expiration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
z.Insert(rrsig)
|
||||||
|
// NS apex may not be set if RR's have been discarded because the origin doesn't match.
|
||||||
|
if len(z.Apex.NS) > 0 {
|
||||||
|
rrsig, err = pair.signRRs(z.Apex.NS, s.origin, s.ttl, s.inception, s.expiration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
z.Insert(rrsig)
|
||||||
|
}
|
||||||
|
if apex {
|
||||||
|
rrsig, err = pair.signRRs([]dns.RR{nsec}, s.origin, s.ttl, s.inception, s.expiration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
z.Insert(rrsig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are walking the tree in the same direction, so names[] can be used here to indicated the next element.
|
||||||
|
i := 1
|
||||||
|
err = z.Walk(func(e *tree.Elem, zrrs map[uint16][]dns.RR) error {
|
||||||
|
if !apex && e.Name() == s.origin {
|
||||||
|
nsec := NSEC(e.Name(), names[(ln+i)%ln], s.ttl, append(e.Types(), dns.TypeNS, dns.TypeSOA, dns.TypeNSEC, dns.TypeRRSIG))
|
||||||
|
z.Insert(nsec)
|
||||||
|
} else {
|
||||||
|
nsec := NSEC(e.Name(), names[(ln+i)%ln], s.ttl, append(e.Types(), dns.TypeNSEC, dns.TypeRRSIG))
|
||||||
|
z.Insert(nsec)
|
||||||
|
}
|
||||||
|
|
||||||
|
for t, rrs := range zrrs {
|
||||||
|
if t == dns.TypeRRSIG {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, pair := range s.keys {
|
||||||
|
rrsig, err := pair.signRRs(rrs, s.origin, s.ttl, s.inception, s.expiration)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.Insert(rrsig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return z, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// resign checks if the signed zone exists, or needs resigning.
|
||||||
|
func (s *Signer) resign() error {
|
||||||
|
signedfile := filepath.Join(s.directory, s.signedfile)
|
||||||
|
rd, err := os.Open(signedfile)
|
||||||
|
if err != nil && os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
return resign(rd, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resign will scan rd and check the signature on the SOA record. We will resign on the basis
|
||||||
|
// of 2 conditions:
|
||||||
|
// * either the inception is more than 6 days ago, or
|
||||||
|
// * we only have 1 week left on the signature
|
||||||
|
//
|
||||||
|
// All SOA signatures will be checked. If the SOA isn't found in the first 100
|
||||||
|
// records, we will resign the zone.
|
||||||
|
func resign(rd io.Reader, now time.Time) (why error) {
|
||||||
|
zp := dns.NewZoneParser(rd, ".", "resign")
|
||||||
|
zp.SetIncludeAllowed(true)
|
||||||
|
i := 0
|
||||||
|
|
||||||
|
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
|
||||||
|
if err := zp.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch x := rr.(type) {
|
||||||
|
case *dns.RRSIG:
|
||||||
|
if x.TypeCovered != dns.TypeSOA {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
incep, _ := time.Parse("20060102150405", dns.TimeToString(x.Inception))
|
||||||
|
// If too long ago, resign.
|
||||||
|
if now.Sub(incep) >= 0 && now.Sub(incep) > DurationResignDays {
|
||||||
|
return fmt.Errorf("inception %q was more than: %s ago from %s: %s", incep.Format(timeFmt), DurationResignDays, now.Format(timeFmt), now.Sub(incep))
|
||||||
|
}
|
||||||
|
// Inception hasn't even start yet.
|
||||||
|
if now.Sub(incep) < 0 {
|
||||||
|
return fmt.Errorf("inception %q date is in the future: %s", incep.Format(timeFmt), now.Sub(incep))
|
||||||
|
}
|
||||||
|
|
||||||
|
expire, _ := time.Parse("20060102150405", dns.TimeToString(x.Expiration))
|
||||||
|
if expire.Sub(now) < DurationExpireDays {
|
||||||
|
return fmt.Errorf("expiration %q is less than: %s away from %s: %s", expire.Format(timeFmt), DurationExpireDays, now.Format(timeFmt), expire.Sub(now))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
if i > 100 {
|
||||||
|
// 100 is a random number. A SOA record should be the first in the zonefile, but RFC 1035 doesn't actually mandate this. So it could
|
||||||
|
// be 3rd or even later. The number 100 looks crazy high enough that it will catch all weird zones, but not high enough to keep the CPU
|
||||||
|
// busy with parsing all the time.
|
||||||
|
return fmt.Errorf("no SOA RRSIG found in first 100 records")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func signAndLog(s *Signer, why error) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
z, err := s.Sign(now)
|
||||||
|
log.Infof("Signing %q because %s", s.origin, why)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("Error signing %q with key tags %q in %s: %s, next: %s", s.origin, keyTag(s.keys), time.Since(now), err, now.Add(DurationRefreshHours).Format(timeFmt))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.write(z); err != nil {
|
||||||
|
log.Warningf("Error signing %q: failed to move zone file into place: %s", s.origin, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Infof("Successfully signed zone %q in %q with key tags %q and %d SOA serial, elapsed %f, next: %s", s.origin, filepath.Join(s.directory, s.signedfile), keyTag(s.keys), z.Apex.SOA.Serial, time.Since(now).Seconds(), now.Add(DurationRefreshHours).Format(timeFmt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh checks every val if some zones need to be resigned.
|
||||||
|
func (s *Signer) refresh(val time.Duration) {
|
||||||
|
tick := time.NewTicker(val)
|
||||||
|
defer tick.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.stop:
|
||||||
|
return
|
||||||
|
case <-tick.C:
|
||||||
|
why := s.resign()
|
||||||
|
if why == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
signAndLog(s, why)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lifetime(now time.Time, jitter time.Duration) (uint32, uint32) {
|
||||||
|
incep := uint32(now.Add(DurationSignatureInceptionHours).Add(jitter).Unix())
|
||||||
|
expir := uint32(now.Add(DurationSignatureExpireDays).Unix())
|
||||||
|
return incep, expir
|
||||||
|
}
|
||||||
102
plugin/sign/signer_test.go
Normal file
102
plugin/sign/signer_test.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSign(t *testing.T) {
|
||||||
|
input := `sign testdata/db.miek.nl miek.nl {
|
||||||
|
key file testdata/Kmiek.nl.+013+59725
|
||||||
|
directory testdata
|
||||||
|
}`
|
||||||
|
c := caddy.NewTestController("dns", input)
|
||||||
|
sign, err := parse(c)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(sign.signers) != 1 {
|
||||||
|
t.Fatalf("Expected 1 signer, got %d", len(sign.signers))
|
||||||
|
}
|
||||||
|
z, err := sign.signers[0].Sign(time.Now().UTC())
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apex, _ := z.Search("miek.nl.")
|
||||||
|
if x := apex.Type(dns.TypeDS); len(x) != 2 {
|
||||||
|
t.Errorf("Expected %d DS records, got %d", 2, len(x))
|
||||||
|
}
|
||||||
|
if x := apex.Type(dns.TypeCDS); len(x) != 2 {
|
||||||
|
t.Errorf("Expected %d CDS records, got %d", 2, len(x))
|
||||||
|
}
|
||||||
|
if x := apex.Type(dns.TypeCDNSKEY); len(x) != 1 {
|
||||||
|
t.Errorf("Expected %d CDNSKEY record, got %d", 1, len(x))
|
||||||
|
}
|
||||||
|
if x := apex.Type(dns.TypeDNSKEY); len(x) != 1 {
|
||||||
|
t.Errorf("Expected %d DNSKEY record, got %d", 1, len(x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignApexZone(t *testing.T) {
|
||||||
|
apex := `$TTL 30M
|
||||||
|
$ORIGIN example.org.
|
||||||
|
@ IN SOA linode miek.miek.nl. ( 1282630060 4H 1H 7D 4H )
|
||||||
|
IN NS linode
|
||||||
|
`
|
||||||
|
if err := ioutil.WriteFile("db.apex-test.example.org", []byte(apex), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove("db.apex-test.example.org")
|
||||||
|
input := `sign db.apex-test.example.org example.org {
|
||||||
|
key file testdata/Kmiek.nl.+013+59725
|
||||||
|
directory testdata
|
||||||
|
}`
|
||||||
|
c := caddy.NewTestController("dns", input)
|
||||||
|
sign, err := parse(c)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
z, err := sign.signers[0].Sign(time.Now().UTC())
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
el, _ := z.Search("example.org.")
|
||||||
|
nsec := el.Type(dns.TypeNSEC)
|
||||||
|
if len(nsec) != 1 {
|
||||||
|
t.Errorf("Expected 1 NSEC for %s, got %d", "example.org.", len(nsec))
|
||||||
|
}
|
||||||
|
if x := nsec[0].(*dns.NSEC).NextDomain; x != "example.org." {
|
||||||
|
t.Errorf("Expected NSEC NextDomain %s, got %s", "example.org.", x)
|
||||||
|
}
|
||||||
|
if x := nsec[0].(*dns.NSEC).TypeBitMap; len(x) != 8 {
|
||||||
|
t.Errorf("Expected NSEC bitmap to be %d elements, got %d", 8, x)
|
||||||
|
}
|
||||||
|
if x := nsec[0].(*dns.NSEC).TypeBitMap; x[7] != dns.TypeCDNSKEY {
|
||||||
|
t.Errorf("Expected NSEC bitmap element 6 to be %d, got %d", dns.TypeCDNSKEY, x[7])
|
||||||
|
}
|
||||||
|
if x := nsec[0].(*dns.NSEC).TypeBitMap; x[5] != dns.TypeDNSKEY {
|
||||||
|
t.Errorf("Expected NSEC bitmap element 5 to be %d, got %d", dns.TypeDNSKEY, x[5])
|
||||||
|
}
|
||||||
|
dnskey := el.Type(dns.TypeDNSKEY)
|
||||||
|
if x := dnskey[0].Header().Ttl; x != 1800 {
|
||||||
|
t.Errorf("Expected DNSKEY TTL to be %d, got %d", 1800, x)
|
||||||
|
}
|
||||||
|
sigs := el.Type(dns.TypeRRSIG)
|
||||||
|
for _, s := range sigs {
|
||||||
|
if s.(*dns.RRSIG).TypeCovered == dns.TypeDNSKEY {
|
||||||
|
if s.(*dns.RRSIG).OrigTtl != dnskey[0].Header().Ttl {
|
||||||
|
t.Errorf("Expected RRSIG original TTL to match DNSKEY TTL, but %d != %d", s.(*dns.RRSIG).OrigTtl, dnskey[0].Header().Ttl)
|
||||||
|
}
|
||||||
|
if s.(*dns.RRSIG).SignerName != dnskey[0].Header().Name {
|
||||||
|
t.Errorf("Expected RRSIG signer name to match DNSKEY ownername, but %s != %s", s.(*dns.RRSIG).SignerName, dnskey[0].Header().Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
plugin/sign/testdata/Kmiek.nl.+013+59725.key
vendored
Normal file
5
plugin/sign/testdata/Kmiek.nl.+013+59725.key
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
; This is a key-signing key, keyid 59725, for miek.nl.
|
||||||
|
; Created: 20190709192036 (Tue Jul 9 20:20:36 2019)
|
||||||
|
; Publish: 20190709192036 (Tue Jul 9 20:20:36 2019)
|
||||||
|
; Activate: 20190709192036 (Tue Jul 9 20:20:36 2019)
|
||||||
|
miek.nl. IN DNSKEY 257 3 13 sfzRg5nDVxbeUc51su4MzjgwpOpUwnuu81SlRHqJuXe3SOYOeypR69tZ 52XLmE56TAmPHsiB8Rgk+NTpf0o1Cw==
|
||||||
6
plugin/sign/testdata/Kmiek.nl.+013+59725.private
vendored
Normal file
6
plugin/sign/testdata/Kmiek.nl.+013+59725.private
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Private-key-format: v1.3
|
||||||
|
Algorithm: 13 (ECDSAP256SHA256)
|
||||||
|
PrivateKey: rm7EdHRca//6xKpJzeoLt/mrfgQnltJ0WpQGtOG59yo=
|
||||||
|
Created: 20190709192036
|
||||||
|
Publish: 20190709192036
|
||||||
|
Activate: 20190709192036
|
||||||
17
plugin/sign/testdata/db.miek.nl
vendored
Normal file
17
plugin/sign/testdata/db.miek.nl
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
$TTL 30M
|
||||||
|
$ORIGIN miek.nl.
|
||||||
|
@ IN SOA linode.atoom.net. miek.miek.nl. ( 1282630060 4H 1H 7D 4H )
|
||||||
|
IN NS linode.atoom.net.
|
||||||
|
IN MX 1 aspmx.l.google.com.
|
||||||
|
IN AAAA 2a01:7e00::f03c:91ff:fe79:234c
|
||||||
|
IN DNSKEY 257 3 13 sfzRg5nDVxbeUc51su4MzjgwpOpUwnuu81SlRHqJuXe3SOYOeypR69tZ52XLmE56TAmPHsiB8Rgk+NTpf0o1Cw==
|
||||||
|
|
||||||
|
a IN AAAA 2a01:7e00::f03c:91ff:fe79:234c
|
||||||
|
www IN CNAME a
|
||||||
|
|
||||||
|
|
||||||
|
bla IN NS ns1.bla.com.
|
||||||
|
ns3.blaaat.miek.nl. IN AAAA ::1 ; non-glue, should be signed.
|
||||||
|
; in baliwick nameserver that requires glue, should not be signed
|
||||||
|
bla IN NS ns2.bla.miek.nl.
|
||||||
|
ns2.bla.miek.nl. IN A 127.0.0.1
|
||||||
Reference in New Issue
Block a user