7 Commits

Author SHA1 Message Date
Rob Best
1bb799094c Update VERSION 2019-03-19 13:59:29 +00:00
Rob Best
39c4ef7937 Update and add to tests for client authentication 2019-03-18 12:28:10 +00:00
Rob Best
f17849f9b2 Update documentation for client authentication 2019-03-11 18:43:21 +00:00
Rob Best
7e59584659 Remove the relationship between client authentication and the alternative ca cert bundle.
Client authentication isn't the only situation where you might want to override the system ca bundle with an alternative.
2019-03-11 17:53:21 +00:00
Rob Best
645d7a6e42 Add defaults for the cert and key file flags. This could help reduce the number of flags passed to the exporter if the user is able to adhere to default. 2019-03-11 17:51:29 +00:00
Rob Best
1af3b2a3b2 Add initial support for client authentication 2019-03-11 17:21:22 +00:00
Rob Best
7e95d03166 Some cosmetic fixes
* Add comments to exports
* Update incorrect metric description text
* Rename variables that break typical golang conventions
2019-03-11 17:13:29 +00:00
6 changed files with 177 additions and 46 deletions

View File

@@ -19,6 +19,10 @@ Similarly to the blackbox_exporter, visiting [http://localhost:9219/probe?target
## 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")
@@ -77,6 +81,13 @@ Identify instances that have failed to create a valid SSL connection:
ssl_https_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.
## 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.

View File

@@ -1 +1 @@
0.2.0
0.4.0

View File

@@ -4,6 +4,7 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
@@ -48,12 +49,12 @@ var (
)
subjectAlernativeIPs = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_subject_alternative_ips"),
"Subject Alternative DNS Names",
"Subject Alternative IPs",
[]string{"serial_no", "issuer_cn", "ips"}, nil,
)
subjectAlernativeEmailAddresses = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_subject_alternative_emails"),
"Subject Alternative DNS Names",
"Subject Alternative Email Addresses",
[]string{"serial_no", "issuer_cn", "emails"}, nil,
)
subjectOrganizationUnits = prometheus.NewDesc(
@@ -63,12 +64,14 @@ var (
)
)
// Exporter is the exporter type...
type Exporter struct {
target string
timeout time.Duration
insecure bool
target string
timeout time.Duration
tlsConfig *tls.Config
}
// Describe metrics
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- httpsConnectSuccess
ch <- notAfter
@@ -79,11 +82,12 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- subjectOrganizationUnits
}
// Collect metrics
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
// Create the HTTP client and make a get request of the target
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: e.insecure},
TLSClientConfig: e.tlsConfig,
}
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
@@ -114,68 +118,69 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
httpsConnectSuccess, prometheus.GaugeValue, 1,
)
peer_certificates := uniq(resp.TLS.PeerCertificates)
// Remove duplicate certificates from the response
peerCertificates := uniq(resp.TLS.PeerCertificates)
// Loop through returned certificates and create metrics
for _, cert := range peer_certificates {
for _, cert := range peerCertificates {
subject_cn := cert.Subject.CommonName
issuer_cn := cert.Issuer.CommonName
subject_dnsn := cert.DNSNames
subject_emails := cert.EmailAddresses
subject_ips := cert.IPAddresses
serial_no := cert.SerialNumber.String()
subject_ous := cert.Subject.OrganizationalUnit
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
if !cert.NotAfter.IsZero() {
ch <- prometheus.MustNewConstMetric(
notAfter, prometheus.GaugeValue, float64(cert.NotAfter.UnixNano()/1e9), serial_no, issuer_cn,
notAfter, prometheus.GaugeValue, float64(cert.NotAfter.UnixNano()/1e9), serialNum, issuerCN,
)
}
if !cert.NotBefore.IsZero() {
ch <- prometheus.MustNewConstMetric(
notBefore, prometheus.GaugeValue, float64(cert.NotBefore.UnixNano()/1e9), serial_no, issuer_cn,
notBefore, prometheus.GaugeValue, float64(cert.NotBefore.UnixNano()/1e9), serialNum, issuerCN,
)
}
if subject_cn != "" {
if subjectCN != "" {
ch <- prometheus.MustNewConstMetric(
commonName, prometheus.GaugeValue, 1, serial_no, issuer_cn, subject_cn,
commonName, prometheus.GaugeValue, 1, serialNum, issuerCN, subjectCN,
)
}
if len(subject_dnsn) > 0 {
if len(subjectDNSNames) > 0 {
ch <- prometheus.MustNewConstMetric(
subjectAlernativeDNSNames, prometheus.GaugeValue, 1, serial_no, issuer_cn, ","+strings.Join(subject_dnsn, ",")+",",
subjectAlernativeDNSNames, prometheus.GaugeValue, 1, serialNum, issuerCN, ","+strings.Join(subjectDNSNames, ",")+",",
)
}
if len(subject_emails) > 0 {
if len(subjectEmails) > 0 {
ch <- prometheus.MustNewConstMetric(
subjectAlernativeEmailAddresses, prometheus.GaugeValue, 1, serial_no, issuer_cn, ","+strings.Join(subject_emails, ",")+",",
subjectAlernativeEmailAddresses, prometheus.GaugeValue, 1, serialNum, issuerCN, ","+strings.Join(subjectEmails, ",")+",",
)
}
if len(subject_ips) > 0 {
if len(subjectIPs) > 0 {
i := ","
for _, ip := range subject_ips {
for _, ip := range subjectIPs {
i = i + ip.String() + ","
}
ch <- prometheus.MustNewConstMetric(
subjectAlernativeIPs, prometheus.GaugeValue, 1, serial_no, issuer_cn, i,
subjectAlernativeIPs, prometheus.GaugeValue, 1, serialNum, issuerCN, i,
)
}
if len(subject_ous) > 0 {
if len(subjectIPs) > 0 {
ch <- prometheus.MustNewConstMetric(
subjectOrganizationUnits, prometheus.GaugeValue, 1, serial_no, issuer_cn, ","+strings.Join(subject_ous, ",")+",",
subjectOrganizationUnits, prometheus.GaugeValue, 1, serialNum, issuerCN, ","+strings.Join(subjectOUs, ",")+",",
)
}
}
}
func probeHandler(w http.ResponseWriter, r *http.Request, insecure bool) {
func probeHandler(w http.ResponseWriter, r *http.Request, tlsConfig *tls.Config) {
target := r.URL.Query().Get("target")
@@ -199,9 +204,9 @@ func probeHandler(w http.ResponseWriter, r *http.Request, insecure bool) {
timeout := time.Duration((timeoutSeconds) * 1e9)
exporter := &Exporter{
target: target,
timeout: timeout,
insecure: insecure,
target: target,
timeout: timeout,
tlsConfig: tlsConfig,
}
registry := prometheus.NewRegistry()
@@ -239,10 +244,17 @@ func init() {
func main() {
var (
tlsConfig *tls.Config
certificates []tls.Certificate
rootCAs *x509.CertPool
listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9219").String()
metricsPath = kingpin.Flag("web.metrics-path", "Path under which to expose metrics").Default("/metrics").String()
probePath = kingpin.Flag("web.probe-path", "Path under which to expose the probe endpoint").Default("/probe").String()
insecure = kingpin.Flag("tls.insecure", "Skip certificate verification").Default("false").Bool()
clientAuth = kingpin.Flag("tls.client-auth", "Enable client authentication").Default("false").Bool()
caFile = kingpin.Flag("tls.cacert", "Local path to an alternative CA cert bundle").String()
certFile = kingpin.Flag("tls.cert", "Local path to a client certificate file (for client authentication)").Default("cert.pem").String()
keyFile = kingpin.Flag("tls.key", "Local path to a private key file (for client authentication)").Default("key.pem").String()
)
log.AddFlags(kingpin.CommandLine)
@@ -250,12 +262,36 @@ func main() {
kingpin.HelpFlag.Short('h')
kingpin.Parse()
if *caFile != "" {
caCert, err := ioutil.ReadFile(*caFile)
if err != nil {
log.Fatalln(err)
}
rootCAs = x509.NewCertPool()
rootCAs.AppendCertsFromPEM(caCert)
}
if *clientAuth {
cert, err := tls.LoadX509KeyPair(*certFile, *keyFile)
if err != nil {
log.Fatalln(err)
}
certificates = append(certificates, cert)
}
tlsConfig = &tls.Config{
InsecureSkipVerify: *insecure,
Certificates: certificates,
RootCAs: rootCAs,
}
log.Infoln("Starting "+namespace+"_exporter", version.Info())
log.Infoln("Build context", version.BuildContext())
http.Handle(*metricsPath, prometheus.Handler())
http.HandleFunc(*probePath, func(w http.ResponseWriter, r *http.Request) {
probeHandler(w, r, *insecure)
probeHandler(w, r, tlsConfig)
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`<html>

View File

@@ -1,7 +1,11 @@
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"regexp"
@@ -10,29 +14,52 @@ import (
)
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")
}
keyContent, err := ioutil.ReadFile("test/badssl.com-client-key.pem")
if err != nil {
t.Fatalf("Can't read test client certificate key from disk")
}
keyBlock, _ := pem.Decode(keyContent)
keyBlockDecrypted, err := x509.DecryptPEMBlock(keyBlock, []byte("badssl.com"))
if err != nil {
t.Fatalf("Issue decrypting test client key")
}
emptyRootCAs := x509.NewCertPool()
certificate, err := tls.X509KeyPair(certContent, keyBlockDecrypted)
// Test the behaviour of various target URIs
// 'ok' denotes whether we expect a succesful https connection
// 'insecure' denotes whether we ignore invalid certs
cases := []struct {
uri string
ok bool
insecure bool
uri string
ok bool
tlsConfig *tls.Config
}{
// Test against an assumed valid, reachable and functioning HTTPS address
{uri: "https://google.com", ok: true, insecure: false},
{uri: "https://google.com", ok: true, tlsConfig: &tls.Config{}},
// Test against a HTTP address
{uri: "http://google.com", ok: false, insecure: false},
{uri: "http://google.com", ok: false, tlsConfig: &tls.Config{}},
// Test against an expired certificate when we're rejecting invalid certs
{uri: "https://expired.badssl.com", ok: false, insecure: false},
{uri: "https://expired.badssl.com", ok: false, tlsConfig: &tls.Config{}},
// Test against an expired certificate when we're accepting invalid certs
{uri: "https://expired.badssl.com", ok: true, insecure: true},
{uri: "https://expired.badssl.com", ok: true, tlsConfig: &tls.Config{InsecureSkipVerify: true}},
// Test against a target with no protocol
{uri: "google.com", ok: false, insecure: false},
{uri: "google.com", ok: false, tlsConfig: &tls.Config{}},
// Test against a string with spaces
{uri: "with spaces", ok: false, insecure: false},
{uri: "with spaces", ok: false, tlsConfig: &tls.Config{}},
// Test against nothing
{uri: "", ok: false, insecure: false},
{uri: "", ok: false, tlsConfig: &tls.Config{}},
// Test with client authentication
{uri: "https://client.badssl.com", ok: true, tlsConfig: &tls.Config{Certificates: []tls.Certificate{certificate}}},
// Test with an empty root CA bundle
{uri: "https://google.com", ok: false, tlsConfig: &tls.Config{RootCAs: emptyRootCAs}},
}
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.")
@@ -47,7 +74,7 @@ func TestProbeHandler(t *testing.T) {
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
probeHandler(w, r, test.insecure)
probeHandler(w, r, test.tlsConfig)
})
handler.ServeHTTP(rr, req)

View File

@@ -0,0 +1,30 @@
-----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-----

View File

@@ -0,0 +1,27 @@
-----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-----