Skip to content

Commit a051b06

Browse files
committed
featuregate UID in RequestHeader authenticator
1 parent 2b472fe commit a051b06

File tree

6 files changed

+142
-26
lines changed

6 files changed

+142
-26
lines changed

pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ import (
3535
"k8s.io/apimachinery/pkg/util/sets"
3636
"k8s.io/apimachinery/pkg/util/wait"
3737
"k8s.io/apiserver/pkg/authentication/request/headerrequest"
38+
"k8s.io/apiserver/pkg/features"
3839
"k8s.io/apiserver/pkg/server/dynamiccertificates"
40+
utilfeature "k8s.io/apiserver/pkg/util/feature"
3941
corev1informers "k8s.io/client-go/informers/core/v1"
4042
"k8s.io/client-go/kubernetes"
4143
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
@@ -262,10 +264,13 @@ func getConfigMapDataFor(authenticationInfo ClusterAuthenticationInfo) (map[stri
262264
if err != nil {
263265
return nil, err
264266
}
265-
data["requestheader-uid-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderUIDHeaders.Value())
266-
if err != nil {
267-
return nil, err
267+
if utilfeature.DefaultFeatureGate.Enabled(features.RemoteRequestHeaderUID) && len(authenticationInfo.RequestHeaderUIDHeaders.Value()) > 0 {
268+
data["requestheader-uid-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderUIDHeaders.Value())
269+
if err != nil {
270+
return nil, err
271+
}
268272
}
273+
269274
data["requestheader-group-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderGroupHeaders.Value())
270275
if err != nil {
271276
return nil, err
@@ -305,9 +310,12 @@ func getClusterAuthenticationInfoFor(data map[string]string) (ClusterAuthenticat
305310
if err != nil {
306311
return ClusterAuthenticationInfo{}, err
307312
}
308-
ret.RequestHeaderUIDHeaders, err = jsonDeserializeStringSlice(data["requestheader-uid-headers"])
309-
if err != nil {
310-
return ClusterAuthenticationInfo{}, err
313+
314+
if utilfeature.DefaultFeatureGate.Enabled(features.RemoteRequestHeaderUID) {
315+
ret.RequestHeaderUIDHeaders, err = jsonDeserializeStringSlice(data["requestheader-uid-headers"])
316+
if err != nil {
317+
return ClusterAuthenticationInfo{}, err
318+
}
311319
}
312320

313321
if caBundle := data["requestheader-client-ca-file"]; len(caBundle) > 0 {

pkg/features/versioned_kube_features.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
306306
{Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
307307
},
308308

309+
genericfeatures.RemoteRequestHeaderUID: {
310+
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
311+
},
312+
309313
genericfeatures.ResilientWatchCacheInitialization: {
310314
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
311315
},

staging/src/k8s.io/apiserver/pkg/features/kube_features.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ const (
149149
// to a chunking list request.
150150
RemainingItemCount featuregate.Feature = "RemainingItemCount"
151151

152+
// owner: @stlaz
153+
//
154+
// Enable kube-apiserver to accept UIDs via request header authentication.
155+
// This will also make the kube-apiserver's API aggregator add UIDs via standard
156+
// headers when forwarding requests to the servers serving the aggregated API.
157+
RemoteRequestHeaderUID featuregate.Feature = "RemoteRequestHeaderUID"
158+
152159
// owner: @wojtek-t
153160
//
154161
// Enables resilient watchcache initialization to avoid controlplane
@@ -359,6 +366,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
359366
{Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
360367
},
361368

369+
RemoteRequestHeaderUID: {
370+
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
371+
},
372+
362373
ResilientWatchCacheInitialization: {
363374
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
364375
},

staging/src/k8s.io/apiserver/pkg/server/options/authentication.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ import (
2929
"k8s.io/apiserver/pkg/apis/apiserver"
3030
"k8s.io/apiserver/pkg/authentication/authenticatorfactory"
3131
"k8s.io/apiserver/pkg/authentication/request/headerrequest"
32+
"k8s.io/apiserver/pkg/features"
3233
"k8s.io/apiserver/pkg/server"
3334
"k8s.io/apiserver/pkg/server/dynamiccertificates"
35+
utilfeature "k8s.io/apiserver/pkg/util/feature"
3436
"k8s.io/client-go/kubernetes"
3537
"k8s.io/client-go/rest"
3638
"k8s.io/client-go/tools/clientcmd"
@@ -68,9 +70,6 @@ func (s *RequestHeaderAuthenticationOptions) Validate() []error {
6870
if err := checkForWhiteSpaceOnly("requestheader-username-headers", s.UsernameHeaders...); err != nil {
6971
allErrors = append(allErrors, err)
7072
}
71-
if err := checkForWhiteSpaceOnly("requestheader-uid-headers", s.UIDHeaders...); err != nil {
72-
allErrors = append(allErrors, err)
73-
}
7473
if err := checkForWhiteSpaceOnly("requestheader-group-headers", s.GroupHeaders...); err != nil {
7574
allErrors = append(allErrors, err)
7675
}
@@ -84,17 +83,27 @@ func (s *RequestHeaderAuthenticationOptions) Validate() []error {
8483
if len(s.UsernameHeaders) > 0 && !caseInsensitiveHas(s.UsernameHeaders, "X-Remote-User") {
8584
klog.Warningf("--requestheader-username-headers is set without specifying the standard X-Remote-User header - API aggregation will not work")
8685
}
87-
if len(s.UIDHeaders) > 0 && !caseInsensitiveHas(s.UIDHeaders, "X-Remote-Uid") {
88-
// this was added later and so we are able to error out
89-
allErrors = append(allErrors, fmt.Errorf("--requestheader-uid-headers is set without specifying the standard X-Remote-Uid header - API aggregation will not work"))
90-
}
9186
if len(s.GroupHeaders) > 0 && !caseInsensitiveHas(s.GroupHeaders, "X-Remote-Group") {
9287
klog.Warningf("--requestheader-group-headers is set without specifying the standard X-Remote-Group header - API aggregation will not work")
9388
}
9489
if len(s.ExtraHeaderPrefixes) > 0 && !caseInsensitiveHas(s.ExtraHeaderPrefixes, "X-Remote-Extra-") {
9590
klog.Warningf("--requestheader-extra-headers-prefix is set without specifying the standard X-Remote-Extra- header prefix - API aggregation will not work")
9691
}
9792

93+
if !utilfeature.DefaultFeatureGate.Enabled(features.RemoteRequestHeaderUID) {
94+
if len(s.UIDHeaders) > 0 {
95+
allErrors = append(allErrors, fmt.Errorf("--requestheader-uid-headers requires the %q feature to be enabled", features.RemoteRequestHeaderUID))
96+
}
97+
} else {
98+
if err := checkForWhiteSpaceOnly("requestheader-uid-headers", s.UIDHeaders...); err != nil {
99+
allErrors = append(allErrors, err)
100+
}
101+
if len(s.UIDHeaders) > 0 && !caseInsensitiveHas(s.UIDHeaders, "X-Remote-Uid") {
102+
// this was added later and so we are able to error out
103+
allErrors = append(allErrors, fmt.Errorf("--requestheader-uid-headers is set without specifying the standard X-Remote-Uid header - API aggregation will not work"))
104+
}
105+
}
106+
98107
return allErrors
99108
}
100109

@@ -126,7 +135,7 @@ func (s *RequestHeaderAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
126135
"List of request headers to inspect for usernames. X-Remote-User is common.")
127136

128137
fs.StringSliceVar(&s.UIDHeaders, "requestheader-uid-headers", s.UIDHeaders, ""+
129-
"List of request headers to inspect for UIDs. X-Remote-Uid is suggested.")
138+
"List of request headers to inspect for UIDs. X-Remote-Uid is suggested. Requires the RemoteRequestHeaderUID feature to be enabled.")
130139

131140
fs.StringSliceVar(&s.GroupHeaders, "requestheader-group-headers", s.GroupHeaders, ""+
132141
"List of request headers to inspect for groups. X-Remote-Group is suggested.")

staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,12 @@ func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
159159
proxyRoundTripper := handlingInfo.proxyRoundTripper
160160
upgrade := httpstream.IsUpgradeRequest(req)
161161

162-
proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), user.GetUID(), user.GetGroups(), user.GetExtra(), proxyRoundTripper)
162+
var userUID string
163+
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.RemoteRequestHeaderUID) {
164+
userUID = user.GetUID()
165+
}
166+
167+
proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), userUID, user.GetGroups(), user.GetExtra(), proxyRoundTripper)
163168

164169
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerTracing) && !upgrade {
165170
tracingWrapper := tracing.WrapperFor(r.tracerProvider)
@@ -170,7 +175,7 @@ func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
170175
// NOT use the proxyRoundTripper. It's a direct dial that bypasses the proxyRoundTripper. This means that we have to
171176
// attach the "correct" user headers to the request ahead of time.
172177
if upgrade {
173-
transport.SetAuthProxyHeaders(newReq, user.GetName(), user.GetUID(), user.GetGroups(), user.GetExtra())
178+
transport.SetAuthProxyHeaders(newReq, user.GetName(), userUID, user.GetGroups(), user.GetExtra())
174179
}
175180

176181
handler := proxy.NewUpgradeAwareHandler(location, proxyRoundTripper, true, upgrade, &responder{w: w})

staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy_test.go

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ import (
3434
"sync/atomic"
3535
"testing"
3636

37+
"github.com/google/go-cmp/cmp"
3738
"k8s.io/apiserver/pkg/audit"
39+
"k8s.io/apiserver/pkg/features"
3840
"k8s.io/apiserver/pkg/server/dynamiccertificates"
3941
"k8s.io/client-go/transport"
4042

@@ -53,8 +55,11 @@ import (
5355
"k8s.io/apiserver/pkg/endpoints/filters"
5456
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
5557
"k8s.io/apiserver/pkg/server/egressselector"
58+
utilfeature "k8s.io/apiserver/pkg/util/feature"
5659
utilflowcontrol "k8s.io/apiserver/pkg/util/flowcontrol"
5760
apiserverproxyutil "k8s.io/apiserver/pkg/util/proxy"
61+
"k8s.io/component-base/featuregate"
62+
featuregatetesting "k8s.io/component-base/featuregate/testing"
5863
"k8s.io/component-base/metrics"
5964
"k8s.io/component-base/metrics/legacyregistry"
6065
apiregistration "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
@@ -130,6 +135,8 @@ func TestProxyHandler(t *testing.T) {
130135
expectedBody string
131136
expectedCalled bool
132137
expectedHeaders map[string][]string
138+
139+
enableFeatureGates []featuregate.Feature
133140
}{
134141
"no target": {
135142
expectedStatusCode: http.StatusNotFound,
@@ -174,6 +181,40 @@ func TestProxyHandler(t *testing.T) {
174181
},
175182
expectedStatusCode: http.StatusOK,
176183
expectedCalled: true,
184+
expectedHeaders: map[string][]string{
185+
"X-Forwarded-Proto": {"https"},
186+
"X-Forwarded-Uri": {"/request/path"},
187+
"X-Forwarded-For": {"127.0.0.1"},
188+
"X-Remote-User": {"username"},
189+
"User-Agent": {"Go-http-client/1.1"},
190+
"Accept-Encoding": {"gzip"},
191+
"X-Remote-Group": {"one", "two"},
192+
},
193+
},
194+
"[RemoteRequestHeaderUID] proxy with user, insecure": {
195+
user: &user.DefaultInfo{
196+
Name: "username",
197+
UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78",
198+
Groups: []string{"one", "two"},
199+
},
200+
path: "/request/path",
201+
apiService: &apiregistration.APIService{
202+
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
203+
Spec: apiregistration.APIServiceSpec{
204+
Service: &apiregistration.ServiceReference{Port: pointer.Int32Ptr(443)},
205+
Group: "foo",
206+
Version: "v1",
207+
InsecureSkipTLSVerify: true,
208+
},
209+
Status: apiregistration.APIServiceStatus{
210+
Conditions: []apiregistration.APIServiceCondition{
211+
{Type: apiregistration.Available, Status: apiregistration.ConditionTrue},
212+
},
213+
},
214+
},
215+
enableFeatureGates: []featuregate.Feature{features.RemoteRequestHeaderUID},
216+
expectedStatusCode: http.StatusOK,
217+
expectedCalled: true,
177218
expectedHeaders: map[string][]string{
178219
"X-Forwarded-Proto": {"https"},
179220
"X-Forwarded-Uri": {"/request/path"},
@@ -208,6 +249,40 @@ func TestProxyHandler(t *testing.T) {
208249
},
209250
expectedStatusCode: http.StatusOK,
210251
expectedCalled: true,
252+
expectedHeaders: map[string][]string{
253+
"X-Forwarded-Proto": {"https"},
254+
"X-Forwarded-Uri": {"/request/path"},
255+
"X-Forwarded-For": {"127.0.0.1"},
256+
"X-Remote-User": {"username"},
257+
"User-Agent": {"Go-http-client/1.1"},
258+
"Accept-Encoding": {"gzip"},
259+
"X-Remote-Group": {"one", "two"},
260+
},
261+
},
262+
"[RemoteRequestHeaderUID] proxy with user, cabundle": {
263+
user: &user.DefaultInfo{
264+
Name: "username",
265+
UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78",
266+
Groups: []string{"one", "two"},
267+
},
268+
path: "/request/path",
269+
apiService: &apiregistration.APIService{
270+
ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"},
271+
Spec: apiregistration.APIServiceSpec{
272+
Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: pointer.Int32Ptr(443)},
273+
Group: "foo",
274+
Version: "v1",
275+
CABundle: testCACrt,
276+
},
277+
Status: apiregistration.APIServiceStatus{
278+
Conditions: []apiregistration.APIServiceCondition{
279+
{Type: apiregistration.Available, Status: apiregistration.ConditionTrue},
280+
},
281+
},
282+
},
283+
enableFeatureGates: []featuregate.Feature{features.RemoteRequestHeaderUID},
284+
expectedStatusCode: http.StatusOK,
285+
expectedCalled: true,
211286
expectedHeaders: map[string][]string{
212287
"X-Forwarded-Proto": {"https"},
213288
"X-Forwarded-Uri": {"/request/path"},
@@ -320,7 +395,11 @@ func TestProxyHandler(t *testing.T) {
320395
target.Reset()
321396
legacyregistry.Reset()
322397

323-
func() {
398+
t.Run(name, func(t *testing.T) {
399+
for _, f := range tc.enableFeatureGates {
400+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, f, true)
401+
}
402+
324403
targetServer := httptest.NewUnstartedServer(target)
325404
serviceCert := tc.serviceCertOverride
326405
if serviceCert == nil {
@@ -354,37 +433,37 @@ func TestProxyHandler(t *testing.T) {
354433

355434
resp, err := http.Get(server.URL + tc.path)
356435
if err != nil {
357-
t.Errorf("%s: %v", name, err)
436+
t.Errorf("%v", err)
358437
return
359438
}
360439
if e, a := tc.expectedStatusCode, resp.StatusCode; e != a {
361440
body, _ := httputil.DumpResponse(resp, true)
362-
t.Logf("%s: %v", name, string(body))
363-
t.Errorf("%s: expected %v, got %v", name, e, a)
441+
t.Logf("%v", string(body))
442+
t.Errorf("expected %v, got %v", e, a)
364443
return
365444
}
366445
bytes, err := io.ReadAll(resp.Body)
367446
if err != nil {
368-
t.Errorf("%s: %v", name, err)
447+
t.Errorf("%v", err)
369448
return
370449
}
371450
if !strings.Contains(string(bytes), tc.expectedBody) {
372-
t.Errorf("%s: expected %q, got %q", name, tc.expectedBody, string(bytes))
451+
t.Errorf("expected %q, got %q", tc.expectedBody, string(bytes))
373452
return
374453
}
375454

376455
if e, a := tc.expectedCalled, target.called; e != a {
377-
t.Errorf("%s: expected %v, got %v", name, e, a)
456+
t.Errorf("expected %v, got %v", e, a)
378457
return
379458
}
380459
// this varies every test
381460
delete(target.headers, "X-Forwarded-Host")
382461
if e, a := tc.expectedHeaders, target.headers; !reflect.DeepEqual(e, a) {
383-
t.Errorf("%s: expected %v, got %v", name, e, a)
462+
t.Errorf("expected != got %v", cmp.Diff(e, a))
384463
return
385464
}
386465
if e, a := targetServer.Listener.Addr().String(), target.host; tc.expectedCalled && !reflect.DeepEqual(e, a) {
387-
t.Errorf("%s: expected %v, got %v", name, e, a)
466+
t.Errorf("expected %v, got %v", e, a)
388467
return
389468
}
390469

@@ -397,7 +476,7 @@ func TestProxyHandler(t *testing.T) {
397476
t.Errorf("expected the x509_missing_san_total to be 1, but it's %d", errorCounter)
398477
}
399478
}
400-
}()
479+
})
401480
}
402481
}
403482

0 commit comments

Comments
 (0)