1
0
mirror of https://github.com/ribbybibby/ssl_exporter.git synced 2024-11-24 08:22:17 +02:00

move metadata out of metrics and into labels

This commit is contained in:
Rob Best 2020-01-24 17:37:37 +00:00
parent 000c8a8907
commit 11e3e4c216
3 changed files with 125 additions and 129 deletions

136
README.md
View File

@ -1,10 +1,22 @@
# SSL Certificate Exporter
The [blackbox_exporter](https://github.com/prometheus/blackbox_exporter) allows you to test the expiry date of a certificate as part of its HTTP(S) probe - which is great. It doesn't, however, tell you which certificate in the chain is nearing expiry or give you any other information that might be useful when sending alerts.
The [blackbox_exporter](https://github.com/prometheus/blackbox_exporter) allows
you to test the expiry date of a certificate as part of its HTTP(S) probe -
which is great. It doesn't, however, tell you which certificate in the chain is
nearing expiry or give you any other information that might be useful when
sending alerts.
For instance, there's a definite value in knowing, upon first receiving an alert, if it's a certificate you manage directly or one further up the chain. It's also not always necessarily clear from the address you're polling what kind of certificate renewal you're looking at. Is it a Let's Encrypt, in which case it should be handled by automation? Or your organisation's wildcard? Maybe the domain is managed by a third-party and you need to submit a ticket to get it renewed.
For instance, there's a definite value in knowing, upon first receiving an
alert, if it's a certificate you manage directly or one further up the chain.
It's also not always necessarily clear from the address you're polling what kind
of certificate renewal you're looking at. Is it a Let's Encrypt, in which case
it should be handled by automation? Or your organisation's wildcard? Maybe the
domain is managed by a third-party and you need to submit a ticket to get it
renewed.
Whatever it is, the SSL exporter gives you visibility over those dimensions at the point at which you receive an alert. It also allows you to produce more meaningful visualisations and consoles.
Whatever it is, the SSL exporter gives you visibility over those dimensions at
the point at which you receive an alert. It also allows you to produce more
meaningful visualisations and consoles.
## Table of Contents
@ -31,7 +43,10 @@ Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
make
./ssl_exporter <flags>
Similarly to the blackbox_exporter, visiting [http://localhost:9219/probe?target=example.com:443](http://localhost:9219/probe?target=example.com:443) will return certificate metrics for example.com. The `ssl_tls_connect_success` metric indicates if the probe has been successful.
Similarly to the blackbox_exporter, visiting
[http://localhost:9219/probe?target=example.com:443](http://localhost:9219/probe?target=example.com:443)
will return certificate metrics for example.com. The `ssl_tls_connect_success`
metric indicates if the probe has been successful.
## Docker
@ -42,38 +57,43 @@ Similarly to the blackbox_exporter, visiting [http://localhost:9219/probe?target
./ssl_exporter --help
- **`--tls.insecure`:** Skip certificate verification (default false). This is insecure but does allow you to collect metrics in the case where a certificate has expired. That being said, I feel that it's more important to catch verification failures than it is to identify an expired certificate, especially as the former includes the latter.
- **`--tls.cacert`:** Provide the path to an alternative bundle of root CA certificates. By default the exporter will use the host's root CA set.
- **`--tls.client-auth`:** Enable client authentication (default false). When enabled the exporter will present the certificate and key configured by `--tls.cert` and `tls.key` to the other side of the connection.
- **`--tls.cert`:** The path to a local certificate for client authentication (default "cert.pem"). Only used when `--tls.client-auth` is toggled on.
- **`--tls.key`:** The path to a local key for client authentication (default "key.pem"). Only used when `--tls.client-auth` is toggled on.
- **`--tls.insecure`:** Skip certificate verification (default false). This is
insecure but does allow you to collect metrics in the case where a certificate
has expired. That being said, I feel that it's more important to catch
verification failures than it is to identify an expired certificate,
especially as the former includes the latter.
- **`--tls.cacert`:** Provide the path to an alternative bundle of root CA
certificates. By default the exporter will use the host's root CA set.
- **`--tls.client-auth`:** Enable client authentication (default false). When
enabled the exporter will present the certificate and key configured by
`--tls.cert` and `tls.key` to the other side of the connection.
- **`--tls.cert`:** The path to a local certificate for client authentication
(default "cert.pem"). Only used when `--tls.client-auth` is toggled on.
- **`--tls.key`:** The path to a local key for client authentication (default
"key.pem"). Only used when `--tls.client-auth` is toggled on.
- **`--web.listen-address`:** The port (default ":9219").
- **`--web.metrics-path`:** The path metrics are exposed under (default "/metrics")
- **`--web.probe-path`:** The path the probe endpoint is exposed under (default "/probe")
- **`--web.metrics-path`:** The path metrics are exposed under (default
"/metrics")
- **`--web.probe-path`:** The path the probe endpoint is exposed under (default
"/probe")
## Metrics
Metrics are exported for each certificate in the chain individually. All of the metrics are labelled with the Issuer's Common Name and the Serial ID, which is pretty much a unique identifier.
I considered having a series for each `ssl_cert_subject_alternative_*` value but these labels aren't actually very cardinal, considering the most frequently they'll change is probably every three months, which is longer than most metric retention times anyway. Joining them within commas as I've done allows for easy parsing and relabelling.
| Metric | Meaning | Labels |
| ------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------- |
| ssl_cert_not_after | The date after which the certificate expires. Expressed as a Unix Epoch Time. | issuer_cn, serial_no |
| ssl_cert_not_before | The date before which the certificate is not valid. Expressed as a Unix Epoch Time. | issuer_cn, serial_no |
| ssl_cert_subject_common_name | The common name of the certificate. Always has a value of 1 | issuer_cn, serial_no, subject_cn |
| ssl_cert_subject_alternative_dnsnames | The subject alternative names (if any). Always has a value of 1 | issuer_cn, serial_no, dnsnames |
| ssl_cert_subject_alternative_emails | The subject alternative email addresses (if any). Always has a value of 1 | issuer_cn, serial_no, emails |
| ssl_cert_subject_alternative_ips | The subject alternative IP addresses (if any). Always has a value of 1 | issuer_cn, serial_no, ips |
| ssl_cert_subject_organization_units | The subject organization names (if any). Always has a value of 1. | issuer_cn, serial_no, subject_ou |
| ssl_client_protocol | The protocol used by the exporter to connect to the target. Boolean. | protocol |
| ssl_tls_connect_success | Was the TLS connection successful? Boolean. | |
| Metric | Meaning | Labels |
| ----------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------- |
| ssl_cert_not_after | The date after which the certificate expires. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou |
| ssl_cert_not_before | The date before which the certificate is not valid. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou |
| ssl_client_protocol | The protocol used by the exporter to connect to the target. Boolean. | protocol |
| ssl_tls_connect_success | Was the TLS connection successful? Boolean. | |
## Prometheus
### Configuration
Just like with the blackbox_exporter, you should pass the targets to a single instance of the exporter in a scrape config with a clever bit of relabelling. This allows you to leverage service discovery and keeps configuration centralised to your Prometheus config.
Just like with the blackbox_exporter, you should pass the targets to a single
instance of the exporter in a scrape config with a clever bit of relabelling.
This allows you to leverage service discovery and keeps configuration
centralised to your Prometheus config.
```yml
scrape_configs:
@ -94,17 +114,21 @@ scrape_configs:
### Targets
The exporter uses the provided uri to decide which client (http or tcp) to use when connecting to the target. The uri must contain
either a protocol scheme (`https://`), a port (`:443`), or both (`https://example.com:443`).
The exporter uses the provided uri to decide which client (http or tcp) to use
when connecting to the target. The uri must contain either a protocol scheme
(`https://`), a port (`:443`), or both (`https://example.com:443`).
If the `https://` scheme is provided then the exporter will use a http client to connect to the target. This allows you to take
advatange of some features not available when using tcp, like host-based proxying. The exporter doesn't understand any other L7
protocols, so it will produce an error for others, like `ldaps://` or `ftps://`.
If the `https://` scheme is provided then the exporter will use a http client to
connect to the target. This allows you to take advantage of some features not
available when using tcp, like host-based proxying. The exporter doesn't
understand any other L7 protocols, so it will produce an error for others, like
`ldaps://` or `ftps://`.
If there's only a port, then a tcp client is used to make the TLS connection. This should allow you to connect to any TLS target, regardless
of L7 protocol.
If there's only a port, then a tcp client is used to make the TLS connection.
This should allow you to connect to any TLS target, regardless of L7 protocol.
If neither are given, the exporter assumes a https connection on port `443` (the most common case).
If neither are given, the exporter assumes a https connection on port `443` (the
most common case).
#### Valid targets
@ -121,22 +145,22 @@ If neither are given, the exporter assumes a https connection on port `443` (the
### Example Queries
Certificates that expire within 7 days, with Subject Common Name and Subject Alternative Names joined on:
Certificates that expire within 7 days:
```
((ssl_cert_not_after - time() < 86400 * 7) * on (instance,issuer_cn,serial_no) group_left (dnsnames) ssl_cert_subject_alternative_dnsnames) * on (instance,issuer_cn,serial_no) group_left (subject_cn) ssl_cert_subject_common_name
ssl_cert_not_after - time() < 86400 * 7
```
Only return wildcard certificates that are expiring:
Wildcard certificates that are expiring:
```
((ssl_cert_not_after - time() < 86400 * 7) * on (instance,issuer_cn,serial_no) group_left (subject_cn) ssl_cert_subject_common_name{subject_cn=~"\\*.*"})
ssl_cert_not_after{cn=~"\*.*"} - time() < 86400 * 7
```
Number of certificates in the chain:
```
count(ssl_cert_subject_common_name) by (instance)
count(ssl_cert_not_after) by (instance, serial_no, issuer_cn)
```
Identify instances that have failed to create a valid SSL connection:
@ -147,28 +171,44 @@ ssl_tls_connect_success == 0
## Client authentication
The exporter optionally supports client authentication, which can be toggled on by providing the `--tls.client-auth` flag. By default, it will use the host system's root CA bundle and attempt to use `./cert.pem` and `./key.pem` as the client certificate and key, respectively. You can override these defaults with `--tls.cacert`, `--tls.cert` and `--tls.key`.
The exporter optionally supports client authentication, which can be toggled on
by providing the `--tls.client-auth` flag. By default, it will use the host
system's root CA bundle and attempt to use `./cert.pem` and `./key.pem` as the
client certificate and key, respectively. You can override these defaults with
`--tls.cacert`, `--tls.cert` and `--tls.key`.
If you do enable client authentication, keep in mind that the certificate will be passed to all targets, even those that don't necessarily require client authentication. I'm not sure what the implications of that are but I think you'd probably want to avoid passing a certificate to an unrelated server.
If you do enable client authentication, keep in mind that the certificate will
be passed to all targets, even those that don't necessarily require client
authentication. I'm not sure what the implications of that are but I think you'd
probably want to avoid passing a certificate to an unrelated server.
Also, if you want to scrape targets with different client certificate requirements, you'll need to run different instances of the exporter for each. This seemed like a better approach than overloading the exporter with the ability to pass different certificates per-target.
Also, if you want to scrape targets with different client certificate
requirements, you'll need to run different instances of the exporter for each.
This seemed like a better approach than overloading the exporter with the
ability to pass different certificates per-target.
## Proxying
The https client used by the exporter supports the use of proxy servers discovered by the environment variables `HTTP_PROXY`,
`HTTPS_PROXY` and `ALL_PROXY`.
The https client used by the exporter supports the use of proxy servers
discovered by the environment variables `HTTP_PROXY`, `HTTPS_PROXY` and
`ALL_PROXY`.
For instance:
$ export HTTPS_PROXY=localhost:8888
$ ./ssl_exporter
In order to use the https client, targets must be provided to the exporter with the protocol in the uri (`https://<host>:<optional port>`).
In order to use the https client, targets must be provided to the exporter with
the protocol in the uri (`https://<host>:<optional port>`).
## Limitations
I've only exported a subset of the information you could extract from a certificate. It would be simple to add more, for instance organisational information, if there's a need.
I've only exported a subset of the information you could extract from a
certificate. It would be simple to add more, for instance organisational
information, if there's a need.
## Acknowledgements
The overall structure and implementation of this exporter is based on the [consul_exporter](https://github.com/prometheus/consul_exporter). The probing functionality borrows from the blackbox_exporter.
The overall structure and implementation of this exporter is based on the
[consul_exporter](https://github.com/prometheus/consul_exporter). The probing
functionality borrows from the blackbox_exporter.

View File

@ -38,37 +38,12 @@ var (
notBefore = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_not_before"),
"NotBefore expressed as a Unix Epoch Time",
[]string{"serial_no", "issuer_cn"}, nil,
[]string{"serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"}, nil,
)
notAfter = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_not_after"),
"NotAfter expressed as a Unix Epoch Time",
[]string{"serial_no", "issuer_cn"}, nil,
)
commonName = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_subject_common_name"),
"Subject Common Name",
[]string{"serial_no", "issuer_cn", "subject_cn"}, nil,
)
subjectAlernativeDNSNames = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_subject_alternative_dnsnames"),
"Subject Alternative DNS Names",
[]string{"serial_no", "issuer_cn", "dnsnames"}, nil,
)
subjectAlernativeIPs = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_subject_alternative_ips"),
"Subject Alternative IPs",
[]string{"serial_no", "issuer_cn", "ips"}, nil,
)
subjectAlernativeEmailAddresses = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_subject_alternative_emails"),
"Subject Alternative Email Addresses",
[]string{"serial_no", "issuer_cn", "emails"}, nil,
)
subjectOrganizationUnits = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_subject_organization_units"),
"Subject Organization Units",
[]string{"serial_no", "issuer_cn", "subject_ou"}, nil,
[]string{"serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"}, nil,
)
)
@ -85,11 +60,6 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- clientProtocol
ch <- notAfter
ch <- notBefore
ch <- commonName
ch <- subjectAlernativeDNSNames
ch <- subjectAlernativeIPs
ch <- subjectAlernativeEmailAddresses
ch <- subjectOrganizationUnits
}
// Collect metrics
@ -190,58 +160,44 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
// Loop through returned certificates and create metrics
for _, cert := range peerCertificates {
var DNSNamesLabel, emailsLabel, ipsLabel, OULabel string
subjectCN := cert.Subject.CommonName
issuerCN := cert.Issuer.CommonName
subjectDNSNames := cert.DNSNames
subjectEmails := cert.EmailAddresses
subjectIPs := cert.IPAddresses
serialNum := cert.SerialNumber.String()
subjectOUs := cert.Subject.OrganizationalUnit
commonName := cert.Subject.CommonName
issuerCommonName := cert.Issuer.CommonName
serialNo := cert.SerialNumber.String()
DNSNames := cert.DNSNames
emailAddresses := cert.EmailAddresses
IPAddresses := cert.IPAddresses
OU := cert.Subject.OrganizationalUnit
if len(DNSNames) > 0 {
DNSNamesLabel = "," + strings.Join(DNSNames, ",") + ","
}
if len(emailAddresses) > 0 {
emailsLabel = "," + strings.Join(DNSNames, ",") + ","
}
if len(IPAddresses) > 0 {
ipsLabel = ","
for _, ip := range IPAddresses {
ipsLabel = ipsLabel + ip.String() + ","
}
}
if len(OU) > 0 {
OULabel = "," + strings.Join(OU, ",") + ","
}
if !cert.NotAfter.IsZero() {
ch <- prometheus.MustNewConstMetric(
notAfter, prometheus.GaugeValue, float64(cert.NotAfter.UnixNano()/1e9), serialNum, issuerCN,
notAfter, prometheus.GaugeValue, float64(cert.NotAfter.UnixNano()/1e9), serialNo, issuerCommonName, commonName, DNSNamesLabel, ipsLabel, emailsLabel, OULabel,
)
}
if !cert.NotBefore.IsZero() {
ch <- prometheus.MustNewConstMetric(
notBefore, prometheus.GaugeValue, float64(cert.NotBefore.UnixNano()/1e9), serialNum, issuerCN,
)
}
if subjectCN != "" {
ch <- prometheus.MustNewConstMetric(
commonName, prometheus.GaugeValue, 1, serialNum, issuerCN, subjectCN,
)
}
if len(subjectDNSNames) > 0 {
ch <- prometheus.MustNewConstMetric(
subjectAlernativeDNSNames, prometheus.GaugeValue, 1, serialNum, issuerCN, ","+strings.Join(subjectDNSNames, ",")+",",
)
}
if len(subjectEmails) > 0 {
ch <- prometheus.MustNewConstMetric(
subjectAlernativeEmailAddresses, prometheus.GaugeValue, 1, serialNum, issuerCN, ","+strings.Join(subjectEmails, ",")+",",
)
}
if len(subjectIPs) > 0 {
i := ","
for _, ip := range subjectIPs {
i = i + ip.String() + ","
}
ch <- prometheus.MustNewConstMetric(
subjectAlernativeIPs, prometheus.GaugeValue, 1, serialNum, issuerCN, i,
)
}
if len(subjectOUs) > 0 {
ch <- prometheus.MustNewConstMetric(
subjectOrganizationUnits, prometheus.GaugeValue, 1, serialNum, issuerCN, ","+strings.Join(subjectOUs, ",")+",",
notBefore, prometheus.GaugeValue, float64(cert.NotBefore.UnixNano()/1e9), serialNo, issuerCommonName, commonName, DNSNamesLabel, ipsLabel, emailsLabel, OULabel,
)
}
}

