Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new probe_ssl_latest_verified_chain_expiry metric #636

Merged
Merged
9 changes: 9 additions & 0 deletions prober/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
Help: "Returns earliest SSL cert expiry in unixtime",
})

probeSSLLastChainExpiryTimestampSeconds = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "probe_ssl_last_chain_expiry_timestamp_seconds",
Help: "Returns last SSL chain expiry in timestamp seconds",
})

probeTLSVersion = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "probe_tls_version_info",
Expand Down Expand Up @@ -549,6 +554,10 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
registry.MustRegister(probeSSLEarliestCertExpiryGauge, probeTLSVersion)
probeSSLEarliestCertExpiryGauge.Set(float64(getEarliestCertExpiry(resp.TLS).Unix()))
probeTLSVersion.WithLabelValues(getTLSVersion(resp.TLS)).Set(1)
if lastChainExpiry, ok := getLastChainExpiry(resp.TLS); ok {
registry.MustRegister(probeSSLLastChainExpiryTimestampSeconds)
probeSSLLastChainExpiryTimestampSeconds.Set(float64(lastChainExpiry.Unix()))
}
if httpConfig.FailIfSSL {
level.Error(logger).Log("msg", "Final request was over SSL")
success = false
Expand Down
7 changes: 6 additions & 1 deletion prober/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package prober
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
Expand Down Expand Up @@ -597,7 +599,9 @@ func TestTLSConfigIsIgnoredForPlainHTTP(t *testing.T) {
func TestHTTPUsesTargetAsTLSServerName(t *testing.T) {
// Create test certificates valid for 1 day.
certExpiry := time.Now().AddDate(0, 0, 1)
testcertPem, testKeyPem := generateTestCertificate(certExpiry, false)
testCertTmpl := generateCertificateTemplate(certExpiry, false)
testCertTmpl.IsCA = true
_, testcertPem, testKey := generateSelfSignedCertificate(testCertTmpl)

// CAFile must be passed via filesystem, use a tempfile.
tmpCaFile, err := ioutil.TempFile("", "cafile.pem")
Expand All @@ -612,6 +616,7 @@ func TestHTTPUsesTargetAsTLSServerName(t *testing.T) {
}
defer os.Remove(tmpCaFile.Name())

testKeyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(testKey)})
testcert, err := tls.X509KeyPair(testcertPem, testKeyPem)
if err != nil {
panic(fmt.Sprintf("Failed to decode TLS testing keypair: %s\n", err))
Expand Down
15 changes: 15 additions & 0 deletions prober/tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ func ProbeTCP(ctx context.Context, target string, module config.Module, registry
Name: "probe_ssl_earliest_cert_expiry",
Help: "Returns earliest SSL cert expiry date",
})
probeSSLLastChainExpiryTimestampSeconds := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "probe_ssl_last_chain_expiry_timestamp_seconds",
Help: "Returns last SSL chain expiry in timestamp seconds",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in unixtime

Looks like the existing metric help could do with some consistency too.

})
probeTLSVersion := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "probe_tls_version_info",
Expand Down Expand Up @@ -128,6 +132,11 @@ func ProbeTCP(ctx context.Context, target string, module config.Module, registry
registry.MustRegister(probeSSLEarliestCertExpiry, probeTLSVersion)
probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix()))
probeTLSVersion.WithLabelValues(getTLSVersion(&state)).Set(1)

if lastChainExpiry, ok := getLastChainExpiry(&state); ok {
registry.MustRegister(probeSSLLastChainExpiryTimestampSeconds)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep this consistent with the other tls metric, both in terms of registration and what happens when there's no certs.

probeSSLLastChainExpiryTimestampSeconds.Set(float64(lastChainExpiry.Unix()))
}
}
scanner := bufio.NewScanner(conn)
for i, qr := range module.TCP.QueryResponse {
Expand Down Expand Up @@ -197,6 +206,12 @@ func ProbeTCP(ctx context.Context, target string, module config.Module, registry
registry.MustRegister(probeSSLEarliestCertExpiry)
probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix()))
probeTLSVersion.WithLabelValues(getTLSVersion(&state)).Set(1)

if lastChainExpiry, ok := getLastChainExpiry(&state); ok {
registry.MustRegister(probeSSLLastChainExpiryTimestampSeconds)
probeSSLLastChainExpiryTimestampSeconds.Set(float64(lastChainExpiry.Unix()))
}

}
}
return true
Expand Down
175 changes: 169 additions & 6 deletions prober/tcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@
package prober

