2023-01-27 13:37:20 -08:00
|
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
2022-03-17 20:00:54 -07:00
|
|
|
|
|
|
|
|
|
package prober
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/tls"
|
2022-10-12 18:41:38 +01:00
|
|
|
|
"crypto/x509"
|
2022-03-17 20:00:54 -07:00
|
|
|
|
"fmt"
|
2022-10-12 18:41:38 +01:00
|
|
|
|
"io"
|
2022-03-17 20:00:54 -07:00
|
|
|
|
"net"
|
2022-10-12 18:41:38 +01:00
|
|
|
|
"net/http"
|
2024-02-19 08:56:58 -08:00
|
|
|
|
"net/netip"
|
2022-03-17 20:00:54 -07:00
|
|
|
|
"time"
|
2022-10-12 18:41:38 +01:00
|
|
|
|
|
|
|
|
|
"tailscale.com/util/multierr"
|
2022-03-17 20:00:54 -07:00
|
|
|
|
)
|
|
|
|
|
|
2022-10-12 18:41:38 +01:00
|
|
|
|
const expiresSoon = 7 * 24 * time.Hour // 7 days from now
|
2025-05-12 10:25:31 -04:00
|
|
|
|
// Let’s Encrypt promises to issue certificates with CRL servers after 2025-05-07:
|
|
|
|
|
// https://letsencrypt.org/2024/12/05/ending-ocsp/
|
|
|
|
|
// https://github.com/tailscale/tailscale/issues/15912
|
|
|
|
|
const letsEncryptStartedStaplingCRL int64 = 1746576000 // 2025-05-07 00:00:00 UTC
|
2022-10-12 18:41:38 +01:00
|
|
|
|
|
2022-03-17 20:00:54 -07:00
|
|
|
|
// TLS returns a Probe that healthchecks a TLS endpoint.
|
|
|
|
|
//
|
2024-02-19 08:56:58 -08:00
|
|
|
|
// The ProbeFunc connects to a hostPort (host:port string), does a TLS
|
2022-10-12 18:41:38 +01:00
|
|
|
|
// handshake, verifies that the hostname matches the presented certificate,
|
|
|
|
|
// checks certificate validity time and OCSP revocation status.
|
2024-03-27 15:13:34 +00:00
|
|
|
|
func TLS(hostPort string) ProbeClass {
|
|
|
|
|
return ProbeClass{
|
|
|
|
|
Probe: func(ctx context.Context) error {
|
|
|
|
|
certDomain, _, err := net.SplitHostPort(hostPort)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return probeTLS(ctx, certDomain, hostPort)
|
|
|
|
|
},
|
|
|
|
|
Class: "tls",
|
2022-03-17 20:00:54 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-19 08:56:58 -08:00
|
|
|
|
// TLSWithIP is like TLS, but dials the provided dialAddr instead
|
|
|
|
|
// of using DNS resolution. The certDomain is the expected name in
|
|
|
|
|
// the cert (and the SNI name to send).
|
2024-03-27 15:13:34 +00:00
|
|
|
|
func TLSWithIP(certDomain string, dialAddr netip.AddrPort) ProbeClass {
|
|
|
|
|
return ProbeClass{
|
|
|
|
|
Probe: func(ctx context.Context) error {
|
|
|
|
|
return probeTLS(ctx, certDomain, dialAddr.String())
|
|
|
|
|
},
|
|
|
|
|
Class: "tls",
|
2022-10-12 18:41:38 +01:00
|
|
|
|
}
|
2024-02-19 08:56:58 -08:00
|
|
|
|
}
|
2022-10-12 18:41:38 +01:00
|
|
|
|
|
2024-02-19 08:56:58 -08:00
|
|
|
|
func probeTLS(ctx context.Context, certDomain string, dialHostPort string) error {
|
|
|
|
|
dialer := &tls.Dialer{Config: &tls.Config{ServerName: certDomain}}
|
|
|
|
|
conn, err := dialer.DialContext(ctx, "tcp", dialHostPort)
|
2022-03-17 20:00:54 -07:00
|
|
|
|
if err != nil {
|
2024-02-19 08:56:58 -08:00
|
|
|
|
return fmt.Errorf("connecting to %q: %w", dialHostPort, err)
|
2022-03-17 20:00:54 -07:00
|
|
|
|
}
|
2022-10-12 18:41:38 +01:00
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
|
|
tlsConnState := conn.(*tls.Conn).ConnectionState()
|
|
|
|
|
return validateConnState(ctx, &tlsConnState)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// validateConnState verifies certificate validity time in all certificates
|
|
|
|
|
// returned by the TLS server and checks OCSP revocation status for the
|
|
|
|
|
// leaf cert.
|
|
|
|
|
func validateConnState(ctx context.Context, cs *tls.ConnectionState) (returnerr error) {
|
|
|
|
|
var errs []error
|
|
|
|
|
defer func() {
|
|
|
|
|
returnerr = multierr.New(errs...)
|
|
|
|
|
}()
|
|
|
|
|
latestAllowedExpiration := time.Now().Add(expiresSoon)
|
|
|
|
|
|
|
|
|
|
var leafCert *x509.Certificate
|
|
|
|
|
var issuerCert *x509.Certificate
|
|
|
|
|
var leafAuthorityKeyID string
|
|
|
|
|
// PeerCertificates will never be len == 0 on the client side
|
|
|
|
|
for i, cert := range cs.PeerCertificates {
|
|
|
|
|
if i == 0 {
|
|
|
|
|
leafCert = cert
|
|
|
|
|
leafAuthorityKeyID = string(cert.AuthorityKeyId)
|
|
|
|
|
}
|
|
|
|
|
if i > 0 {
|
|
|
|
|
if leafAuthorityKeyID == string(cert.SubjectKeyId) {
|
|
|
|
|
issuerCert = cert
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Do not check certificate validity period for self-signed certs.
|
|
|
|
|
// The practical reason is to avoid raising alerts for expiring
|
|
|
|
|
// DERP metaCert certificates that are returned as part of regular
|
|
|
|
|
// TLS handshake.
|
|
|
|
|
if string(cert.SubjectKeyId) == string(cert.AuthorityKeyId) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if time.Now().Before(cert.NotBefore) {
|
|
|
|
|
errs = append(errs, fmt.Errorf("one of the certs has NotBefore in the future (%v): %v", cert.NotBefore, cert.Subject))
|
|
|
|
|
}
|
|
|
|
|
if latestAllowedExpiration.After(cert.NotAfter) {
|
|
|
|
|
left := cert.NotAfter.Sub(time.Now())
|
|
|
|
|
errs = append(errs, fmt.Errorf("one of the certs expires in %v: %v", left, cert.Subject))
|
|
|
|
|
}
|
2022-03-17 20:00:54 -07:00
|
|
|
|
}
|
2022-10-12 18:41:38 +01:00
|
|
|
|
|
2025-05-12 10:25:31 -04:00
|
|
|
|
if len(leafCert.CRLDistributionPoints) == 0 {
|
|
|
|
|
if leafCert.NotBefore.Before(time.Unix(letsEncryptStartedStaplingCRL, 0)) {
|
|
|
|
|
// Certificate might not have a CRL.
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
errs = append(errs, fmt.Errorf("no CRL server presented in leaf cert for %v", leafCert.Subject))
|
2022-10-12 18:41:38 +01:00
|
|
|
|
return
|
2022-03-17 20:00:54 -07:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-12 10:25:31 -04:00
|
|
|
|
err := checkCertCRL(ctx, leafCert.CRLDistributionPoints[0], leafCert, issuerCert)
|
2022-10-12 18:41:38 +01:00
|
|
|
|
if err != nil {
|
2025-05-12 10:25:31 -04:00
|
|
|
|
errs = append(errs, fmt.Errorf("CRL verification failed for %v: %w", leafCert.Subject, err))
|
2022-10-12 18:41:38 +01:00
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-12 10:25:31 -04:00
|
|
|
|
func checkCertCRL(ctx context.Context, crlURL string, leafCert, issuerCert *x509.Certificate) error {
|
|
|
|
|
hreq, err := http.NewRequestWithContext(ctx, "GET", crlURL, nil)
|
2022-10-12 18:41:38 +01:00
|
|
|
|
if err != nil {
|
2025-05-12 10:25:31 -04:00
|
|
|
|
return fmt.Errorf("could not create CRL GET request: %w", err)
|
2022-10-12 18:41:38 +01:00
|
|
|
|
}
|
|
|
|
|
hresp, err := http.DefaultClient.Do(hreq)
|
|
|
|
|
if err != nil {
|
2025-05-12 10:25:31 -04:00
|
|
|
|
return fmt.Errorf("CRL request failed: %w", err)
|
2022-10-12 18:41:38 +01:00
|
|
|
|
}
|
|
|
|
|
defer hresp.Body.Close()
|
|
|
|
|
if hresp.StatusCode != http.StatusOK {
|
2025-05-12 10:25:31 -04:00
|
|
|
|
return fmt.Errorf("crl: non-200 status code from CRL server: %s", hresp.Status)
|
2022-10-12 18:41:38 +01:00
|
|
|
|
}
|
|
|
|
|
lr := io.LimitReader(hresp.Body, 10<<20) // 10MB
|
2025-05-12 10:25:31 -04:00
|
|
|
|
crlB, err := io.ReadAll(lr)
|
2022-10-12 18:41:38 +01:00
|
|
|
|
if err != nil {
|
2025-05-12 10:25:31 -04:00
|
|
|
|
return err
|
2022-10-12 18:41:38 +01:00
|
|
|
|
}
|
2025-05-12 10:25:31 -04:00
|
|
|
|
|
|
|
|
|
crl, err := x509.ParseRevocationList(crlB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("could not parse CRL: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := crl.CheckSignatureFrom(issuerCert); err != nil {
|
|
|
|
|
return fmt.Errorf("could not verify CRL signature: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, revoked := range crl.RevokedCertificateEntries {
|
|
|
|
|
if revoked.SerialNumber.Cmp(leafCert.SerialNumber) == 0 {
|
|
|
|
|
return fmt.Errorf("cert for %v has been revoked on %v, reason: %v", leafCert.Subject, revoked.RevocationTime, revoked.ReasonCode)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
2022-03-17 20:00:54 -07:00
|
|
|
|
}
|