View File

@ -326,9 +326,9 @@ func TestProbeHandlerIPs(t *testing.T) {
t.Fatalf(err.Error())
}
ok := strings.Contains(rr.Body.String(), "ssl_cert_subject_alternative_ips{ips=\",127.0.0.1,\"")
ok := strings.Contains(rr.Body.String(), "ips=\",127.0.0.1,\"")
if !ok {
t.Errorf("expected `ssl_cert_subject_alternative_ips{ips=\",127.0.0.1,\"`")
t.Errorf("expected `ips=\",127.0.0.1,\"`")
}
server.Close()
@ -346,9 +346,9 @@ func TestProbeHandlerCommonName(t *testing.T) {
t.Fatalf(err.Error())
}
log.Println(rr.Body.String())
ok := strings.Contains(rr.Body.String(), "ssl_cert_subject_common_name{issuer_cn=\"ribbybibby.me\",serial_no=\"318581226177353336430613662595136105644\",subject_cn=\"cert.ribbybibby.me\"} 1")
ok := strings.Contains(rr.Body.String(), "cn=\"cert.ribbybibby.me\"")
if !ok {
t.Errorf("expected `ssl_cert_subject_common_name{issuer_cn=\"ribbybibby.me\",serial_no=\"318581226177353336430613662595136105644\",subject_cn=\"cert.ribbybibby.me\"} 1`")
t.Errorf("expected `cn=\"cert.ribbybibby.me\"`")
}
server.Close()
@ -366,9 +366,9 @@ func TestProbeHandlerDNSNames(t *testing.T) {
t.Fatalf(err.Error())
}
ok := strings.Contains(rr.Body.String(), "ssl_cert_subject_alternative_dnsnames{dnsnames=\",cert.ribbybibby.me,localhost,\"")
ok := strings.Contains(rr.Body.String(), "dnsnames=\",cert.ribbybibby.me,localhost,\"")
if !ok {
t.Errorf("expected `ssl_cert_subject_alternative_dnsnames{dnsnames=\",cert.ribbybibby.me,localhost,\"`")
t.Errorf("expected `dnsnames=\",cert.ribbybibby.me,localhost,\"`")
}
server.Close()