ContainerImage.Pinniped/internal/crypto/ptls/ptls.go

137 lines
4.6 KiB
Go
Raw Normal View History

// Copyright 2021-2022 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.
// defaultServingOptionsMinTLSVersion is the minimum tls version in the format
// expected by SecureServingOptions.MinTLSVersion from
// k8s.io/apiserver/pkg/server/options.
const defaultServingOptionsMinTLSVersion = "VersionTLS12"
type ConfigFunc func(*x509.CertPool) *tls.Config
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 = defaultServingOptionsMinTLSVersion
}
func secureServing(opts *options.SecureServingOptionsWithLoopback) {
opts.MinTLSVersion = secureServingOptionsMinTLSVersion
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
}
}