import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net"
Expand Down Expand Up @@ -84,14 +89,16 @@ func TestTCPConnectionWithTLS(t *testing.T) {

// Create test certificates valid for 1 day.
certExpiry := time.Now().AddDate(0, 0, 1)
testcert_pem, testkey_pem := generateTestCertificate(certExpiry, false)
rootCertTmpl := generateCertificateTemplate(certExpiry, false)
rootCertTmpl.IsCA = true
_, rootCertPem, rootKey := generateSelfSignedCertificate(rootCertTmpl)

// CAFile must be passed via filesystem, use a tempfile.
tmpCaFile, err := ioutil.TempFile("", "cafile.pem")
if err != nil {
t.Fatalf(fmt.Sprintf("Error creating CA tempfile: %s", err))
}
if _, err := tmpCaFile.Write(testcert_pem); err != nil {
if _, err := tmpCaFile.Write(rootCertPem); err != nil {
t.Fatalf(fmt.Sprintf("Error writing CA tempfile: %s", err))
}
if err := tmpCaFile.Close(); err != nil {
Expand All @@ -109,7 +116,8 @@ func TestTCPConnectionWithTLS(t *testing.T) {
}
defer conn.Close()

testcert, err := tls.X509KeyPair(testcert_pem, testkey_pem)
rootKeyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rootKey)})
testcert, err := tls.X509KeyPair(rootCertPem, rootKeyPem)
if err != nil {
panic(fmt.Sprintf("Failed to decode TLS testing keypair: %s\n", err))
}
Expand Down Expand Up @@ -193,6 +201,158 @@ func TestTCPConnectionWithTLS(t *testing.T) {
checkRegistryResults(expectedResults, mfs, t)
}

func TestTCPConnectionWithTLSAndVerifiedCertificateChain(t *testing.T) {
if os.Getenv("TRAVIS") == "true" {
t.Skip("skipping; travisci is failing on ipv6 dns requests")
}

ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Error listening on socket: %s", err)
}
defer ln.Close()
_, listenPort, _ := net.SplitHostPort(ln.Addr().String())

testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

rootPrivatekey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(fmt.Sprintf("Error creating rsa key: %s", err))
}

// Prepare certificates to simulate a situation where
// the root certificate in the chain sent by the server
// expired but a verified certificate chain can be found
// because a new one has installed on the local machine.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"one" is ambiguous here. Root?

This also feels like we're testing Go's root handling rather than testing our own code. Having two chains where one is expired would seem like a better test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I made a change to do that in d45159b


rootCertExpiry := time.Now().AddDate(0, 0, 1)
rootCertTmpl := generateCertificateTemplate(rootCertExpiry, false)
rootCertTmpl.IsCA = true
_, rootCertPem := generateSelfSignedCertificateWithPrivateKey(rootCertTmpl, rootPrivatekey)

oldRootCertExpiry := time.Now().AddDate(0, 0, -1)
expiredRootCertTmpl := generateCertificateTemplate(oldRootCertExpiry, false)
expiredRootCertTmpl.IsCA = true
expiredRootCert, expiredRootCertPem := generateSelfSignedCertificateWithPrivateKey(expiredRootCertTmpl, rootPrivatekey)

intermediateCertTmpl := generateCertificateTemplate(rootCertExpiry, false)
intermediateCertTmpl.IsCA = true
intermediateCert, intermediateCertPem, intermediateKey := generateSignedCertificate(intermediateCertTmpl, expiredRootCert, rootPrivatekey)

leafCertImpl := generateCertificateTemplate(rootCertExpiry, false)
_, leafCertPem, leafKey := generateSignedCertificate(leafCertImpl, intermediateCert, intermediateKey)

// CAFile must be passed via filesystem, use a tempfile.
tmpCaFile, err := ioutil.TempFile("", "cafile.pem")
if err != nil {
t.Fatalf(fmt.Sprintf("Error creating CA tempfile: %s", err))
}
if _, err := tmpCaFile.Write(rootCertPem); err != nil {
t.Fatalf(fmt.Sprintf("Error writing CA tempfile: %s", err))
}
if err := tmpCaFile.Close(); err != nil {
t.Fatalf(fmt.Sprintf("Error closing CA tempfile: %s", err))
}
defer os.Remove(tmpCaFile.Name())

ch := make(chan (struct{}))
logger := log.NewNopLogger()
// Handle server side of this test.
serverFunc := func() {
conn, err := ln.Accept()
if err != nil {
panic(fmt.Sprintf("Error accepting on socket: %s", err))
}
defer conn.Close()

expiredRootKeyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rootPrivatekey)})
intermediateKeyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(intermediateKey)})
leafKeyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey)})

certPem := bytes.Join([][]byte{leafCertPem, intermediateCertPem, expiredRootCertPem}, []byte("\n"))
keyPem := bytes.Join([][]byte{leafKeyPem, intermediateKeyPem, expiredRootKeyPem}, []byte("\n"))

keypair, err := tls.X509KeyPair(certPem, keyPem)
if err != nil {
panic(fmt.Sprintf("Failed to decode TLS testing keypair: %s\n", err))
}

