216 lines
7.7 KiB
Go
216 lines
7.7 KiB
Go
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||
|
// SPDX-License-Identifier: Apache-2.0
|
||
|
|
||
|
package ptls
|
||
|
|
||
|
import (
|
||
|
"crypto/tls"
|
||
|
"crypto/x509"
|
||
|
"fmt"
|
||
|
"net/http"
|
||
|
"sync"
|
||
|
|
||
|
"k8s.io/apiserver/pkg/admission"
|
||
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||
|
"k8s.io/apiserver/pkg/server/options"
|
||
|
kubeinformers "k8s.io/client-go/informers"
|
||
|
"k8s.io/client-go/kubernetes"
|
||
|
"k8s.io/client-go/rest"
|
||
|
"k8s.io/client-go/transport"
|
||
|
)
|
||
|
|
||
|
// TODO decide if we need to expose the four TLS levels (secure, default, default-ldap, legacy) as config.
|
||
|
|
||
|
type ConfigFunc func(*x509.CertPool) *tls.Config
|
||
|
|
||
|
func Default(rootCAs *x509.CertPool) *tls.Config {
|
||
|
return &tls.Config{
|
||
|
// Can't use SSLv3 because of POODLE and BEAST
|
||
|
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
|
||
|
// Can't use TLSv1.1 because of RC4 cipher usage
|
||
|
//
|
||
|
// The Kubernetes API Server must use TLS 1.2, at a minimum,
|
||
|
// to protect the confidentiality of sensitive data during electronic dissemination.
|
||
|
// https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242378
|
||
|
MinVersion: tls.VersionTLS12,
|
||
|
|
||
|
// the order does not matter in go 1.17+ https://go.dev/blog/tls-cipher-suites
|
||
|
// we match crypto/tls.cipherSuitesPreferenceOrder because it makes unit tests easier to write
|
||
|
// this list is ignored when TLS 1.3 is used
|
||
|
//
|
||
|
// as of 2021-10-19, Mozilla Guideline v5.6, Go 1.17.2, intermediate configuration, supports:
|
||
|
// - Firefox 27
|
||
|
// - Android 4.4.2
|
||
|
// - Chrome 31
|
||
|
// - Edge
|
||
|
// - IE 11 on Windows 7
|
||
|
// - Java 8u31
|
||
|
// - OpenSSL 1.0.1
|
||
|
// - Opera 20
|
||
|
// - Safari 9
|
||
|
// https://ssl-config.mozilla.org/#server=go&version=1.17.2&config=intermediate&guideline=5.6
|
||
|
//
|
||
|
// The Kubernetes API server must use approved cipher suites.
|
||
|
// https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242418
|
||
|
CipherSuites: []uint16{
|
||
|
// these are all AEADs with ECDHE, some use ChaCha20Poly1305 while others use AES-GCM
|
||
|
// this provides forward secrecy, confidentiality and authenticity of data
|
||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||
|
},
|
||
|
|
||
|
// enable HTTP2 for go's 1.7 HTTP Server
|
||
|
// setting this explicitly is only required in very specific circumstances
|
||
|
// it is simpler to just set it here than to try and determine if we need to
|
||
|
NextProtos: []string{"h2", "http/1.1"},
|
||
|
|
||
|
// optional root CAs, nil means use the host's root CA set
|
||
|
RootCAs: rootCAs,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func Secure(rootCAs *x509.CertPool) *tls.Config {
|
||
|
// as of 2021-10-19, Mozilla Guideline v5.6, Go 1.17.2, modern configuration, supports:
|
||
|
// - Firefox 63
|
||
|
// - Android 10.0
|
||
|
// - Chrome 70
|
||
|
// - Edge 75
|
||
|
// - Java 11
|
||
|
// - OpenSSL 1.1.1
|
||
|
// - Opera 57
|
||
|
// - Safari 12.1
|
||
|
// https://ssl-config.mozilla.org/#server=go&version=1.17.2&config=modern&guideline=5.6
|
||
|
c := Default(rootCAs)
|
||
|
c.MinVersion = tls.VersionTLS13 // max out the security
|
||
|
c.CipherSuites = []uint16{
|
||
|
// TLS 1.3 ciphers are not configurable, but we need to explicitly set them here to make our client hello behave correctly
|
||
|
// See https://github.com/golang/go/pull/49293
|
||
|
tls.TLS_AES_128_GCM_SHA256,
|
||
|
tls.TLS_AES_256_GCM_SHA384,
|
||
|
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||
|
}
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
func DefaultLDAP(rootCAs *x509.CertPool) *tls.Config {
|
||
|
c := Default(rootCAs)
|
||
|
// add less secure ciphers to support the default AWS Active Directory config
|
||
|
c.CipherSuites = append(c.CipherSuites,
|
||
|
// CBC with ECDHE
|
||
|
// this provides forward secrecy and confidentiality of data but not authenticity
|
||
|
// MAC-then-Encrypt CBC ciphers are susceptible to padding oracle attacks
|
||
|
// See https://crypto.stackexchange.com/a/205 and https://crypto.stackexchange.com/a/224
|
||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||
|
)
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
func Legacy(rootCAs *x509.CertPool) *tls.Config {
|
||
|
c := Default(rootCAs)
|
||
|
// add all the ciphers (even the crappy ones) except the ones that Go considers to be outright broken like 3DES
|
||
|
c.CipherSuites = suitesToIDs(tls.CipherSuites())
|
||
|
return c
|
||
|
}
|
||
|
|
||
|
func suitesToIDs(suites []*tls.CipherSuite) []uint16 {
|
||
|
out := make([]uint16, 0, len(suites))
|
||
|
for _, suite := range suites {
|
||
|
suite := suite
|
||
|
out = append(out, suite.ID)
|
||
|
}
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
func Merge(tlsConfigFunc ConfigFunc, tlsConfig *tls.Config) {
|
||
|
secureTLSConfig := tlsConfigFunc(nil)
|
||
|
|
||
|
// override the core security knobs of the TLS config
|
||
|
// note that these have to be kept in sync with Default / Secure above
|
||
|
tlsConfig.MinVersion = secureTLSConfig.MinVersion
|
||
|
tlsConfig.CipherSuites = secureTLSConfig.CipherSuites
|
||
|
|
||
|
// if the TLS config already states what protocols it wants to use, honor that instead of overriding
|
||
|
if len(tlsConfig.NextProtos) == 0 {
|
||
|
tlsConfig.NextProtos = secureTLSConfig.NextProtos
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// RestConfigFunc allows this package to not depend on the kubeclient package.
|
||
|
type RestConfigFunc func(*rest.Config) (kubernetes.Interface, *rest.Config, error)
|
||
|
|
||
|
func DefaultRecommendedOptions(opts *options.RecommendedOptions, f RestConfigFunc) error {
|
||
|
defaultServing(opts.SecureServing)
|
||
|
return secureClient(opts, f)
|
||
|
}
|
||
|
|
||
|
func SecureRecommendedOptions(opts *options.RecommendedOptions, f RestConfigFunc) error {
|
||
|
secureServing(opts.SecureServing)
|
||
|
return secureClient(opts, f)
|
||
|
}
|
||
|
|
||
|
func defaultServing(opts *options.SecureServingOptionsWithLoopback) {
|
||
|
c := Default(nil)
|
||
|
cipherSuites := make([]string, 0, len(c.CipherSuites))
|
||
|
for _, id := range c.CipherSuites {
|
||
|
cipherSuites = append(cipherSuites, tls.CipherSuiteName(id))
|
||
|
}
|
||
|
opts.CipherSuites = cipherSuites
|
||
|
|
||
|
opts.MinTLSVersion = "VersionTLS12"
|
||
|
}
|
||
|
|
||
|
func secureServing(opts *options.SecureServingOptionsWithLoopback) {
|
||
|
opts.MinTLSVersion = "VersionTLS13"
|
||
|
opts.CipherSuites = nil
|
||
|
}
|
||
|
|
||
|
func secureClient(opts *options.RecommendedOptions, f RestConfigFunc) error {
|
||
|
inClusterClient, inClusterConfig, err := f(nil)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("failed to build in cluster client: %w", err)
|
||
|
}
|
||
|
|
||
|
if n, z := opts.Authentication.RemoteKubeConfigFile, opts.Authorization.RemoteKubeConfigFile; len(n) > 0 || len(z) > 0 {
|
||
|
return fmt.Errorf("delgating auth is not using in-cluster config:\nauthentication=%s\nauthorization=%s", n, z)
|
||
|
}
|
||
|
|
||
|
// delegated authn and authz provide easy hooks for us to set the TLS config.
|
||
|
// however, the underlying clients use client-go's global TLS cache with an
|
||
|
// in-cluster config. to make this safe, we simply do the mutation once.
|
||
|
wrapperFunc := wrapTransportOnce(inClusterConfig.WrapTransport)
|
||
|
opts.Authentication.CustomRoundTripperFn = wrapperFunc
|
||
|
opts.Authorization.CustomRoundTripperFn = wrapperFunc
|
||
|
|
||
|
opts.CoreAPI = nil // set this to nil to make sure our ExtraAdmissionInitializers is used
|
||
|
baseExtraAdmissionInitializers := opts.ExtraAdmissionInitializers
|
||
|
opts.ExtraAdmissionInitializers = func(c *genericapiserver.RecommendedConfig) ([]admission.PluginInitializer, error) {
|
||
|
// abuse this closure to rewrite how we load admission plugins
|
||
|
c.ClientConfig = inClusterConfig
|
||
|
c.SharedInformerFactory = kubeinformers.NewSharedInformerFactory(inClusterClient, 0)
|
||
|
|
||
|
// abuse this closure to rewrite our loopback config
|
||
|
// this is mostly future proofing for post start hooks
|
||
|
_, loopbackConfig, err := f(c.LoopbackClientConfig)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("failed to build loopback config: %w", err)
|
||
|
}
|
||
|
c.LoopbackClientConfig = loopbackConfig
|
||
|
|
||
|
return baseExtraAdmissionInitializers(c)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func wrapTransportOnce(f transport.WrapperFunc) transport.WrapperFunc {
|
||
|
var once sync.Once
|
||
|
return func(rt http.RoundTripper) http.RoundTripper {
|
||
|
once.Do(func() {
|
||
|
_ = f(rt) // assume in-place mutation
|
||
|
})
|
||
|
return rt
|
||
|
}
|
||
|
}
|