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 }