// Immediately upgrade to TLS.
tlsConfig := &tls.Config{
ServerName: "localhost",
Certificates: []tls.Certificate{keypair},
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12,
}
tlsConn := tls.Server(conn, tlsConfig)
defer tlsConn.Close()
if err := tlsConn.Handshake(); err != nil {
level.Error(logger).Log("msg", "Error TLS Handshake (server) failed", "err", err)
} else {
// Send some bytes before terminating the connection.
fmt.Fprintf(tlsConn, "Hello World!\n")
}
ch <- struct{}{}
}

// Expect name-verified TLS connection.
module := config.Module{
TCP: config.TCPProbe{
IPProtocol: "ip4",
IPProtocolFallback: true,
TLS: true,
TLSConfig: pconfig.TLSConfig{
CAFile: tmpCaFile.Name(),
InsecureSkipVerify: false,
},
},
}

registry := prometheus.NewRegistry()
go serverFunc()
// Test name-verification with name from target.
target := net.JoinHostPort("localhost", listenPort)
if !ProbeTCP(testCTX, target, module, registry, log.NewNopLogger()) {
t.Fatalf("TCP module failed, expected success.")
}
<-ch

// Check the resulting metrics.
mfs, err := registry.Gather()
if err != nil {
t.Fatal(err)
}

// Check values
expectedResults := map[string]float64{
"probe_ssl_earliest_cert_expiry": float64(oldRootCertExpiry.Unix()),
"probe_ssl_last_chain_expiry_timestamp_seconds": float64(rootCertExpiry.Unix()),
"probe_tls_version_info": 1,
}
checkRegistryResults(expectedResults, mfs, t)

module.TCP.TLSConfig.InsecureSkipVerify = true

registry = prometheus.NewRegistry()
go serverFunc()
if !ProbeTCP(testCTX, target, module, registry, log.NewNopLogger()) {
t.Fatalf("TCP module failed, expected success.")
}
<-ch

mfs, err = registry.Gather()
if err != nil {
t.Fatal(err)
}

for i := range mfs {
if mfs[i].GetName() == "probe_ssl_last_chain_expiry_timestamp_seconds" {
t.Fatalf("Unexpected metric probe_ssl_last_chain_expiry_timestamp_seconds found in returned metrics")
}
}
}

func TestTCPConnectionQueryResponseStartTLS(t *testing.T) {
ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
Expand All @@ -205,14 +365,16 @@ func TestTCPConnectionQueryResponseStartTLS(t *testing.T) {

// Create test certificates valid for 1 day.
certExpiry := time.Now().AddDate(0, 0, 1)
testcert_pem, testkey_pem := generateTestCertificate(certExpiry, true)
testCertTmpl := generateCertificateTemplate(certExpiry, true)
testCertTmpl.IsCA = true
_, testCertPem, testKey := generateSelfSignedCertificate(testCertTmpl)

// CAFile must be passed via filesystem, use a tempfile.
tmpCaFile, err := ioutil.TempFile("", "cafile.pem")
if err != nil {
t.Fatalf(fmt.Sprintf("Error creating CA tempfile: %s", err))
}
if _, err := tmpCaFile.Write(testcert_pem); err != nil {
if _, err := tmpCaFile.Write(testCertPem); err != nil {
t.Fatalf(fmt.Sprintf("Error writing CA tempfile: %s", err))
}
if err := tmpCaFile.Close(); err != nil {
Expand Down Expand Up @@ -263,7 +425,8 @@ func TestTCPConnectionQueryResponseStartTLS(t *testing.T) {
}
fmt.Fprintf(conn, "220 2.0.0 Ready to start TLS\n")

testcert, err := tls.X509KeyPair(testcert_pem, testkey_pem)
testKeyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(testKey)})
testcert, err := tls.X509KeyPair(testCertPem, testKeyPem)
if err != nil {
panic(fmt.Sprintf("Failed to decode TLS testing keypair: %s\n", err))
}
Expand Down
20 changes: 20 additions & 0 deletions prober/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ func getEarliestCertExpiry(state *tls.ConnectionState) time.Time {
return earliest
}

func getLastChainExpiry(state *tls.ConnectionState) (time.Time, bool) {
lastChainExpiry := time.Time{}
if len(state.VerifiedChains) == 0 {
return lastChainExpiry, false
}
for _, chain := range state.VerifiedChains {
earliestCertExpiry := time.Time{}
for _, cert := range chain {
if (earliestCertExpiry.IsZero() || cert.NotAfter.Before(earliestCertExpiry)) && !cert.NotAfter.IsZero() {
earliestCertExpiry = cert.NotAfter
}
}
if lastChainExpiry.IsZero() || lastChainExpiry.After(earliestCertExpiry) {
lastChainExpiry = earliestCertExpiry
}

}
return lastChainExpiry, true
}

func getTLSVersion(state *tls.ConnectionState) string {
switch state.Version {
case tls.VersionTLS10:
Expand Down
Loading