k8s.io/apiserver@v0.31.1/pkg/util/x509metrics/server_cert_deprecations.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package x509metrics
    18  
    19  import (
    20  	"crypto/x509"
    21  	"errors"
    22  	"fmt"
    23  	"net/http"
    24  	"reflect"
    25  	"strings"
    26  
    27  	utilnet "k8s.io/apimachinery/pkg/util/net"
    28  	"k8s.io/apiserver/pkg/audit"
    29  	"k8s.io/component-base/metrics"
    30  	"k8s.io/klog/v2"
    31  )
    32  
    33  var _ utilnet.RoundTripperWrapper = &x509DeprecatedCertificateMetricsRTWrapper{}
    34  
    35  type x509DeprecatedCertificateMetricsRTWrapper struct {
    36  	rt http.RoundTripper
    37  
    38  	checkers []deprecatedCertificateAttributeChecker
    39  }
    40  
    41  type deprecatedCertificateAttributeChecker interface {
    42  	// CheckRoundTripError returns true if the err is an error specific
    43  	// to this deprecated certificate attribute
    44  	CheckRoundTripError(err error) bool
    45  	// CheckPeerCertificates returns true if the deprecated attribute/value pair
    46  	// was found in a given certificate in the http.Response.TLS.PeerCertificates bundle
    47  	CheckPeerCertificates(certs []*x509.Certificate) bool
    48  	// IncreaseCounter increases the counter internal to this interface
    49  	// Use the req to derive and log information useful for troubleshooting the certificate issue
    50  	IncreaseMetricsCounter(req *http.Request)
    51  }
    52  
    53  // counterRaiser is a helper structure to include in certificate deprecation checkers.
    54  // It implements the IncreaseMetricsCounter() method so that, when included in the checker,
    55  // it does not have to be reimplemented.
    56  type counterRaiser struct {
    57  	counter *metrics.Counter
    58  	// programmatic id used in log and audit annotations prefixes
    59  	id string
    60  	// human readable explanation
    61  	reason string
    62  }
    63  
    64  func (c *counterRaiser) IncreaseMetricsCounter(req *http.Request) {
    65  	if req != nil && req.URL != nil {
    66  		if hostname := req.URL.Hostname(); len(hostname) > 0 {
    67  			prefix := fmt.Sprintf("%s.invalid-cert.kubernetes.io", c.id)
    68  			klog.Infof("%s: invalid certificate detected connecting to %q: %s", prefix, hostname, c.reason)
    69  			audit.AddAuditAnnotation(req.Context(), prefix+"/"+hostname, c.reason)
    70  		}
    71  	}
    72  	c.counter.Inc()
    73  }
    74  
    75  // NewDeprecatedCertificateRoundTripperWrapperConstructor returns a RoundTripper wrapper that's usable within ClientConfig.Wrap.
    76  //
    77  // It increases the `missingSAN` counter whenever:
    78  //  1. we get a x509.HostnameError with string `x509: certificate relies on legacy Common Name field`
    79  //     which indicates an error caused by the deprecation of Common Name field when veryfing remote
    80  //     hostname
    81  //  2. the server certificate in response contains no SAN. This indicates that this binary run
    82  //     with the GODEBUG=x509ignoreCN=0 in env
    83  //
    84  // It increases the `sha1` counter whenever:
    85  //  1. we get a x509.InsecureAlgorithmError with string `SHA1`
    86  //     which indicates an error caused by an insecure SHA1 signature
    87  //  2. the server certificate in response contains a SHA1WithRSA or ECDSAWithSHA1 signature.
    88  //     This indicates that this binary run with the GODEBUG=x509sha1=1 in env
    89  func NewDeprecatedCertificateRoundTripperWrapperConstructor(missingSAN, sha1 *metrics.Counter) func(rt http.RoundTripper) http.RoundTripper {
    90  	return func(rt http.RoundTripper) http.RoundTripper {
    91  		return &x509DeprecatedCertificateMetricsRTWrapper{
    92  			rt: rt,
    93  			checkers: []deprecatedCertificateAttributeChecker{
    94  				NewSANDeprecatedChecker(missingSAN),
    95  				NewSHA1SignatureDeprecatedChecker(sha1),
    96  			},
    97  		}
    98  	}
    99  }
   100  
   101  func (w *x509DeprecatedCertificateMetricsRTWrapper) RoundTrip(req *http.Request) (*http.Response, error) {
   102  	resp, err := w.rt.RoundTrip(req)
   103  
   104  	if err != nil {
   105  		for _, checker := range w.checkers {
   106  			if checker.CheckRoundTripError(err) {
   107  				checker.IncreaseMetricsCounter(req)
   108  			}
   109  		}
   110  	} else if resp != nil {
   111  		if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 {
   112  			for _, checker := range w.checkers {
   113  				if checker.CheckPeerCertificates(resp.TLS.PeerCertificates) {
   114  					checker.IncreaseMetricsCounter(req)
   115  				}
   116  			}
   117  		}
   118  	}
   119  
   120  	return resp, err
   121  }
   122  
   123  func (w *x509DeprecatedCertificateMetricsRTWrapper) WrappedRoundTripper() http.RoundTripper {
   124  	return w.rt
   125  }
   126  
   127  var _ deprecatedCertificateAttributeChecker = &missingSANChecker{}
   128  
   129  type missingSANChecker struct {
   130  	counterRaiser
   131  }
   132  
   133  func NewSANDeprecatedChecker(counter *metrics.Counter) *missingSANChecker {
   134  	return &missingSANChecker{
   135  		counterRaiser: counterRaiser{
   136  			counter: counter,
   137  			id:      "missing-san",
   138  			reason:  "relies on a legacy Common Name field instead of the SAN extension for subject validation",
   139  		},
   140  	}
   141  }
   142  
   143  // CheckRoundTripError returns true when we're running w/o GODEBUG=x509ignoreCN=0
   144  // and the client reports a HostnameError about the legacy CN fields
   145  func (c *missingSANChecker) CheckRoundTripError(err error) bool {
   146  	if err != nil && errors.As(err, &x509.HostnameError{}) && strings.Contains(err.Error(), "x509: certificate relies on legacy Common Name field") {
   147  		// increase the count of registered failures due to Go 1.15 x509 cert Common Name deprecation
   148  		return true
   149  	}
   150  
   151  	return false
   152  }
   153  
   154  // CheckPeerCertificates returns true when the server response contains
   155  // a leaf certificate w/o the SAN extension
   156  func (c *missingSANChecker) CheckPeerCertificates(peerCertificates []*x509.Certificate) bool {
   157  	if len(peerCertificates) > 0 {
   158  		if serverCert := peerCertificates[0]; !hasSAN(serverCert) {
   159  			return true
   160  		}
   161  	}
   162  
   163  	return false
   164  }
   165  
   166  func hasSAN(c *x509.Certificate) bool {
   167  	sanOID := []int{2, 5, 29, 17}
   168  
   169  	for _, e := range c.Extensions {
   170  		if e.Id.Equal(sanOID) {
   171  			return true
   172  		}
   173  	}
   174  	return false
   175  }
   176  
   177  type sha1SignatureChecker struct {
   178  	*counterRaiser
   179  }
   180  
   181  func NewSHA1SignatureDeprecatedChecker(counter *metrics.Counter) *sha1SignatureChecker {
   182  	return &sha1SignatureChecker{
   183  		counterRaiser: &counterRaiser{
   184  			counter: counter,
   185  			id:      "insecure-sha1",
   186  			reason:  "uses an insecure SHA-1 signature",
   187  		},
   188  	}
   189  }
   190  
   191  // CheckRoundTripError returns true when we're running w/o GODEBUG=x509sha1=1
   192  // and the client reports an InsecureAlgorithmError about a SHA1 signature
   193  func (c *sha1SignatureChecker) CheckRoundTripError(err error) bool {
   194  	var unknownAuthorityError x509.UnknownAuthorityError
   195  	if err == nil {
   196  		return false
   197  	}
   198  	if !errors.As(err, &unknownAuthorityError) {
   199  		return false
   200  	}
   201  
   202  	errMsg := err.Error()
   203  	if strIdx := strings.Index(errMsg, "x509: cannot verify signature: insecure algorithm"); strIdx != -1 && strings.Contains(errMsg[strIdx:], "SHA1") {
   204  		// increase the count of registered failures due to Go 1.18 x509 sha1 signature deprecation
   205  		return true
   206  	}
   207  
   208  	return false
   209  }
   210  
   211  // CheckPeerCertificates returns true when the server response contains
   212  // a non-root non-self-signed  certificate with a deprecated SHA1 signature
   213  func (c *sha1SignatureChecker) CheckPeerCertificates(peerCertificates []*x509.Certificate) bool {
   214  	// check all received non-self-signed certificates for deprecated signing algorithms
   215  	for _, cert := range peerCertificates {
   216  		if cert.SignatureAlgorithm == x509.SHA1WithRSA || cert.SignatureAlgorithm == x509.ECDSAWithSHA1 {
   217  			// the SHA-1 deprecation does not involve self-signed root certificates
   218  			if !reflect.DeepEqual(cert.Issuer, cert.Subject) {
   219  				return true
   220  			}
   221  		}
   222  	}
   223  
   224  	return false
   225  }