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