Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ require (
k8s.io/apiserver v0.37.0-alpha.1
k8s.io/client-go v0.37.0-alpha.1
k8s.io/klog/v2 v2.140.0
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
k8s.io/utils v0.0.0-20260626114624-be93311217bd
sigs.k8s.io/structured-merge-diff/v6 v6.4.0
sigs.k8s.io/yaml v1.6.0
)

require k8s.io/webhookauth v0.0.0-00010101000000-000000000000

require (
cel.dev/expr v0.25.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
Expand All @@ -39,6 +41,7 @@ require (
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-oidc v2.5.0+incompatible // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
Expand Down Expand Up @@ -71,6 +74,7 @@ require (
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pquerna/cachecontrol v0.1.0 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/spf13/cobra v1.10.2 // indirect
Expand All @@ -88,6 +92,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.55.1-0.20260602153038-42abb857022c // indirect
golang.org/x/oauth2 v0.36.0 // indirect
Expand All @@ -99,11 +104,14 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.81.1 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/component-base v0.37.0-alpha.1 // indirect
k8s.io/kube-openapi v0.0.0-20260519202549-bbf5c5577288 // indirect
k8s.io/kube-openapi v0.0.0-20260618221249-bc653b64f974 // indirect
k8s.io/streaming v0.37.0-alpha.1 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
)

replace k8s.io/webhookauth => /Users/benjaminpetersen/github.com/benjaminapetersen_forks/kubernetes/staging/src/k8s.io/webhookauth
18 changes: 14 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc v2.5.0+incompatible h1:6W0vGJR3Tu0r0PwfmjOrRZSlfxeEln8dsejt3ZWIvwo=
github.com/coreos/go-oidc v2.5.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -132,6 +134,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
Expand All @@ -152,6 +156,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
Expand Down Expand Up @@ -194,6 +199,8 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
Expand Down Expand Up @@ -231,8 +238,11 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs=
gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.37.0-alpha.1 h1:fTnRCeg6apyW4NEMAzh4I0QKmhLrfBn/Wd7ofdiWrWs=
Expand All @@ -249,12 +259,12 @@ k8s.io/component-base v0.37.0-alpha.1 h1:C+wu77XGtTrSvU/v4CMCmg8Uq3BmYsgDSbAy8PD
k8s.io/component-base v0.37.0-alpha.1/go.mod h1:syzFTe+v8SyroLQ5JItALe3snAEmX9VTPCrCEk2K0gk=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260519202549-bbf5c5577288 h1:A7Lby6ekC6nv+6oO38huCMFBRP0Os+tIeq1GkwxOQes=
k8s.io/kube-openapi v0.0.0-20260519202549-bbf5c5577288/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY=
k8s.io/kube-openapi v0.0.0-20260618221249-bc653b64f974 h1:JVogoTvOj6gutlx8bUwGh0e8o8L4X8nDbTLyONmoVvk=
k8s.io/kube-openapi v0.0.0-20260618221249-bc653b64f974/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY=
k8s.io/streaming v0.37.0-alpha.1 h1:8/IDL5B3WI2l7cnO3na104t9oCuQYEXZ6H3095GpfLo=
k8s.io/streaming v0.37.0-alpha.1/go.mod h1:QN1+yCAfxcSvo0908Z3rFKkA5Xlr3KgUZAGwKuR6qQk=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs=
k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
Expand Down
161 changes: 161 additions & 0 deletions pkg/webhook/admission/authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
Copyright 2026 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package admission

import (
"context"
"net/http"

"k8s.io/webhookauth/verify"
"k8s.io/webhookauth/verify/admissionhttp"
"k8s.io/webhookauth/verify/oidc"
"k8s.io/webhookauth/verify/oidc/incluster"

logf "sigs.k8s.io/controller-runtime/pkg/log"
)

// healthChecker is implemented by Authenticators whose readiness can be probed
// (e.g. the verifier-backed authenticator, which is not ready until its audience is bound).
type healthChecker interface{ HealthCheck() error }

// verifierAuthenticator adapts a k8s.io/webhookauth *verify.Verifier onto the
// admission.Authenticator seam.
type verifierAuthenticator struct{ v *verify.Verifier }

var (
_ Authenticator = verifierAuthenticator{}
_ healthChecker = verifierAuthenticator{}
)

// NewAuthenticator wraps a k8s.io/webhookauth *verify.Verifier as an
// Authenticator that enforces KEP-6060 API server authentication.
//
// This is the advanced / bring-your-own verifier entry point. Most users should
// prefer the fluent Webhook.WithInClusterAuthenticator (zero-config, in-cluster)
// or Webhook.WithRemoteAuthenticator (explicit issuer/audience) methods, which
// build the verifier for you.
//
// The verifier verifies the API server's bearer token entirely offline against
// the library's contract (signature, issuer, audience, exp/nbf/iat, webhook
// binding, and allowed API group). Any verification failure fails closed.
func NewAuthenticator(v *verify.Verifier) Authenticator {
return verifierAuthenticator{v: v}
}

// Authenticate verifies the API server's bearer token against the KEP-6060
// contract and fails closed on any error. It reuses the AdmissionRequest that
// controller-runtime already decoded, so the review is never decoded twice.
//
// The library's failure model is deliberately opaque: every rejection is a single
// generic error (verify.ErrVerificationFailed). We therefore always deny with a
// 401 and log only a static message; callers must not branch on the reason and no
// claim material is ever surfaced.
func (a verifierAuthenticator) Authenticate(ctx context.Context, r *http.Request, req Request) Response {
token, ok := admissionhttp.BearerToken(r)
if !ok {
return Unauthenticated("missing or malformed bearer token")
}
if a.v == nil {
return Unauthenticated("verifier is not configured")
}
// Reuse controller-runtime's single decode; never re-read r.Body.
if err := admissionhttp.VerifyAdmissionRequest(ctx, a.v, &req.AdmissionRequest, token); err != nil {
logf.FromContext(ctx).Info("webhook-auth: denied unauthenticated request")
return Unauthenticated("unauthenticated")
}
return Allowed("")
}

// HealthCheck reports whether the backing verifier is ready to verify API server
// tokens, delegating to the library verifier's own HealthCheck. A nil verifier
// gates nothing and always reports ready.
func (a verifierAuthenticator) HealthCheck() error {
if a.v == nil {
return nil // no verifier configured → nothing to gate readiness on
}
return a.v.HealthCheck()
}

// HealthCheck reports whether the webhook's KEP-6060 authenticator (if any) is
// ready to verify API server tokens. It has the signature of healthz.Checker, so
// it can be registered directly:
//
// mgr.AddReadyzCheck("webhook-auth", wh.HealthCheck)
//
// This is a readiness check: register it with AddReadyzCheck, not AddHealthzCheck.
// An authenticator that cannot yet bind its audience should stay not-ready (so the
// pod is not sent traffic) rather than fail liveness, which would crash-loop the pod.
//
// When no verifier-backed authenticator is configured it always reports ready, so
// it is safe to register unconditionally. When an in-cluster/remote authenticator
// is configured, it reports not-ready until the verifier has bound its audience —
// so a pod that can never derive its audience fails readiness and is restarted,
// instead of silently denying all traffic.
func (wh *Webhook) HealthCheck(_ *http.Request) error {
if hc, ok := wh.authenticator.(healthChecker); ok {
return hc.HealthCheck()
}
return nil
}

// WithAuthenticator sets a custom Authenticator (bring-your-own / testing) and
// returns the Webhook for fluent chaining. A nil Authenticator preserves the
// default admission webhook behavior.
func (wh *Webhook) WithAuthenticator(a Authenticator) *Webhook {
wh.authenticator = a
return wh
}

// WithInClusterAuthenticator configures zero-config in-cluster KEP-6060
// authentication: the issuer, audience, and trusted CA are read from the pod's
// own projected service-account token (see k8s.io/webhookauth/verify/oidc/incluster.InCluster).
//
// It performs OIDC discovery now (a network round-trip) and returns an error so
// setup fails fast at startup. Pass a process-lifetime ctx (e.g. the manager's) —
// it governs OIDC discovery and the long-lived background JWKS refresh.
//
// On success it returns the Webhook for fluent chaining.
func (wh *Webhook) WithInClusterAuthenticator(ctx context.Context) (*Webhook, error) {
v, err := incluster.InCluster(ctx)
if err != nil {
return nil, err
}
wh.authenticator = NewAuthenticator(v)
return wh, nil
}

// WithRemoteAuthenticator configures explicit issuer/audience KEP-6060
// authentication via OIDC discovery (see k8s.io/webhookauth/verify/oidc.NewRemoteVerifier).
//
// httpClient (may be nil) is the transport that trusts the issuer's serving CA.
// It performs OIDC discovery now (a network round-trip) and returns an error so
// setup fails fast at startup. Pass a process-lifetime ctx — it governs OIDC
// discovery and the long-lived background JWKS refresh.
//
// On success it returns the Webhook for fluent chaining.
func (wh *Webhook) WithRemoteAuthenticator(ctx context.Context, issuer, audience string, httpClient *http.Client) (*Webhook, error) {
var opts []oidc.Option
if httpClient != nil {
opts = append(opts, oidc.WithHTTPClient(httpClient))
}
v, err := oidc.NewRemoteVerifier(ctx, issuer, audience, opts...)
if err != nil {
return nil, err
}
wh.authenticator = NewAuthenticator(v)
return wh, nil
}
Loading
Loading