knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/webhook.go (about)

     1  /*
     2  Copyright 2017 The Knative 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 webhook
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"errors"
    23  	"fmt"
    24  	"html"
    25  	"log"
    26  	"net"
    27  	"net/http"
    28  	"time"
    29  
    30  	// Injection stuff
    31  
    32  	"knative.dev/pkg/controller"
    33  	kubeinformerfactory "knative.dev/pkg/injection/clients/namespacedkube/informers/factory"
    34  	"knative.dev/pkg/network"
    35  	"knative.dev/pkg/network/handlers"
    36  	knativetls "knative.dev/pkg/network/tls"
    37  	"knative.dev/pkg/observability/semconv"
    38  
    39  	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    40  	"go.opentelemetry.io/otel/metric"
    41  	"go.opentelemetry.io/otel/propagation"
    42  	"go.opentelemetry.io/otel/trace"
    43  
    44  	"go.uber.org/zap"
    45  	"golang.org/x/sync/errgroup"
    46  	admissionv1 "k8s.io/api/admission/v1"
    47  	"knative.dev/pkg/logging"
    48  	"knative.dev/pkg/system"
    49  )
    50  
    51  // Options contains the configuration for the webhook.
    52  //
    53  // TLS fields (TLSMinVersion, TLSMaxVersion, TLSCipherSuites, TLSCurvePreferences)
    54  // are resolved with the following precedence:
    55  //  1. Values set explicitly in Options (programmatic).
    56  //  2. WEBHOOK_TLS_* environment variables (WEBHOOK_TLS_MIN_VERSION,
    57  //     WEBHOOK_TLS_MAX_VERSION, WEBHOOK_TLS_CIPHER_SUITES, WEBHOOK_TLS_CURVE_PREFERENCES).
    58  //  3. Defaults (TLS 1.3 minimum version; zero values for the rest, meaning the
    59  //     Go standard library picks its defaults).
    60  type Options struct {
    61  	// TLSMinVersion contains the minimum TLS version that is acceptable to communicate with the API server.
    62  	// TLS 1.3 is the minimum version if not specified otherwise.
    63  	TLSMinVersion uint16
    64  
    65  	// TLSMaxVersion contains the maximum TLS version that is acceptable.
    66  	// If not set (0), the maximum version supported by the implementation will be used.
    67  	// This is useful for enforcing Modern profile (TLS 1.3 only) by setting both
    68  	// TLSMinVersion and TLSMaxVersion to tls.VersionTLS13.
    69  	TLSMaxVersion uint16
    70  
    71  	// TLSCipherSuites specifies the list of enabled cipher suites.
    72  	// If empty, a default list of secure cipher suites will be used.
    73  	// Note: Cipher suites are not configurable in TLS 1.3; they are determined by the implementation.
    74  	TLSCipherSuites []uint16
    75  
    76  	// TLSCurvePreferences specifies the elliptic curves that will be used in an ECDHE handshake.
    77  	// If empty, the default curves will be used.
    78  	TLSCurvePreferences []tls.CurveID
    79  
    80  	// ServiceName is the service name of the webhook.
    81  	ServiceName string
    82  
    83  	// SecretName is the name of k8s secret that contains the webhook
    84  	// server key/cert and corresponding CA cert that signed them. The
    85  	// server key/cert are used to serve the webhook and the CA cert
    86  	// is provided to k8s apiserver during admission controller
    87  	// registration.
    88  	// If no SecretName is provided, then the webhook serves without TLS.
    89  	SecretName string
    90  
    91  	// ServerPrivateKeyName is the name for the webhook secret's data key e.g. `tls.key`.
    92  	// Default value is `server-key.pem` if no value is passed.
    93  	ServerPrivateKeyName string
    94  
    95  	// ServerCertificateName is the name for the webhook secret's ca data key e.g. `tls.crt`.
    96  	// Default value is `server-cert.pem` if no value is passed.
    97  	ServerCertificateName string
    98  
    99  	// Port where the webhook is served. Per k8s admission
   100  	// registration requirements this should be 443 unless there is
   101  	// only a single port for the service.
   102  	Port int
   103  
   104  	// GracePeriod is how long to wait after failing readiness probes
   105  	// before shutting down.
   106  	GracePeriod time.Duration
   107  
   108  	// DisableNamespaceOwnership configures if the SYSTEM_NAMESPACE is added as an owner reference to the
   109  	// webhook configuration resources. Overridden by the WEBHOOK_DISABLE_NAMESPACE_OWNERSHIP environment variable.
   110  	// Disabling can be useful to avoid breaking systems that expect ownership to indicate a true controller
   111  	// relationship: https://github.com/knative/serving/issues/15483
   112  	DisableNamespaceOwnership bool
   113  
   114  	// ControllerOptions encapsulates options for creating a new controller,
   115  	// including throttling and stats behavior.
   116  	ControllerOptions *controller.ControllerOptions
   117  
   118  	// EnableHTTP2 enables HTTP2 for webhooks.
   119  	// Mitigate CVE-2023-44487 by disabling HTTP2 by default until the Go
   120  	// standard library and golang.org/x/net are fully fixed.
   121  	// Right now, it is possible for authenticated and unauthenticated users to
   122  	// hold open HTTP2 connections and consume huge amounts of memory.
   123  	// See:
   124  	// * https://github.com/kubernetes/kubernetes/pull/121120
   125  	// * https://github.com/kubernetes/kubernetes/issues/121197
   126  	// * https://github.com/golang/go/issues/63417#issuecomment-1758858612
   127  	EnableHTTP2 bool
   128  
   129  	// MeterProvider is used to configure the MeterProvider used by the webhook
   130  	// If nil it will use the global meter provider
   131  	MeterProvider metric.MeterProvider
   132  
   133  	// TracerProvider is used to config the TracerProvider used by the webhook
   134  	// if nil it will use the global tracer provider
   135  	TracerProvider trace.TracerProvider
   136  
   137  	// TextMapPropagator is used to configure the TextMapPropagator used by the webhook
   138  	// if nil it will use the global text map propagator
   139  	TextMapPropagator propagation.TextMapPropagator
   140  }
   141  
   142  // Operation is the verb being operated on
   143  // it is aliased in Validation from the k8s admission package
   144  type Operation = admissionv1.Operation
   145  
   146  // Operation types
   147  const (
   148  	Create  Operation = admissionv1.Create
   149  	Update  Operation = admissionv1.Update
   150  	Delete  Operation = admissionv1.Delete
   151  	Connect Operation = admissionv1.Connect
   152  )
   153  
   154  // Webhook implements the external webhook for validation of
   155  // resources and configuration.
   156  type Webhook struct {
   157  	Options Options
   158  	Logger  *zap.SugaredLogger
   159  
   160  	// synced is function that is called when the informers have been synced.
   161  	synced context.CancelFunc
   162  
   163  	mux http.ServeMux
   164  
   165  	// The TLS configuration to use for serving (or nil for non-TLS)
   166  	tlsConfig *tls.Config
   167  
   168  	// testListener is only used in testing so we don't get port conflicts
   169  	testListener net.Listener
   170  
   171  	metrics *metrics
   172  }
   173  
   174  // New constructs a Webhook
   175  func New(
   176  	ctx context.Context,
   177  	controllers []interface{},
   178  ) (webhook *Webhook, err error) {
   179  	// ServeMux.Handle panics on duplicate paths
   180  	defer func() {
   181  		if r := recover(); r != nil {
   182  			err = fmt.Errorf("error creating webhook %v", r)
   183  		}
   184  	}()
   185  
   186  	opts := GetOptions(ctx)
   187  	if opts == nil {
   188  		return nil, errors.New("context must have Options specified")
   189  	}
   190  
   191  	logger := logging.FromContext(ctx)
   192  
   193  	tlsCfg, err := knativetls.DefaultConfigFromEnv("WEBHOOK_")
   194  	if err != nil {
   195  		return nil, fmt.Errorf("reading TLS configuration from environment: %w", err)
   196  	}
   197  
   198  	if opts.TLSMinVersion != 0 {
   199  		tlsCfg.MinVersion = opts.TLSMinVersion
   200  	}
   201  	if opts.TLSMaxVersion != 0 {
   202  		tlsCfg.MaxVersion = opts.TLSMaxVersion
   203  	}
   204  	if opts.TLSCipherSuites != nil {
   205  		tlsCfg.CipherSuites = opts.TLSCipherSuites
   206  	}
   207  	if opts.TLSCurvePreferences != nil {
   208  		tlsCfg.CurvePreferences = opts.TLSCurvePreferences
   209  	}
   210  
   211  	if tlsCfg.MinVersion != tls.VersionTLS12 && tlsCfg.MinVersion != tls.VersionTLS13 {
   212  		return nil, fmt.Errorf("unsupported TLS minimum version %d: must be TLS 1.2 or TLS 1.3", tlsCfg.MinVersion)
   213  	}
   214  	if tlsCfg.MaxVersion != 0 && tlsCfg.MinVersion > tlsCfg.MaxVersion {
   215  		return nil, fmt.Errorf("TLS minimum version (%#x) is greater than maximum version (%#x)", tlsCfg.MinVersion, tlsCfg.MaxVersion)
   216  	}
   217  
   218  	syncCtx, cancel := context.WithCancel(context.Background())
   219  
   220  	webhook = &Webhook{
   221  		Options: *opts,
   222  		Logger:  logger,
   223  		synced:  cancel,
   224  		metrics: newMetrics(*opts),
   225  	}
   226  
   227  	if opts.SecretName != "" {
   228  		// Injection is too aggressive for this case because by simply linking this
   229  		// library we force consumers to have secret access.  If we require that one
   230  		// of the admission controllers' informers *also* require the secret
   231  		// informer, then we can fetch the shared informer factory here and produce
   232  		// a new secret informer from it.
   233  		secretInformer := kubeinformerfactory.Get(ctx).Core().V1().Secrets()
   234  
   235  		// If we return (nil, error) the client sees - 'tls: internal error'
   236  		// If we return (nil, nil) the client sees - 'tls: no certificates configured'
   237  		//
   238  		// We'll return (nil, nil) when we don't find a certificate
   239  		tlsCfg.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
   240  			secret, err := secretInformer.Lister().Secrets(system.Namespace()).Get(opts.SecretName)
   241  			if err != nil {
   242  				logger.Errorw("failed to fetch secret", zap.Error(err))
   243  				return nil, nil
   244  			}
   245  			webOpts := GetOptions(ctx)
   246  			sKey, sCert := getSecretDataKeyNamesOrDefault(webOpts.ServerPrivateKeyName, webOpts.ServerCertificateName)
   247  			serverKey, ok := secret.Data[sKey]
   248  			if !ok {
   249  				logger.Warn("server key missing")
   250  				return nil, nil
   251  			}
   252  			serverCert, ok := secret.Data[sCert]
   253  			if !ok {
   254  				logger.Warn("server cert missing")
   255  				return nil, nil
   256  			}
   257  			cert, err := tls.X509KeyPair(serverCert, serverKey)
   258  			if err != nil {
   259  				return nil, err
   260  			}
   261  			return &cert, nil
   262  		}
   263  		webhook.tlsConfig = tlsCfg
   264  	}
   265  
   266  	webhook.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   267  		http.Error(w, fmt.Sprint("no controller registered for: ", html.EscapeString(r.URL.Path)), http.StatusBadRequest)
   268  	})
   269  
   270  	for _, controller := range controllers {
   271  		switch c := controller.(type) {
   272  		case AdmissionController:
   273  			handler := admissionHandler(webhook, c, syncCtx.Done())
   274  			webhook.mux.Handle(c.Path(), handler)
   275  
   276  		case ConversionController:
   277  			handler := conversionHandler(webhook, c)
   278  			webhook.mux.Handle(c.Path(), handler)
   279  
   280  		default:
   281  			return nil, fmt.Errorf("unknown webhook controller type:  %T", controller)
   282  		}
   283  	}
   284  
   285  	return webhook, err
   286  }
   287  
   288  // InformersHaveSynced is called when the informers have all been synced, which allows any outstanding
   289  // admission webhooks through.
   290  func (wh *Webhook) InformersHaveSynced() {
   291  	wh.synced()
   292  	wh.Logger.Info("Informers have been synced, unblocking admission webhooks.")
   293  }
   294  
   295  type zapWrapper struct {
   296  	logger *zap.SugaredLogger
   297  }
   298  
   299  func (z *zapWrapper) Write(p []byte) (n int, err error) {
   300  	z.logger.Errorw(string(p))
   301  	return len(p), nil
   302  }
   303  
   304  // Run implements the admission controller run loop.
   305  func (wh *Webhook) Run(stop <-chan struct{}) error {
   306  	logger := wh.Logger
   307  	ctx := logging.WithLogger(context.Background(), logger)
   308  
   309  	drainer := &handlers.Drainer{
   310  		Inner:       wh,
   311  		QuietPeriod: wh.Options.GracePeriod,
   312  	}
   313  
   314  	otelHandler := otelhttp.NewHandler(
   315  		&routeLabeler{next: drainer},
   316  		wh.Options.ServiceName, // Note this service is k8s service name
   317  		otelhttp.WithMeterProvider(wh.Options.MeterProvider),
   318  		otelhttp.WithTracerProvider(wh.Options.TracerProvider),
   319  		otelhttp.WithPropagators(wh.Options.TextMapPropagator),
   320  		otelhttp.WithFilter(func(r *http.Request) bool {
   321  			// Don't trace kubelet probes
   322  			return !network.IsKubeletProbe(r)
   323  		}),
   324  		otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
   325  			if r.URL.Path == "" {
   326  				return r.Method + " /"
   327  			}
   328  			return fmt.Sprintf("%s %s", r.Method, r.URL.Path)
   329  		}),
   330  	)
   331  
   332  	// If TLSNextProto is not nil, HTTP/2 support is not enabled automatically.
   333  	nextProto := map[string]func(*http.Server, *tls.Conn, http.Handler){}
   334  	if wh.Options.EnableHTTP2 {
   335  		nextProto = nil
   336  	}
   337  
   338  	server := &http.Server{
   339  		ErrorLog:          log.New(&zapWrapper{logger}, "", 0),
   340  		Handler:           otelHandler,
   341  		Addr:              fmt.Sprint(":", wh.Options.Port),
   342  		TLSConfig:         wh.tlsConfig,
   343  		ReadHeaderTimeout: time.Minute, // https://medium.com/a-journey-with-go/go-understand-and-mitigate-slowloris-attack-711c1b1403f6
   344  		TLSNextProto:      nextProto,
   345  	}
   346  
   347  	serve := server.ListenAndServe
   348  
   349  	if server.TLSConfig != nil && wh.testListener != nil {
   350  		serve = func() error {
   351  			return server.ServeTLS(wh.testListener, "", "")
   352  		}
   353  	} else if server.TLSConfig != nil {
   354  		serve = func() error {
   355  			return server.ListenAndServeTLS("", "")
   356  		}
   357  	} else if wh.testListener != nil {
   358  		serve = func() error {
   359  			return server.Serve(wh.testListener)
   360  		}
   361  	}
   362  
   363  	eg, ctx := errgroup.WithContext(ctx)
   364  	eg.Go(func() error {
   365  		if err := serve(); err != nil && !errors.Is(err, http.ErrServerClosed) {
   366  			logger.Errorw("ListenAndServe for admission webhook returned error", zap.Error(err))
   367  			return err
   368  		}
   369  		return nil
   370  	})
   371  
   372  	select {
   373  	case <-stop:
   374  		eg.Go(func() error {
   375  			// Start failing readiness probes immediately.
   376  			logger.Info("Starting to fail readiness probes...")
   377  			drainer.Drain()
   378  
   379  			return server.Shutdown(context.Background())
   380  		})
   381  
   382  		// Wait for all outstanding go routined to terminate, including our new one.
   383  		return eg.Wait()
   384  
   385  	case <-ctx.Done():
   386  		return fmt.Errorf("webhook server bootstrap failed %w", ctx.Err())
   387  	}
   388  }
   389  
   390  func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   391  	// Verify the content type is accurate.
   392  	contentType := r.Header.Get("Content-Type")
   393  	if contentType != "application/json" {
   394  		http.Error(w, "invalid Content-Type, want `application/json`", http.StatusUnsupportedMediaType)
   395  		return
   396  	}
   397  
   398  	const MaxBodySize = 3 * 1024 * 1024 // 3 MiB
   399  	h := http.MaxBytesHandler(&wh.mux, MaxBodySize)
   400  	h.ServeHTTP(w, r)
   401  }
   402  
   403  type routeLabeler struct {
   404  	next http.Handler
   405  }
   406  
   407  func (rl *routeLabeler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   408  	if r.URL.Path != "" {
   409  		labeler, _ := otelhttp.LabelerFromContext(r.Context())
   410  		labeler.Add(semconv.HTTPRoute(r.URL.Path))
   411  	}
   412  
   413  	rl.next.ServeHTTP(w, r)
   414  }