first commit
This commit is contained in:
commit
5f709d4a52
72
README.md
Normal file
72
README.md
Normal file
@ -0,0 +1,72 @@
|
||||
# DNSSEC Exporter for Prometheus
|
||||
|
||||
Check for validity and expiration in DNSSEC signatures and expose metrics for Prometheus
|
||||
|
||||
## Installation
|
||||
|
||||
$ go get -u github.com/chrj/prometheus-dnssec-exporter
|
||||
|
||||
## Usage
|
||||
|
||||
Usage of prometheus-dnssec-exporter:
|
||||
-config string
|
||||
Configuration file (default "/etc/dnssec-checks")
|
||||
-listen-address string
|
||||
Prometheus metrics port (default ":9204")
|
||||
-resolver string
|
||||
Resolver to use (default "8.8.8.8:53")
|
||||
-timeout duration
|
||||
Timeout for network operations (default 10s)
|
||||
|
||||
## Metrics
|
||||
|
||||
### Gauge: `dnssec_zone_record_days_left`
|
||||
|
||||
Number of days the signature will be valid.
|
||||
|
||||
Labels:
|
||||
|
||||
* `zone`
|
||||
* `record`
|
||||
* `type`
|
||||
|
||||
### Gauge: `dnssec_zone_record_valid`
|
||||
|
||||
Does this record pass DNSSEC validation.
|
||||
|
||||
Labels:
|
||||
|
||||
* `zone`
|
||||
* `record`
|
||||
* `type`
|
||||
|
||||
### Examples
|
||||
|
||||
# HELP dnssec_zone_record_days_left Number of days the signature will be valid
|
||||
# TYPE dnssec_zone_record_days_left gauge
|
||||
dnssec_zone_record_days_left{record="@",type="SOA",zone="ietf.org"} 357.5
|
||||
dnssec_zone_record_days_left{record="@",type="SOA",zone="verisigninc.com"} 11.333333333333334
|
||||
# HELP dnssec_zone_record_valid Does this record pass DNSSEC validation
|
||||
# TYPE dnssec_zone_record_valid gauge
|
||||
dnssec_zone_record_valid{record="@",type="SOA",zone="ietf.org"} 1
|
||||
dnssec_zone_record_valid{record="@",type="SOA",zone="verisigninc.com"} 1
|
||||
|
||||
## Configuration
|
||||
|
||||
Supply a configuration file path with `-config` (optionally, defaults to `/etc/dnssec-checks`). Uses [TOML](https://github.com/toml-lang/toml).
|
||||
|
||||
[Sample configuration file](config.sample)
|
||||
|
||||
## Prometheus target
|
||||
|
||||
Supply a listen address with `-addr` (optionally, defaults to `:9204`), and configure a Prometheus job:
|
||||
|
||||
- job_name: "dnssec"
|
||||
scrape_interval: "1m"
|
||||
static_configs:
|
||||
- targets:
|
||||
- "server:9204"
|
||||
|
||||
## Prometheus alert
|
||||
|
||||
The real benefit is getting an alert triggered when a signature is nearing expiration or is not longer valid. Check this [sample alert definition](dnssec.rules).
|
9
config.sample
Normal file
9
config.sample
Normal file
@ -0,0 +1,9 @@
|
||||
[[records]]
|
||||
zone = "ietf.org"
|
||||
record = "@"
|
||||
type = "SOA"
|
||||
|
||||
[[records]]
|
||||
zone = "verisigninc.com"
|
||||
record = "@"
|
||||
type = "SOA"
|
19
dnssec.rules
Normal file
19
dnssec.rules
Normal file
@ -0,0 +1,19 @@
|
||||
groups:
|
||||
- name: prometheus/alerts/dnssec.rules
|
||||
rules:
|
||||
- alert: DNSSECSignatureExpiration
|
||||
expr: dnssec_zone_record_days_left < 10
|
||||
for: 1m
|
||||
labels:
|
||||
urgency: immediate
|
||||
annotations:
|
||||
description: The DNSSEC signature for the {{$labels.record}} in {{$labels.zone}} type {{$labels.type}}) expires in {{$value}} day(s)
|
||||
title: The DNSSEC signature for the {{$labels.record}} in {{$labels.zone}} is expiring
|
||||
- alert: DNSSECSignatureInvalid
|
||||
expr: dnssec_zone_record_valid == 0
|
||||
for: 1m
|
||||
labels:
|
||||
urgency: immediate
|
||||
annotations:
|
||||
description: The DNSSEC signature for the {{$labels.record}} in {{$labels.zone}} type {{$labels.type}}) is invalid
|
||||
title: The DNSSEC signature for the {{$labels.record}} in {{$labels.zone}} is invalid
|
17
go.mod
Normal file
17
go.mod
Normal file
@ -0,0 +1,17 @@
|
||||
module github.com/chrj/prometheus-dnssec-exporter
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect
|
||||
github.com/golang/protobuf v1.2.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/miekg/dns v1.0.12
|
||||
github.com/naoina/go-stringutil v0.1.0 // indirect
|
||||
github.com/naoina/toml v0.1.1
|
||||
github.com/prometheus/client_golang v0.8.0
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e // indirect
|
||||
github.com/prometheus/procfs v0.0.0-20180920065004-418d78d0b9a7 // indirect
|
||||
golang.org/x/crypto v0.0.0-20180927165925-5295e8364332 // indirect
|
||||
golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3 // indirect
|
||||
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 // indirect
|
||||
)
|
26
go.sum
Normal file
26
go.sum
Normal file
@ -0,0 +1,26 @@
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.12 h1:814rTNaw7Q7pGncpSEDT06YS8rdGmpUEnKgpQzctJsk=
|
||||
github.com/miekg/dns v1.0.12/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks=
|
||||
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
|
||||
github.com/naoina/toml v0.1.1 h1:PT/lllxVVN0gzzSqSlHEmP8MJB4MY2U7STGxiouV4X8=
|
||||
github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
|
||||
github.com/prometheus/client_golang v0.8.0 h1:1921Yw9Gc3iSc4VQh3PIoOqgPCZS7G/4xQNVUp8Mda8=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e h1:n/3MEhJQjQxrOUCzh1Y3Re6aJUUWRp2M9+Oc3eVn/54=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180920065004-418d78d0b9a7 h1:NgR6WN8nQ4SmFC1sSUHY8SriLuWCZ6cCIQtH4vDZN3c=
|
||||
github.com/prometheus/procfs v0.0.0-20180920065004-418d78d0b9a7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
golang.org/x/crypto v0.0.0-20180927165925-5295e8364332 h1:hvQVdF6P9DX4OiKA5tpehlG6JsgzmyQiThG7q5Bn3UQ=
|
||||
golang.org/x/crypto v0.0.0-20180927165925-5295e8364332/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3 h1:dgd4x4kJt7G4k4m93AYLzM8Ni6h2qLTfh9n9vXJT3/0=
|
||||
golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 h1:O33LKL7WyJgjN9CvxfTIomjIClbd/Kq86/iipowHQU0=
|
||||
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
229
main.go
Normal file
229
main.go
Normal file
@ -0,0 +1,229 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/naoina/toml"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
var addr = flag.String("listen-address", ":9204", "Prometheus metrics port")
|
||||
var conf = flag.String("config", "/etc/dnssec-checks", "Configuration file")
|
||||
var resolver = flag.String("resolver", "8.8.8.8:53", "Resolver to use")
|
||||
var timeout = flag.Duration("timeout", 10*time.Second, "Timeout for network operations")
|
||||
|
||||
var dnsClient *dns.Client
|
||||
|
||||
type Records struct {
|
||||
Zone string
|
||||
Record string
|
||||
Type string
|
||||
}
|
||||
|
||||
type Exporter struct {
|
||||
Records []Records
|
||||
|
||||
records *prometheus.GaugeVec
|
||||
valid *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
func NewDNSSECExporter() *Exporter {
|
||||
return &Exporter{
|
||||
records: prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: "dnssec",
|
||||
Subsystem: "zone",
|
||||
Name: "record_days_left",
|
||||
Help: "Number of days the signature will be valid",
|
||||
},
|
||||
[]string{
|
||||
"zone",
|
||||
"record",
|
||||
"type",
|
||||
},
|
||||
),
|
||||
valid: prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: "dnssec",
|
||||
Subsystem: "zone",
|
||||
Name: "record_valid",
|
||||
Help: "Does this record pass DNSSEC validation",
|
||||
},
|
||||
[]string{
|
||||
"zone",
|
||||
"record",
|
||||
"type",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
|
||||
e.records.Describe(ch)
|
||||
e.valid.Describe(ch)
|
||||
}
|
||||
|
||||
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(len(e.Records))
|
||||
|
||||
for _, rec := range e.Records {
|
||||
|
||||
rec := rec
|
||||
|
||||
go func() {
|
||||
e.collectRecord(rec.Zone, rec.Record, rec.Type)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
e.records.Collect(ch)
|
||||
e.valid.Collect(ch)
|
||||
|
||||
}
|
||||
|
||||
func (e *Exporter) collectRecord(zone, record, recordType string) {
|
||||
|
||||
// Start by finding the DNSKEY
|
||||
|
||||
msg := &dns.Msg{}
|
||||
msg.SetQuestion(fmt.Sprintf("%s.", zone), dns.TypeDNSKEY)
|
||||
|
||||
response, _, err := dnsClient.Exchange(msg, *resolver)
|
||||
if err != nil {
|
||||
log.Printf("while looking up DNSKEY for %v: %v", zone, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Found keys are mapped by tag -> key
|
||||
keys := make(map[uint16]*dns.DNSKEY)
|
||||
|
||||
for _, rr := range response.Answer {
|
||||
if dnskey, ok := rr.(*dns.DNSKEY); ok && dnskey.Flags&dns.ZONE != 0 {
|
||||
keys[dnskey.KeyTag()] = dnskey
|
||||
}
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
log.Printf("didn't find DNSKEY for %v", zone)
|
||||
}
|
||||
|
||||
// Now lookup the signature
|
||||
|
||||
msg = &dns.Msg{}
|
||||
msg.SetQuestion(hostname(zone, record), dns.TypeRRSIG)
|
||||
|
||||
response, _, err = dnsClient.Exchange(msg, *resolver)
|
||||
if err != nil {
|
||||
log.Printf("while looking up RRSIG for %v: %v", hostname(zone, record), err)
|
||||
return
|
||||
}
|
||||
|
||||
var sig *dns.RRSIG
|
||||
var key *dns.DNSKEY
|
||||
|
||||
for _, rr := range response.Answer {
|
||||
if rrsig, ok := rr.(*dns.RRSIG); ok {
|
||||
|
||||
if rrsig.TypeCovered == dns.StringToType[recordType] &&
|
||||
keys[rrsig.KeyTag] != nil {
|
||||
|
||||
sig = rrsig
|
||||
key = keys[rrsig.KeyTag]
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if sig == nil {
|
||||
log.Printf("didn't find RRSIG for %v covering type %v", hostname(zone, record), recordType)
|
||||
return
|
||||
}
|
||||
|
||||
exp := time.Unix(int64(sig.Expiration), 0)
|
||||
if exp.IsZero() {
|
||||
log.Print("zero exp")
|
||||
return
|
||||
}
|
||||
|
||||
e.records.WithLabelValues(
|
||||
zone, record, recordType,
|
||||
).Set(float64(time.Until(exp)/time.Hour) / 24)
|
||||
|
||||
// Finally, lookup the records to validate
|
||||
|
||||
if key == nil {
|
||||
e.valid.WithLabelValues(zone, record, recordType).Set(0)
|
||||
return
|
||||
}
|
||||
|
||||
msg = &dns.Msg{}
|
||||
msg.SetQuestion(hostname(zone, record), dns.StringToType[recordType])
|
||||
|
||||
response, _, err = dnsClient.Exchange(msg, *resolver)
|
||||
if err != nil {
|
||||
log.Printf("while looking up RRSet for %v type %v: %v", hostname(zone, record), recordType, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sig.Verify(key, response.Answer); err == nil {
|
||||
e.valid.WithLabelValues(zone, record, recordType).Set(1)
|
||||
} else {
|
||||
log.Printf("verify error for %v type %v): %v", hostname(zone, record), recordType, err)
|
||||
e.valid.WithLabelValues(zone, record, recordType).Set(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func hostname(zone, record string) string {
|
||||
|
||||
if record == "@" {
|
||||
return fmt.Sprintf("%s.", zone)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s.%s.", record, zone)
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
flag.Parse()
|
||||
|
||||
dnsClient = &dns.Client{
|
||||
Net: "tcp",
|
||||
Timeout: *timeout,
|
||||
}
|
||||
|
||||
f, err := os.Open(*conf)
|
||||
if err != nil {
|
||||
log.Fatalf("couldn't open configuration file: %v", err)
|
||||
}
|
||||
|
||||
exporter := NewDNSSECExporter()
|
||||
|
||||
if err := toml.NewDecoder(f).Decode(exporter); err != nil {
|
||||
log.Fatalf("couldn't parse configuration file: %v", err)
|
||||
}
|
||||
|
||||
prometheus.MustRegister(exporter)
|
||||
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
log.Fatal(http.ListenAndServe(*addr, nil))
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user