mirror of
https://github.com/ribbybibby/ssl_exporter.git
synced 2026-02-25 15:03:49 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
606f4f6032 | ||
|
|
f91d97c220 | ||
|
|
cfab972f8f | ||
|
|
10353fe7fb | ||
|
|
5a1b013445 | ||
|
|
215029534e |
22
Dockerfile
22
Dockerfile
@@ -1,5 +1,21 @@
|
||||
FROM quay.io/prometheus/busybox:latest
|
||||
FROM golang:stretch AS build
|
||||
|
||||
COPY ssl_exporter /bin/ssl_exporter
|
||||
ADD . /tmp/ssl_exporter
|
||||
|
||||
ENTRYPOINT ["/bin/ssl_exporter"]
|
||||
RUN cd /tmp/ssl_exporter && \
|
||||
echo "ssl:*:100:ssl" > group && \
|
||||
echo "ssl:*:100:100::/:/ssl_exporter" > passwd && \
|
||||
make
|
||||
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=build /tmp/ssl_exporter/group \
|
||||
/tmp/ssl_exporter/passwd \
|
||||
/etc/
|
||||
COPY --from=build /tmp/ssl_exporter/ssl_exporter /
|
||||
|
||||
USER ssl:ssl
|
||||
EXPOSE 9219/tcp
|
||||
ENTRYPOINT ["/ssl_exporter"]
|
||||
|
||||
137
README.md
137
README.md
@@ -1,80 +1,136 @@
|
||||
# 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.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
* [SSL Certificate Exporter](#ssl-certificate-exporter)
|
||||
* [Building](#building)
|
||||
* [Docker](#docker)
|
||||
* [Flags](#flags)
|
||||
* [Metrics](#metrics)
|
||||
* [Prometheus](#prometheus)
|
||||
* [Configuration](#configuration)
|
||||
* [Targets](#targets)
|
||||
* [Valid targets](#valid-targets)
|
||||
* [Invalid targets](#invalid-targets)
|
||||
* [Example Queries](#example-queries)
|
||||
* [Client authentication](#client-authentication)
|
||||
* [Proxying](#proxying)
|
||||
* [Limitations](#limitations)
|
||||
* [Acknowledgements](#acknowledgements)
|
||||
|
||||
Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)
|
||||
|
||||
## Building
|
||||
|
||||
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
|
||||
|
||||
docker pull ribbybibby/ssl-exporter
|
||||
docker run -p 9219:9219 ssl-exporter:latest <flags>
|
||||
|
||||
## Flags
|
||||
|
||||
./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.
|
||||
* __`--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")
|
||||
|
||||
- **`--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")
|
||||
|
||||
## 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.
|
||||
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_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. | 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. | |
|
||||
|
||||
## 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.
|
||||
|
||||
```yml
|
||||
scrape_configs:
|
||||
- job_name: 'ssl'
|
||||
- job_name: "ssl"
|
||||
metrics_path: /probe
|
||||
static_configs:
|
||||
- targets:
|
||||
- example.com:443
|
||||
- prometheus.io:443
|
||||
- example.com:443
|
||||
- prometheus.io:443
|
||||
relabel_configs:
|
||||
- source_labels: [__address__]
|
||||
target_label: __param_target
|
||||
- source_labels: [__param_target]
|
||||
target_label: instance
|
||||
- target_label: __address__
|
||||
replacement: 127.0.0.1:9219 # SSL exporter.
|
||||
replacement: 127.0.0.1:9219 # SSL exporter.
|
||||
```
|
||||
### Example Queries
|
||||
Certificates that expire within 7 days, with Subject Common Name and Subject Alternative Names joined on:
|
||||
|
||||
((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
|
||||
|
||||
### 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`).
|
||||
|
||||
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 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).
|
||||
|
||||
#### Valid targets
|
||||
|
||||
- `https://example.com`
|
||||
- `https://example.com:443`
|
||||
- `example.com:443`
|
||||
- `example.com:636`
|
||||
- `example.com`
|
||||
|
||||
#### Invalid targets
|
||||
|
||||
- `ldaps://example.com`
|
||||
- `ldaps://example.com:636`
|
||||
|
||||
### Example Queries
|
||||
|
||||
Certificates that expire within 7 days, with Subject Common Name and Subject Alternative Names joined on:
|
||||
|
||||
((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
|
||||
|
||||
Only return 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=~"\\*.*"})
|
||||
|
||||
|
||||
Number of certificates in the chain:
|
||||
|
||||
|
||||
count(ssl_cert_subject_common_name) by (instance)
|
||||
|
||||
Identify instances that have failed to create a valid SSL connection:
|
||||
@@ -82,14 +138,29 @@ Identify instances that have failed to create a valid SSL connection:
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
## 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`.
|
||||
|
||||
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>`).
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
118
ssl_exporter.go
118
ssl_exporter.go
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@@ -29,6 +30,11 @@ var (
|
||||
"If the TLS connection was a success",
|
||||
nil, nil,
|
||||
)
|
||||
clientProtocol = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "client_protocol"),
|
||||
"The protocol used by the exporter to connect to the target",
|
||||
[]string{"protocol"}, nil,
|
||||
)
|
||||
notBefore = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "cert_not_before"),
|
||||
"NotBefore expressed as a Unix Epoch Time",
|
||||
@@ -76,6 +82,7 @@ type Exporter struct {
|
||||
// Describe metrics
|
||||
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- tlsConnectSuccess
|
||||
ch <- clientProtocol
|
||||
ch <- notAfter
|
||||
ch <- commonName
|
||||
ch <- subjectAlernativeDNSNames
|
||||
@@ -86,8 +93,10 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
|
||||
|
||||
// Collect metrics
|
||||
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
|
||||
var peerCertificates []*x509.Certificate
|
||||
|
||||
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: e.timeout}, "tcp", e.target, e.tlsConfig)
|
||||
// Parse the target and return the appropriate connection protocol and target address
|
||||
target, proto, err := parseTarget(e.target)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
@@ -96,10 +105,75 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
|
||||
return
|
||||
}
|
||||
|
||||
state := conn.ConnectionState()
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
clientProtocol, prometheus.GaugeValue, 1, proto,
|
||||
)
|
||||
|
||||
if len(state.PeerCertificates) < 1 {
|
||||
log.Errorln("No certificates found in connection state")
|
||||
if proto == "https" {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
clientProtocol, prometheus.GaugeValue, 0, "tcp",
|
||||
)
|
||||
|
||||
// Create the http client
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: e.tlsConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
Timeout: e.timeout,
|
||||
}
|
||||
|
||||
// Issue a GET request to the target
|
||||
resp, err := client.Get(e.target)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
tlsConnectSuccess, prometheus.GaugeValue, 0,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the response from the target is encrypted
|
||||
if resp.TLS == nil {
|
||||
log.Errorln("The response from " + target + " is unencrypted")
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
tlsConnectSuccess, prometheus.GaugeValue, 0,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
peerCertificates = resp.TLS.PeerCertificates
|
||||
|
||||
} else if proto == "tcp" {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
clientProtocol, prometheus.GaugeValue, 0, "https",
|
||||
)
|
||||
|
||||
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: e.timeout}, "tcp", target, e.tlsConfig)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
tlsConnectSuccess, prometheus.GaugeValue, 0,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
state := conn.ConnectionState()
|
||||
|
||||
peerCertificates = state.PeerCertificates
|
||||
|
||||
if len(peerCertificates) < 1 {
|
||||
log.Errorln("No certificates found in connection state for " + target)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
tlsConnectSuccess, prometheus.GaugeValue, 0,
|
||||
)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.Errorln("Unrecognised protocol: " + string(proto) + " for target: " + target)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
tlsConnectSuccess, prometheus.GaugeValue, 0,
|
||||
)
|
||||
@@ -111,7 +185,7 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
|
||||
)
|
||||
|
||||
// Remove duplicate certificates from the response
|
||||
peerCertificates := uniq(state.PeerCertificates)
|
||||
peerCertificates = uniq(peerCertificates)
|
||||
|
||||
// Loop through returned certificates and create metrics
|
||||
for _, cert := range peerCertificates {
|
||||
@@ -173,7 +247,6 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
|
||||
}
|
||||
|
||||
func probeHandler(w http.ResponseWriter, r *http.Request, tlsConfig *tls.Config) {
|
||||
|
||||
target := r.URL.Query().Get("target")
|
||||
|
||||
// The following timeout block was taken wholly from the blackbox exporter
|
||||
@@ -195,13 +268,8 @@ func probeHandler(w http.ResponseWriter, r *http.Request, tlsConfig *tls.Config)
|
||||
|
||||
timeout := time.Duration((timeoutSeconds) * 1e9)
|
||||
|
||||
t, err := parseTarget(target)
|
||||
if err != nil {
|
||||
t = target
|
||||
}
|
||||
|
||||
exporter := &Exporter{
|
||||
target: t,
|
||||
target: target,
|
||||
timeout: timeout,
|
||||
tlsConfig: tlsConfig,
|
||||
}
|
||||
@@ -235,33 +303,25 @@ func contains(certs []*x509.Certificate, cert *x509.Certificate) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// parseTarget makes an attempt at converting URLs of the form scheme://host
|
||||
// into host:port
|
||||
func parseTarget(target string) (parsedTarget string, err error) {
|
||||
func parseTarget(target string) (parsedTarget string, proto string, err error) {
|
||||
if !strings.Contains(target, "://") {
|
||||
target = "//" + target
|
||||
}
|
||||
|
||||
u, err := url.Parse(target)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
return "", proto, err
|
||||
}
|
||||
|
||||
if u.Port() == "" {
|
||||
switch scheme := u.Scheme; scheme {
|
||||
case "https":
|
||||
parsedTarget = u.Host + ":443"
|
||||
case "ldaps":
|
||||
parsedTarget = u.Host + ":636"
|
||||
default:
|
||||
parsedTarget = u.Host + ":443"
|
||||
if u.Scheme != "" {
|
||||
if u.Scheme == "https" {
|
||||
return u.String(), "https", nil
|
||||
}
|
||||
} else {
|
||||
parsedTarget = u.Host
|
||||
return "", proto, errors.New("can't handle the scheme '" + u.Scheme + "' - try providing the target in the format <host>:<port>")
|
||||
} else if u.Port() == "" {
|
||||
return "https://" + u.Host, "https", nil
|
||||
}
|
||||
|
||||
return parsedTarget, nil
|
||||
return u.Host, "tcp", nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -3,115 +3,580 @@ package main
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProbeHandler(t *testing.T) {
|
||||
certContent, err := ioutil.ReadFile("test/badssl.com-client.pem")
|
||||
if err != nil {
|
||||
t.Fatalf("Can't read test client certificate from disk")
|
||||
}
|
||||
var clientCert = `-----BEGIN CERTIFICATE-----
|
||||
MIIC6jCCApCgAwIBAgIQPbn1oJJ0lvHOxk3BbnhGMTAKBggqhkjOPQQDAjCBhTEL
|
||||
MAkGA1UEBhMCR0IxEDAOBgNVBAgTB0VuZ2xhbmQxDzANBgNVBAcTBkxvbmRvbjEU
|
||||
MBIGA1UECRMLMTIzIEZha2UgU3QxEDAOBgNVBBETB1NXMThYWFgxEzARBgNVBAoT
|
||||
CnJpYmJ5YmliYnkxFjAUBgNVBAMTDXJpYmJ5YmliYnkubWUwHhcNMTkwMzI5MDc1
|
||||
MjI5WhcNMjAwMzI4MDc1MjI5WjAdMRswGQYDVQQDExJjZXJ0LnJpYmJ5YmliYnku
|
||||
bWUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASlHGGsAAEMpyBVkgSZazMcYmHH
|
||||
4K8+m9VI9nSnD4t1b01jYuNAsJjvnRI2iGLOxQ1i8KgzgeZz6ud1mJLIudTzo4IB
|
||||
RzCCAUMwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
|
||||
BQcDAjAMBgNVHRMBAf8EAjAAMGgGA1UdDgRhBF9mNzphMzo4NDo0ZDo0NjowOTpl
|
||||
Nzo5ZDpiNzo3MjphMTo5ZTpkOTpjMDoxYTpmYzpjMzplODplZDozOTozMTo5Mzox
|
||||
MjpmMDplZTowODo2YTo2Mzo3NzphNjplMDoyMjBqBgNVHSMEYzBhgF8xNTpkZDo0
|
||||
MTo4ODoxODo0YjoxOTo2NToyYjo2ZTo0Njo1NTozZTo3MTo0MzpjYjphMjo3Nzpk
|
||||
YzpiNTpjZToxMTpiZTo2NDo3ODo3Zjo1OTo2NzpiYTpmMDo0YTowNTAuBgNVHREE
|
||||
JzAlghJjZXJ0LnJpYmJ5YmliYnkubWWCCWxvY2FsaG9zdIcEfwAAATAKBggqhkjO
|
||||
PQQDAgNIADBFAiEAq5AUjiAQxMy0g0f2KyFshTu5QPXXSPo+VTBSQcYuEzICIAWr
|
||||
JxpZXB4hH2+sEZ4z+bH6l47wbYqOT02d/VNbk3vw
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
keyContent, err := ioutil.ReadFile("test/badssl.com-client-key.pem")
|
||||
if err != nil {
|
||||
t.Fatalf("Can't read test client certificate key from disk")
|
||||
}
|
||||
var clientKey = `-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIPfP8yJatMwUfCyNdIQiQANO2vd3QQIoHJ6g+o8kb7PJoAoGCCqGSM49
|
||||
AwEHoUQDQgAEpRxhrAABDKcgVZIEmWszHGJhx+CvPpvVSPZ0pw+LdW9NY2LjQLCY
|
||||
750SNohizsUNYvCoM4Hmc+rndZiSyLnU8w==
|
||||
-----END EC PRIVATE KEY-----`
|
||||
|
||||
keyBlock, _ := pem.Decode(keyContent)
|
||||
var clientCertWrong = `-----BEGIN CERTIFICATE-----
|
||||
MIIC7zCCApWgAwIBAgIRAPx4XNhgs5QfvE6FHnYa3uQwCgYIKoZIzj0EAwIwgYUx
|
||||
CzAJBgNVBAYTAkdCMRAwDgYDVQQIEwdFbmdsYW5kMQ8wDQYDVQQHEwZMb25kb24x
|
||||
FDASBgNVBAkTCzEyMyBGYWtlIFN0MRAwDgYDVQQREwdTVzE4WFhYMRMwEQYDVQQK
|
||||
EwpyaWJieWJpYmJ5MRYwFAYDVQQDEw1yaWJieWJpYmJ5Lm1lMB4XDTE5MDMyNzE2
|
||||
MTgzOVoXDTIwMDMyNjE2MTgzOVowHzEdMBsGA1UEAxMUY2xpZW50LnJpYmJ5Ymli
|
||||
YnkubWUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQtlqtCTzZNCdDiMHKD/p1F
|
||||
97/I1MnkRK+QdUxEDnRhHAuMOhypxJ6NruZz+wXLnJEmUYmTsHkz1a4tKz2YJCUp
|
||||
o4IBSTCCAUUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggr
|
||||
BgEFBQcDATAMBgNVHRMBAf8EAjAAMGgGA1UdDgRhBF9kYzowNDozMjo0ZTpkOTo4
|
||||
YjphNTplMDpmNjo5MjpkYzpiYzoxOTo1NTo0ZDo0YjpiNTo5YTo5OTpjYjo4Zjoz
|
||||
ZjplMTpkNzo3MDoyMTo2MzpmZDo4YTo4MDpjMzpiNzBqBgNVHSMEYzBhgF82YTo0
|
||||
MDozNTowZjpmZTowMjpkNzo0Zjo5ODozZTo3ODoyMTpjMDo0YTo5YzpjZTo2Nzoz
|
||||
NDpiZDo4MjowYTo3MjpkMzpjOTo3Njo5MDo3Nzo5ODpmMDo2NTpmYzpkMDAwBgNV
|
||||
HREEKTAnghRjbGllbnQucmliYnliaWJieS5tZYIJbG9jYWxob3N0hwR/AAABMAoG
|
||||
CCqGSM49BAMCA0gAMEUCIQCa7ru0f0/HVoGa7aBJqACMBfiXWCI159WGt2B7Mxvf
|
||||
VAIgX9O8fOl6qmsJyfMkfdmv6lo9oAWIecDLpVtqEj5i2Qc=
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
keyBlockDecrypted, err := x509.DecryptPEMBlock(keyBlock, []byte("badssl.com"))
|
||||
if err != nil {
|
||||
t.Fatalf("Issue decrypting test client key")
|
||||
}
|
||||
var clientKeyWrong = `-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEILnEJttULi+2cupO4ta6IB9bEeul6rMGFSpPMB7kPuSwoAoGCCqGSM49
|
||||
AwEHoUQDQgAELZarQk82TQnQ4jByg/6dRfe/yNTJ5ESvkHVMRA50YRwLjDocqcSe
|
||||
ja7mc/sFy5yRJlGJk7B5M9WuLSs9mCQlKQ==
|
||||
-----END EC PRIVATE KEY-----`
|
||||
|
||||
keyContent = pem.EncodeToMemory(&pem.Block{Type: keyBlock.Type, Bytes: keyBlockDecrypted})
|
||||
var serverCert = `-----BEGIN CERTIFICATE-----
|
||||
MIIC6jCCApGgAwIBAgIRAO+sgyd/vcnDgfmafkgALKwwCgYIKoZIzj0EAwIwgYUx
|
||||
CzAJBgNVBAYTAkdCMRAwDgYDVQQIEwdFbmdsYW5kMQ8wDQYDVQQHEwZMb25kb24x
|
||||
FDASBgNVBAkTCzEyMyBGYWtlIFN0MRAwDgYDVQQREwdTVzE4WFhYMRMwEQYDVQQK
|
||||
EwpyaWJieWJpYmJ5MRYwFAYDVQQDEw1yaWJieWJpYmJ5Lm1lMB4XDTE5MDMyOTA3
|
||||
NTIyN1oXDTIwMDMyODA3NTIyN1owHTEbMBkGA1UEAxMSY2VydC5yaWJieWJpYmJ5
|
||||
Lm1lMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY5nQFSmpZnFvjbAicuElYlT2
|
||||
xQvO+LgYt+5bcGfemT5HRq63tljiGlsyNXAysAmMwT9+blu8sLqkyh6PMFesJ6OC
|
||||
AUcwggFDMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
|
||||
BQUHAwIwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfZmI6NDM6NWY6M2Y6NTE6NGI6
|
||||
NjA6YTI6YzQ6NzI6ZjE6MGQ6OTM6ZDA6YjQ6ODA6N2Y6Mjc6NjM6Yjk6NWI6NTQ6
|
||||
ZGQ6NzI6NzU6N2Q6MDU6N2U6ZTc6Y2U6OTM6YTMwagYDVR0jBGMwYYBfMTU6ZGQ6
|
||||
NDE6ODg6MTg6NGI6MTk6NjU6MmI6NmU6NDY6NTU6M2U6NzE6NDM6Y2I6YTI6Nzc6
|
||||
ZGM6YjU6Y2U6MTE6YmU6NjQ6Nzg6N2Y6NTk6Njc6YmE6ZjA6NGE6MDUwLgYDVR0R
|
||||
BCcwJYISY2VydC5yaWJieWJpYmJ5Lm1lgglsb2NhbGhvc3SHBH8AAAEwCgYIKoZI
|
||||
zj0EAwIDRwAwRAIgI6w7Px0UnI3AAP4n9ApO1gNIhY+ECEb0EZvKopmNUn0CIHN4
|
||||
MEaXLzEfNdNi7E521qIR+bhV/mu8nubZIsG4K383
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
emptyRootCAs := x509.NewCertPool()
|
||||
var serverKey = `-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIAeLgH2jonGdCgdG1MpEy9wAgxvCSC4N7sK3hC0GZM7MoAoGCCqGSM49
|
||||
AwEHoUQDQgAEY5nQFSmpZnFvjbAicuElYlT2xQvO+LgYt+5bcGfemT5HRq63tlji
|
||||
GlsyNXAysAmMwT9+blu8sLqkyh6PMFesJw==
|
||||
-----END EC PRIVATE KEY-----`
|
||||
|
||||
certificate, err := tls.X509KeyPair(certContent, keyContent)
|
||||
var expiredCert = `-----BEGIN CERTIFICATE-----
|
||||
MIIC2DCCAn6gAwIBAgIQeP4wyiBMCZ5TLpM40Ho6UzAKBggqhkjOPQQDAjCBhTEL
|
||||
MAkGA1UEBhMCR0IxEDAOBgNVBAgTB0VuZ2xhbmQxDzANBgNVBAcTBkxvbmRvbjEU
|
||||
MBIGA1UECRMLMTIzIEZha2UgU3QxEDAOBgNVBBETB1NXMThYWFgxEzARBgNVBAoT
|
||||
CnJpYmJ5YmliYnkxFjAUBgNVBAMTDXJpYmJ5YmliYnkubWUwHhcNMTkwMzI5MDgw
|
||||
MTM4WhcNMTkwMzI4MDgwMTM4WjAdMRswGQYDVQQDExJjZXJ0LnJpYmJ5YmliYnku
|
||||
bWUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASjDs0ehi0miAKmDnuCmRyWaKOY
|
||||
+h0MugoFngChyygYCY+mOb/+HV5AYUEf1NFJLz4DtYnNKyWNHnX7vUPEh+Ico4IB
|
||||
NTCCATEwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
|
||||
BQcDAjAMBgNVHRMBAf8EAjAAMGgGA1UdDgRhBF9mNTo1NDpmYzphNTo1ZjplMzo5
|
||||
YTo3MzplNzo1YTo0ZDowNzo0MTo4YjoyOTo2ZDpiNzpiNTpjMDpiZjowMzpkZTo5
|
||||
Zjo5NTozNzphMjphNDo4MDo2YTo3MDozNDpmNjBqBgNVHSMEYzBhgF9iOTpjMDo2
|
||||
NzoyYjo2YTpiNzowMToyMjo2Zjo1NTplMjpiMDphNDoyNDo1YTo5NzplMzpjYzpi
|
||||
MTo3Yjo4ZjoyNDpiNTo1NToxYzpiMDo3NTozMDplNToxZDo3OTpmZDAcBgNVHREE
|
||||
FTATggCCCWxvY2FsaG9zdIcEfwAAATAKBggqhkjOPQQDAgNIADBFAiB+ZGtScM5Y
|
||||
QHra5d+lqFRJOd7WXkoU03QHWOP3pSqbCAIhAJreqVQ3dUME4j9LYbQWmD96agdL
|
||||
2uxG31qfCa/T5TCq
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
var expiredKey = `-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIFDlw65IF8NLdgIWU1ipkMffcE6MgZ5DHTGzf0WN09EJoAoGCCqGSM49
|
||||
AwEHoUQDQgAEow7NHoYtJogCpg57gpkclmijmPodDLoKBZ4AocsoGAmPpjm//h1e
|
||||
QGFBH9TRSS8+A7WJzSsljR51+71DxIfiHA==
|
||||
-----END EC PRIVATE KEY-----`
|
||||
|
||||
var caCert = `-----BEGIN CERTIFICATE-----
|
||||
MIIDBjCCAqygAwIBAgIRAJxzFmvhp8ef68W7SQrt5KwwCgYIKoZIzj0EAwIwgYUx
|
||||
CzAJBgNVBAYTAkdCMRAwDgYDVQQIEwdFbmdsYW5kMQ8wDQYDVQQHEwZMb25kb24x
|
||||
FDASBgNVBAkTCzEyMyBGYWtlIFN0MRAwDgYDVQQREwdTVzE4WFhYMRMwEQYDVQQK
|
||||
EwpyaWJieWJpYmJ5MRYwFAYDVQQDEw1yaWJieWJpYmJ5Lm1lMB4XDTE5MDMyOTA3
|
||||
NTIyMloXDTI0MDMyNzA3NTIyMlowgYUxCzAJBgNVBAYTAkdCMRAwDgYDVQQIEwdF
|
||||
bmdsYW5kMQ8wDQYDVQQHEwZMb25kb24xFDASBgNVBAkTCzEyMyBGYWtlIFN0MRAw
|
||||
DgYDVQQREwdTVzE4WFhYMRMwEQYDVQQKEwpyaWJieWJpYmJ5MRYwFAYDVQQDEw1y
|
||||
aWJieWJpYmJ5Lm1lMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE94APL4adMA7A
|
||||
tSSfxcHzzxdVBCwJju6jVCf5qRqG4Qz0neXlde6jIXocZvoboZJiA2e7BadnjoPN
|
||||
2sTB8mgg4KOB+jCB9zAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zBo
|
||||
BgNVHQ4EYQRfMTU6ZGQ6NDE6ODg6MTg6NGI6MTk6NjU6MmI6NmU6NDY6NTU6M2U6
|
||||
NzE6NDM6Y2I6YTI6Nzc6ZGM6YjU6Y2U6MTE6YmU6NjQ6Nzg6N2Y6NTk6Njc6YmE6
|
||||
ZjA6NGE6MDUwagYDVR0jBGMwYYBfMTU6ZGQ6NDE6ODg6MTg6NGI6MTk6NjU6MmI6
|
||||
NmU6NDY6NTU6M2U6NzE6NDM6Y2I6YTI6Nzc6ZGM6YjU6Y2U6MTE6YmU6NjQ6Nzg6
|
||||
N2Y6NTk6Njc6YmE6ZjA6NGE6MDUwCgYIKoZIzj0EAwIDSAAwRQIhANycTcKTH1DU
|
||||
eu3Xuz8CdtgT67yqUTxDy0O5kS8fFPUVAiAV0u1M7dQYV+buY8oOLYnZxondrb7/
|
||||
BNltD7A8Y0S0hw==
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
// Test the basic case: a typical HTTPS server
|
||||
func TestProbeHandlerConnectSuccess(t *testing.T) {
|
||||
server, err := server()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
// Test the behaviour of various target URIs
|
||||
// 'ok' denotes whether we expect a succesful tls connection
|
||||
cases := []struct {
|
||||
uri string
|
||||
ok bool
|
||||
tlsConfig *tls.Config
|
||||
}{
|
||||
// Test against an assumed valid, reachable and functioning HTTPS address
|
||||
{uri: "google.com:443", ok: true, tlsConfig: &tls.Config{}},
|
||||
// Test against a HTTP address
|
||||
{uri: "google.com:80", ok: false, tlsConfig: &tls.Config{}},
|
||||
// Test against an expired certificate when we're rejecting invalid certs
|
||||
{uri: "expired.badssl.com:443", ok: false, tlsConfig: &tls.Config{}},
|
||||
// Test against an expired certificate when we're accepting invalid certs
|
||||
{uri: "expired.badssl.com:443", ok: true, tlsConfig: &tls.Config{InsecureSkipVerify: true}},
|
||||
// Test against a target with no port
|
||||
{uri: "google.com", ok: true, tlsConfig: &tls.Config{}},
|
||||
// Test against a string with spaces
|
||||
{uri: "with spaces", ok: false, tlsConfig: &tls.Config{}},
|
||||
// Test against nothing
|
||||
{uri: "", ok: false, tlsConfig: &tls.Config{}},
|
||||
// Test with client authentication
|
||||
{uri: "client.badssl.com:443", ok: true, tlsConfig: &tls.Config{Certificates: []tls.Certificate{certificate}}},
|
||||
// Test with an empty root CA bundle
|
||||
{uri: "google.com:443", ok: false, tlsConfig: &tls.Config{RootCAs: emptyRootCAs}},
|
||||
// Test with a https scheme
|
||||
{uri: "https://google.com", ok: true, tlsConfig: &tls.Config{}},
|
||||
// Test with a https scheme and port
|
||||
{uri: "https://google.com:443", ok: true, tlsConfig: &tls.Config{}},
|
||||
}
|
||||
|
||||
fmt.Println("Note: The error logs in these tests are expected. One of the important tests is that we return the expected body, even in the face of errors.")
|
||||
|
||||
successMetricRegexp, err := regexp.Compile("(ssl_tls_connect_success [0-1])")
|
||||
rr, err := probe(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Error compiling success metric: " + err.Error())
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_tls_connect_success 1`")
|
||||
}
|
||||
|
||||
uri := "/probe?target=" + test.uri
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server.Close()
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
probeHandler(w, r, test.tlsConfig)
|
||||
})
|
||||
// Test against a non-existent server
|
||||
func TestProbeHandlerConnectSuccessFalse(t *testing.T) {
|
||||
rr, err := probe("localhost:6666")
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_tls_connect_success 0`")
|
||||
}
|
||||
|
||||
// We should always return a 200, no matter what
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we're getting the ssl_tls_connect_success metric back
|
||||
if !successMetricRegexp.MatchString(rr.Body.String()) {
|
||||
t.Errorf("can't find ssl_tls_connect_success metric in response body w/ %q", uri)
|
||||
}
|
||||
// Test with an empty target
|
||||
func TestProbeHandlerEmptyTarget(t *testing.T) {
|
||||
rr, err := probe("")
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
// Make sure we're getting the result we expect from ssl_tls_connect_success
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1")
|
||||
if test.ok && !ok {
|
||||
t.Errorf("expected tls connection to succeed but it failed w/ %q", uri)
|
||||
}
|
||||
if !test.ok && ok {
|
||||
t.Errorf("expected tls connection to fail but it succeeded w/ %q", uri)
|
||||
}
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_tls_connect_success 0`")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test with spaces in the target
|
||||
func TestProbeHandlerSpaces(t *testing.T) {
|
||||
rr, err := probe("with spaces")
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_tls_connect_success 0`")
|
||||
}
|
||||
}
|
||||
|
||||
// Test with a uri protocol the exporter doesn't implement a client for
|
||||
func TestProbeHandlerBadScheme(t *testing.T) {
|
||||
rr, err := probe("ldaps://example.com")
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_tls_connect_success 0`")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that probe uses a http client when the scheme is https://
|
||||
func TestProbeHandlerHTTPSClient(t *testing.T) {
|
||||
rr, err := probe("https://example.com")
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_client_protocol{protocol=\"https\"} 1")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_client_protocol{protocol=\"https\"} 1`")
|
||||
}
|
||||
|
||||
ok = strings.Contains(rr.Body.String(), "ssl_client_protocol{protocol=\"tcp\"} 0")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_client_protocol{protocol=\"tcp\"} 0`")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that probe uses a tcp client when the host is of the form <host>:<port>
|
||||
func TestProbeHandlerTCPClient(t *testing.T) {
|
||||
rr, err := probe("example.com:443")
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_client_protocol{protocol=\"tcp\"} 1")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_client_protocol{protocol=\"tcp\"} 1`")
|
||||
}
|
||||
|
||||
ok = strings.Contains(rr.Body.String(), "ssl_client_protocol{protocol=\"https\"} 0")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_client_protocol{protocol=\"https\"} 0`")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that a https client is used when there is no protocol or port in the target address
|
||||
func TestProbeHandlerNoProtocolNoPort(t *testing.T) {
|
||||
rr, err := probe("example.com")
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_client_protocol{protocol=\"https\"} 1")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_client_protocol{protocol=\"https\"} 1`")
|
||||
}
|
||||
}
|
||||
|
||||
// Test against a HTTP server
|
||||
func TestProbeHandlerHTTP(t *testing.T) {
|
||||
server, err := serverHTTP()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
rr, err := probe(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_tls_connect_success 0`")
|
||||
}
|
||||
|
||||
server.Close()
|
||||
}
|
||||
|
||||
// Test that the exporter returns the correct list of IPs
|
||||
func TestProbeHandlerIPs(t *testing.T) {
|
||||
server, err := server()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
rr, err := probe(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_cert_subject_alternative_ips{ips=\",127.0.0.1,\"")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_cert_subject_alternative_ips{ips=\",127.0.0.1,\"`")
|
||||
}
|
||||
|
||||
server.Close()
|
||||
}
|
||||
|
||||
// Test that the exporter returns the correct CN
|
||||
func TestProbeHandlerCommonName(t *testing.T) {
|
||||
server, err := server()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
rr, err := probe(server.URL)
|
||||
if err != nil {
|
||||
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")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_cert_subject_common_name{issuer_cn=\"ribbybibby.me\",serial_no=\"318581226177353336430613662595136105644\",subject_cn=\"cert.ribbybibby.me\"} 1`")
|
||||
}
|
||||
|
||||
server.Close()
|
||||
}
|
||||
|
||||
// Test that the exporter returns the correct list of DNS names
|
||||
func TestProbeHandlerDNSNames(t *testing.T) {
|
||||
server, err := server()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
rr, err := probe(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_cert_subject_alternative_dnsnames{dnsnames=\",cert.ribbybibby.me,localhost,\"")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_cert_subject_alternative_dnsnames{dnsnames=\",cert.ribbybibby.me,localhost,\"`")
|
||||
}
|
||||
|
||||
server.Close()
|
||||
}
|
||||
|
||||
// Test client authentication
|
||||
func TestProbeHandlerClientAuth(t *testing.T) {
|
||||
server, err := serverClientAuth()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
rr, err := probeClientAuth(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_tls_connect_success 1`")
|
||||
}
|
||||
|
||||
server.Close()
|
||||
}
|
||||
|
||||
// Test client authentication with a bad client certificate
|
||||
func TestProbeHandlerClientAuthWrongClientCert(t *testing.T) {
|
||||
server, err := serverClientAuth()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
rr, err := probeClientAuthBad(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_tls_connect_success 0`")
|
||||
}
|
||||
|
||||
server.Close()
|
||||
}
|
||||
|
||||
// Test against a server with an expired certificate
|
||||
func TestProbeHandlerExpired(t *testing.T) {
|
||||
server, err := serverExpired()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
rr, err := probe(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_tls_connect_success 0`")
|
||||
}
|
||||
|
||||
server.Close()
|
||||
}
|
||||
|
||||
// Test against a server with an expired certificate with an insecure probe
|
||||
func TestProbeHandlerExpiredInsecure(t *testing.T) {
|
||||
server, err := serverExpired()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
rr, err := probeInsecure(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1")
|
||||
if !ok {
|
||||
t.Errorf("expected `ssl_tls_connect_success 1`")
|
||||
}
|
||||
|
||||
server.Close()
|
||||
}
|
||||
|
||||
func probe(url string) (*httptest.ResponseRecorder, error) {
|
||||
uri := "/probe?target=" + url
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
probeHandler(w, r, &tls.Config{
|
||||
RootCAs: certPool(),
|
||||
})
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
return rr, nil
|
||||
}
|
||||
|
||||
func probeInsecure(url string) (*httptest.ResponseRecorder, error) {
|
||||
uri := "/probe?target=" + url
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
probeHandler(w, r, &tls.Config{
|
||||
RootCAs: certPool(),
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
return rr, nil
|
||||
}
|
||||
|
||||
func probeClientAuth(url string) (*httptest.ResponseRecorder, error) {
|
||||
clientCertificate, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := "/probe?target=" + url
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
probeHandler(w, r, &tls.Config{
|
||||
Certificates: []tls.Certificate{clientCertificate},
|
||||
RootCAs: certPool(),
|
||||
})
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
return rr, nil
|
||||
}
|
||||
|
||||
func probeClientAuthBad(url string) (*httptest.ResponseRecorder, error) {
|
||||
clientCertificate, err := tls.X509KeyPair([]byte(clientCertWrong), []byte(clientKeyWrong))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := "/probe?target=" + url
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
probeHandler(w, r, &tls.Config{
|
||||
Certificates: []tls.Certificate{clientCertificate},
|
||||
RootCAs: certPool(),
|
||||
})
|
||||
})
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
return rr, nil
|
||||
}
|
||||
|
||||
func server() (*httptest.Server, error) {
|
||||
serverCertificate, err := tls.X509KeyPair([]byte(serverCert), []byte(serverKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "Hello world")
|
||||
}))
|
||||
|
||||
server.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCertificate},
|
||||
}
|
||||
|
||||
server.StartTLS()
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func serverClientAuth() (*httptest.Server, error) {
|
||||
certPool := certPool()
|
||||
|
||||
serverCertificate, err := tls.X509KeyPair([]byte(serverCert), []byte(serverKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "Hello world")
|
||||
}))
|
||||
|
||||
server.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCertificate},
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
RootCAs: certPool,
|
||||
ClientCAs: certPool,
|
||||
}
|
||||
|
||||
server.StartTLS()
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func serverExpired() (*httptest.Server, error) {
|
||||
certPool := certPool()
|
||||
|
||||
serverCertificate, err := tls.X509KeyPair([]byte(expiredCert), []byte(expiredKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "Hello world")
|
||||
}))
|
||||
|
||||
server.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCertificate},
|
||||
RootCAs: certPool,
|
||||
ClientCAs: certPool,
|
||||
}
|
||||
|
||||
server.StartTLS()
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func serverHTTP() (*httptest.Server, error) {
|
||||
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "Hello world")
|
||||
}))
|
||||
|
||||
server.Start()
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func certPool() *x509.CertPool {
|
||||
certPool := x509.NewCertPool()
|
||||
certPool.AppendCertsFromPEM([]byte(caCert))
|
||||
return certPool
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: DES-EDE3-CBC,8906E5B87ECB8682
|
||||
|
||||
cY/EOwDILOiCPzyqZanlrhrb5h+fCnS/mHhsNme0Zyrk9udaBZKwxkPtUC6rJZ9/
|
||||
rX4HQch9tuVy992CDe1bl4l+3EfqshUhxtivG8GHiFj7TNo5Ia9LaE+KwugB2rdX
|
||||
J8odwWmwE8Y9TX09WyPKHikjg2nAOgN3Gf+GeJW6fL9xcEVmIgDyX6kBusQYWIQ3
|
||||
PMYqyaBhL8UCZVRpiUINTrJXCnNAI4t6K0i4J+hjyekRnA5PQ+A/0+C9J+kQW+wj
|
||||
uDWkQtyyBNsscGhXrL9ita7/UtJa+/aS99FLTnxD+fqyU4svzNIQWmZBcIxb9Lxq
|
||||
LHcxNaQCtFycD1iU8Tsq9HY1apKrdPBjaaYne6qCg8nIPWpo3gWoffu+R3EfVcdZ
|
||||
lNgKL3Cd1oCJy/Vcz+u0iayCPT0zfP1dLlGpL2U7L7+8pJ88FbXt/F1th9QEUp5z
|
||||
dISg/6ukHyIkfXwoxYXkin9fW2clo99+fuXkJaVPEOnRmr0+kf+1S0Uw8fkete4v
|
||||
f0IUxUwydxCXFN2ZFZ6o/LFLMLRfNAqGSGjqGeY1L0ILkJPo3KBqlmUhl2FIROOW
|
||||
4rSrfpujjsOqlPncJ9apW04RnqhY7t0YOtp0rMK+gIDteKD+utCyh+UAetiiqTiB
|
||||
ZDup/kzPNDClSDU6cgRbZUf/Nt8RiBcVeX4TfDb1eEhqV0BTHrLOBo8yp6ETDlJM
|
||||
CKO5i9kb5fUaFPFD5VTc5rnY4qV/hUj/uyAVK757A+m9nn7fYOMeaRAgOEvChYSf
|
||||
Qam0VPgpJxfjsaw0BFOMyfsHNJqvTbPXA+hIKD3fhP6jNcgpKTLHJp89J4XUOeIR
|
||||
tvh/u7vljhVygP7ZQOpCoX1xSMdJMO7Qhrh3O/2fq/EJertJD2PQ0Zgqb8wosHn/
|
||||
Aw9VWT6849jL55Xd5l3zXmI9vU0Le4HP3NstV9jjpcp91dU9yfQpcuIo8U26u+4r
|
||||
LR1VmhZJjo7FBOpyJZ1Jb3vyp+nPI+tH214DhM9LuXvMbf3ORhXTMOlqSABUmo/+
|
||||
t+QjVfcEuhFR9CTVWZUIXflJk/euvzqTQdm8iz7JuFzQOhoXjiPIq0GqtCk10SeL
|
||||
zqHz1s0TZNcrZyzkmiHuWjGVwHN/XZA3dW67uj522hD7EzucKE9CoCdJ3f4JEWmS
|
||||
CQwEbba6SKAR6iBlouIYLVdkOBgimGiF13rCwvdN234hQeJT8Wc87iG+uDw73PJL
|
||||
+amDglATH4wIpBk4xmjh/GTRDK0yH9jp7Dv9iwShbjk0yOuoz5yDn8VPqRIREn8d
|
||||
9sAaiFUUQ/9XrdlA5F+49OznClDWLKHK8sSAAFyrzvoCcqseSKbvLyrlHGT0fiot
|
||||
obgDu/W+K2xEOjQeaIyVI5J1qOi6k78fyv1vutjEs6mcTRtDAIxi+V5y5lXUEj0v
|
||||
OWYsbp9yb8Yq602vV8UYSROd+1xdE+7Td3ENLYE7MnVqju7a5NRfnZYgAU03NgIf
|
||||
nHGFZC6/tMz/PXS+D0dqzXxwEjH5JQzGBjvSQHK09gHtCfcyshMQQWtXZGQViZX8
|
||||
QdYXiaq67nJex0DjWTt56a4EgsdYC1J28bJ3GAkrWNkDFRmlx49zvA==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1,27 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEnTCCAoWgAwIBAgIJAPC7KMFjfslXMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV
|
||||
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp
|
||||
c2NvMQ8wDQYDVQQKDAZCYWRTU0wxMTAvBgNVBAMMKEJhZFNTTCBDbGllbnQgUm9v
|
||||
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTcxMTE2MDUzNjMzWhcNMTkxMTE2
|
||||
MDUzNjMzWjBvMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQG
|
||||
A1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGQmFkU1NMMSIwIAYDVQQDDBlC
|
||||
YWRTU0wgQ2xpZW50IENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEAxzdfEeseTs/rukjly6MSLHM+Rh0enA3Ai4Mj2sdl31x3SbPoen08
|
||||
utVhjPmlxIUdkiMG4+ffe7N+JtDLG75CaxZp9CxytX7kywooRBJsRnQhmQPca8MR
|
||||
WAJBIz+w/L+3AFkTIqWBfyT+1VO8TVKPkEpGdLDovZOmzZAASi9/sj+j6gM7AaCi
|
||||
DeZTf2ES66abA5pOp60Q6OEdwg/vCUJfarhKDpi9tj3P6qToy9Y4DiBUhOct4MG8
|
||||
w5XwmKAC+Vfm8tb7tMiUoU0yvKKOcL6YXBXxB2kPcOYxYNobXavfVBEdwSrjQ7i/
|
||||
s3o6hkGQlm9F7JPEuVgbl/Jdwa64OYIqjQIDAQABoy0wKzAJBgNVHRMEAjAAMBEG
|
||||
CWCGSAGG+EIBAQQEAwIHgDALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQELBQADggIB
|
||||
AKpzk1ZTunWuof3DIer2Abq7IV3STGeFaoH4TuHdSbmXwC0KuPkv7wVPgPekyRaH
|
||||
b9CBnsreRF7eleD1M63kakhdnA1XIbdJw8sfSDlKdI4emmb4fzdaaPxbrkQ5IxOB
|
||||
QDw5rTUFVPPqFWw1bGP2zrKD1/i1pxUtGM0xem1jR7UZYpsSPs0JCOHKZOmk8OEW
|
||||
Uy+Jp4gRzbMLZ0TrvajGEZXRepjOkXObR81xZGtvTNP2wl1zm13ffwIYdqJUrf1H
|
||||
H4miU9lVX+3/Z+2mVHBWhzBgbTmo06s3uwUE6JsxUGm2/w4NNblRit0uQcGw7ba8
|
||||
kl2d5rZQscFsqNFz2vRjj1G0dO8S3owmuF0izZO9Fqvq0jB6oaUkxcAcTKFSjs2z
|
||||
wy1oy+cu8iO3GRbfAW7U0xzGp9MnkdPS5dHzvhod3/DK0YVskfxZF7M8GhkjT7Qm
|
||||
2EUBQNNMNXC3g/GXTdXOgqqjW5GXahI8Z6Q4OYN6xZwuEhizwKkgojwaww2YgYT9
|
||||
MJXciJZWr3QXvFdBH7m0zwpKgQ1wm6j3yeyuRphq2lEtU3OQl55A3tXtvqyMXsxk
|
||||
xMCCNQdmKQt0WYmMS3Xj/AfAY2sjCWziDflvW5mGCUjSYdZ+r3JIIF4m/FNCIO1d
|
||||
Ioacp9qb0qL9duFlVHtFiPgoKrEdJaNVUL7NG9ppF8pR
|
||||
-----END CERTIFICATE-----
|
||||
Reference in New Issue
Block a user