diff --git a/README.md b/README.md index 0bd5ae41..5a84d81a 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ For more details, see [Usage](#usage) - [Monitoring a service using ICMP](#monitoring-a-service-using-icmp) - [Monitoring a service using DNS queries](#monitoring-a-service-using-dns-queries) - [Monitoring a service using STARTTLS](#monitoring-a-service-using-starttls) + - [Monitoring a service using TLS](#monitoring-a-service-using-tls) - [Basic authentication](#basic-authentication) - [disable-monitoring-lock](#disable-monitoring-lock) - [Reloading configuration on the fly](#reloading-configuration-on-the-fly) @@ -963,6 +964,24 @@ services: - name: starttls-smtp-example url: "starttls://smtp.gmail.com:587" interval: 30m + client: + timeout: 5s + conditions: + - "[CONNECTED] == true" + - "[CERTIFICATE_EXPIRATION] > 48h" +``` + + +### Monitoring a service using TLS +Monitoring services using SSL/TLS encryption, such as LDAP over TLS, can help +detecting certificate expiration: +```yaml +services: + - name: tls-ldaps-example + url: "tls://ldap.example.com:636" + interval: 30m + client: + timeout: 5s conditions: - "[CONNECTED] == true" - "[CERTIFICATE_EXPIRATION] > 48h" diff --git a/client/client.go b/client/client.go index 79933d0f..3e46ca88 100644 --- a/client/client.go +++ b/client/client.go @@ -38,7 +38,11 @@ func CanPerformStartTLS(address string, config *Config) (connected bool, certifi if len(hostAndPort) != 2 { return false, nil, errors.New("invalid address for starttls, format must be host:port") } - smtpClient, err := smtp.Dial(address) + conn, err := net.DialTimeout("tcp", address, config.Timeout) + if err != nil { + return + } + smtpClient, err := smtp.NewClient(conn, hostAndPort[0]) if err != nil { return } @@ -57,6 +61,28 @@ func CanPerformStartTLS(address string, config *Config) (connected bool, certifi return true, certificate, nil } +// CanPerformTLS checks whether a connection can be established to an address using the TLS protocol +func CanPerformTLS(address string, config *Config) (connected bool, certificate *x509.Certificate, err error) { + conn, err := tls.DialWithDialer(&net.Dialer{Timeout: config.Timeout}, "tcp", address, nil) + if err != nil { + return + } + defer conn.Close() + + verifiedChains := conn.ConnectionState().VerifiedChains + if len(verifiedChains) == 0 { + return + } + + chain := verifiedChains[0] // VerifiedChains[0] == PeerCertificates[0] + if len(chain) == 0 { + return + } + + certificate = chain[0] + return true, certificate, nil +} + // Ping checks if an address can be pinged and returns the round-trip time if the address can be pinged // // Note that this function takes at least 100ms, even if the address is 127.0.0.1 diff --git a/client/client_test.go b/client/client_test.go index 7f1046f9..5bbb4d99 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -91,6 +91,56 @@ func TestCanPerformStartTLS(t *testing.T) { } } +func TestCanPerformTLS(t *testing.T) { + type args struct { + address string + insecure bool + } + tests := []struct { + name string + args args + wantConnected bool + wantErr bool + }{ + { + name: "invalid address", + args: args{ + address: "test", + }, + wantConnected: false, + wantErr: true, + }, + { + name: "error dial", + args: args{ + address: "test:1234", + }, + wantConnected: false, + wantErr: true, + }, + { + name: "valid tls", + args: args{ + address: "smtp.gmail.com:465", + }, + wantConnected: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + connected, _, err := CanPerformTLS(tt.args.address, &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second}) + if (err != nil) != tt.wantErr { + t.Errorf("CanPerformTLS() err=%v, wantErr=%v", err, tt.wantErr) + return + } + if connected != tt.wantConnected { + t.Errorf("CanPerformTLS() connected=%v, wantConnected=%v", connected, tt.wantConnected) + } + }) + } +} + func TestCanCreateTCPConnection(t *testing.T) { if CanCreateTCPConnection("127.0.0.1", &Config{Timeout: 5 * time.Second}) { t.Error("should've failed, because there's no port in the address") diff --git a/core/service.go b/core/service.go index 4840e9d5..726d5e03 100644 --- a/core/service.go +++ b/core/service.go @@ -217,7 +217,8 @@ func (service *Service) call(result *Result) { isServiceTCP := strings.HasPrefix(service.URL, "tcp://") isServiceICMP := strings.HasPrefix(service.URL, "icmp://") isServiceStartTLS := strings.HasPrefix(service.URL, "starttls://") - isServiceHTTP := !isServiceDNS && !isServiceTCP && !isServiceICMP && !isServiceStartTLS + isServiceTLS := strings.HasPrefix(service.URL, "tls://") + isServiceHTTP := !isServiceDNS && !isServiceTCP && !isServiceICMP && !isServiceStartTLS && !isServiceTLS if isServiceHTTP { request = service.buildHTTPRequest() } @@ -225,8 +226,18 @@ func (service *Service) call(result *Result) { if isServiceDNS { service.DNS.query(service.URL, result) result.Duration = time.Since(startTime) - } else if isServiceStartTLS { - result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(service.URL, "starttls://"), service.ClientConfig) + } else if isServiceStartTLS || isServiceTLS { + var clientFunction func(address string, config *client.Config) (connected bool, certificate *x509.Certificate, err error) + var addressPrefix string + if isServiceStartTLS { + clientFunction = client.CanPerformStartTLS + addressPrefix = "starttls://" + } else if isServiceTLS { + clientFunction = client.CanPerformTLS + addressPrefix = "tls://" + } + + result.Connected, certificate, err = clientFunction(strings.TrimPrefix(service.URL, addressPrefix), service.ClientConfig) if err != nil { result.AddError(err.Error()) return