first commit

This commit is contained in:
Christian Joergensen 2018-10-04 12:36:49 +02:00
commit 5f709d4a52
6 changed files with 372 additions and 0 deletions

72
README.md Normal file
View 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
View File

@ -0,0 +1,9 @@
[[records]]
zone = "ietf.org"
record = "@"
type = "SOA"
[[records]]
zone = "verisigninc.com"
record = "@"
type = "SOA"

19
dnssec.rules Normal file
View 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
View 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
View 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
View 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))
}