Skip to content

Commit 2bda5dd

Browse files
authored
Merge pull request kubernetes#129656 from vinayakankugoyal/kep2862beta
KEP-2862: Graduate to BETA.
2 parents 956eb9b + 3a780a1 commit 2bda5dd

File tree

7 files changed

+236
-63
lines changed

7 files changed

+236
-63
lines changed

pkg/features/versioned_kube_features.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
452452

453453
KubeletFineGrainedAuthz: {
454454
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
455+
{Version: version.MustParse("1.33"), Default: true, PreRelease: featuregate.Beta},
455456
},
456457

457458
KubeletInUserNamespace: {

pkg/kubelet/server/server_test.go

Lines changed: 72 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -534,39 +534,44 @@ func TestAuthzCoverage(t *testing.T) {
534534
fw := newServerTest()
535535
defer fw.testHTTPServer.Close()
536536

537-
// method:path -> has coverage
538-
expectedCases := map[string]bool{}
539-
540-
// Test all the non-web-service handlers
541-
for _, path := range fw.serverUnderTest.restfulCont.RegisteredHandlePaths() {
542-
expectedCases["GET:"+path] = false
543-
expectedCases["POST:"+path] = false
544-
}
537+
for _, fineGrained := range []bool{false, true} {
538+
t.Run(fmt.Sprintf("fineGrained=%v", fineGrained), func(t *testing.T) {
539+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletFineGrainedAuthz, fineGrained)
540+
// method:path -> has coverage
541+
expectedCases := map[string]bool{}
542+
543+
// Test all the non-web-service handlers
544+
for _, path := range fw.serverUnderTest.restfulCont.RegisteredHandlePaths() {
545+
expectedCases["GET:"+path] = false
546+
expectedCases["POST:"+path] = false
547+
}
545548

546-
// Test all the generated web-service paths
547-
for _, ws := range fw.serverUnderTest.restfulCont.RegisteredWebServices() {
548-
for _, r := range ws.Routes() {
549-
expectedCases[r.Method+":"+r.Path] = false
550-
}
551-
}
549+
// Test all the generated web-service paths
550+
for _, ws := range fw.serverUnderTest.restfulCont.RegisteredWebServices() {
551+
for _, r := range ws.Routes() {
552+
expectedCases[r.Method+":"+r.Path] = false
553+
}
554+
}
552555

553-
// This is a sanity check that the Handle->HandleWithFilter() delegation is working
554-
// Ideally, these would move to registered web services and this list would get shorter
555-
expectedPaths := []string{"/healthz", "/metrics", "/metrics/cadvisor"}
556-
for _, expectedPath := range expectedPaths {
557-
if _, expected := expectedCases["GET:"+expectedPath]; !expected {
558-
t.Errorf("Expected registered handle path %s was missing", expectedPath)
559-
}
560-
}
556+
// This is a sanity check that the Handle->HandleWithFilter() delegation is working
557+
// Ideally, these would move to registered web services and this list would get shorter
558+
expectedPaths := []string{"/healthz", "/metrics", "/metrics/cadvisor"}
559+
for _, expectedPath := range expectedPaths {
560+
if _, expected := expectedCases["GET:"+expectedPath]; !expected {
561+
t.Errorf("Expected registered handle path %s was missing", expectedPath)
562+
}
563+
}
561564

562-
for _, tc := range AuthzTestCases(false) {
563-
expectedCases[tc.Method+":"+tc.Path] = true
564-
}
565+
for _, tc := range AuthzTestCases(fineGrained) {
566+
expectedCases[tc.Method+":"+tc.Path] = true
567+
}
565568

566-
for tc, found := range expectedCases {
567-
if !found {
568-
t.Errorf("Missing authz test case for %s", tc)
569-
}
569+
for tc, found := range expectedCases {
570+
if !found {
571+
t.Errorf("Missing authz test case for %s", tc)
572+
}
573+
}
574+
})
570575
}
571576
}
572577

@@ -580,43 +585,47 @@ func TestAuthFilters(t *testing.T) {
580585

581586
attributesGetter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
582587

583-
for _, tc := range AuthzTestCases(false) {
584-
t.Run(tc.Method+":"+tc.Path, func(t *testing.T) {
585-
var (
586-
expectedUser = AuthzTestUser()
587-
588-
calledAuthenticate = false
589-
calledAuthorize = false
590-
calledAttributes = false
591-
)
592-
593-
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
594-
calledAuthenticate = true
595-
return &authenticator.Response{User: expectedUser}, true, nil
596-
}
597-
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) []authorizer.Attributes {
598-
calledAttributes = true
599-
require.Equal(t, expectedUser, u)
600-
return attributesGetter.GetRequestAttributes(u, req)
601-
}
602-
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
603-
calledAuthorize = true
604-
tc.AssertAttributes(t, []authorizer.Attributes{a})
605-
return authorizer.DecisionNoOpinion, "", nil
606-
}
588+
for _, fineGraned := range []bool{false, true} {
589+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletFineGrainedAuthz, fineGraned)
590+
for _, tc := range AuthzTestCases(fineGraned) {
591+
t.Run(fmt.Sprintf("method=%v:path=%v:fineGrained=%v", tc.Method, tc.Method, fineGraned), func(t *testing.T) {
592+
var (
593+
expectedUser = AuthzTestUser()
594+
595+
calledAuthenticate = false
596+
calledAuthorize = false
597+
calledAttributes = false
598+
)
599+
600+
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
601+
calledAuthenticate = true
602+
return &authenticator.Response{User: expectedUser}, true, nil
603+
}
604+
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) []authorizer.Attributes {
605+
calledAttributes = true
606+
require.Equal(t, expectedUser, u)
607+
attrs := attributesGetter.GetRequestAttributes(u, req)
608+
tc.AssertAttributes(t, attrs)
609+
return attrs
610+
}
611+
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
612+
calledAuthorize = true
613+
return authorizer.DecisionNoOpinion, "", nil
614+
}
607615

608-
req, err := http.NewRequest(tc.Method, fw.testHTTPServer.URL+tc.Path, nil)
609-
require.NoError(t, err)
616+
req, err := http.NewRequest(tc.Method, fw.testHTTPServer.URL+tc.Path, nil)
617+
require.NoError(t, err)
610618

611-
resp, err := http.DefaultClient.Do(req)
612-
require.NoError(t, err)
613-
defer resp.Body.Close()
619+
resp, err := http.DefaultClient.Do(req)
620+
require.NoError(t, err)
621+
defer resp.Body.Close() //nolint:errcheck
614622

615-
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
616-
assert.True(t, calledAuthenticate, "Authenticate was not called")
617-
assert.True(t, calledAttributes, "Attributes were not called")
618-
assert.True(t, calledAuthorize, "Authorize was not called")
619-
})
623+
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
624+
assert.True(t, calledAuthenticate, "Authenticate was not called")
625+
assert.True(t, calledAttributes, "Attributes were not called")
626+
assert.True(t, calledAuthorize, "Authorize was not called")
627+
})
628+
}
620629
}
621630
}
622631

plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/cluster-roles.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,14 @@ items:
916916
- nodes/stats
917917
verbs:
918918
- '*'
919+
- apiGroups:
920+
- ""
921+
resources:
922+
- nodes/configz
923+
- nodes/healthz
924+
- nodes/pods
925+
verbs:
926+
- '*'
919927
- apiVersion: rbac.authorization.k8s.io/v1
920928
kind: ClusterRole
921929
metadata:

test/e2e/feature/feature.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ var (
245245
// TODO: document the feature (owning SIG, when to use this feature for a test)
246246
KubeletCredentialProviders = framework.WithFeature(framework.ValidFeatures.Add("KubeletCredentialProviders"))
247247

248+
KubeletFineGrainedAuthz = framework.WithFeature(framework.ValidFeatures.Add("KubeletFineGrainedAuthz"))
249+
248250
// TODO: document the feature (owning SIG, when to use this feature for a test)
249251
KubeletSecurity = framework.WithFeature(framework.ValidFeatures.Add("KubeletSecurity"))
250252

test/e2e/framework/auth/helpers.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,29 @@ type bindingsGetter interface {
4343
v1rbac.ClusterRolesGetter
4444
}
4545

46+
// WaitForAuthzUpdate checks if the give user can perform named verb and action
47+
// on a resource or subresource.
48+
func WaitForAuthzUpdate(ctx context.Context, c v1authorization.SubjectAccessReviewsGetter, user string, ra *authorizationv1.ResourceAttributes, allowed bool) error {
49+
review := &authorizationv1.SubjectAccessReview{
50+
Spec: authorizationv1.SubjectAccessReviewSpec{
51+
ResourceAttributes: ra,
52+
User: user,
53+
},
54+
}
55+
56+
err := wait.PollUntilContextTimeout(ctx, policyCachePollInterval, policyCachePollTimeout, false, func(ctx context.Context) (bool, error) {
57+
response, err := c.SubjectAccessReviews().Create(ctx, review, metav1.CreateOptions{})
58+
if err != nil {
59+
return false, err
60+
}
61+
if response.Status.Allowed != allowed {
62+
return false, nil
63+
}
64+
return true, nil
65+
})
66+
return err
67+
}
68+
4669
// WaitForAuthorizationUpdate checks if the given user can perform the named verb and action.
4770
// If policyCachePollTimeout is reached without the expected condition matching, an error is returned
4871
func WaitForAuthorizationUpdate(ctx context.Context, c v1authorization.SubjectAccessReviewsGetter, user, namespace, verb string, resource schema.GroupResource, allowed bool) error {
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
Copyright 2025 The Kubernetes 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 e2enode
18+
19+
import (
20+
"context"
21+
"crypto/tls"
22+
"fmt"
23+
"net/http"
24+
25+
"github.com/onsi/ginkgo/v2"
26+
"github.com/onsi/gomega"
27+
authenticationv1 "k8s.io/api/authentication/v1"
28+
authorizationv1 "k8s.io/api/authorization/v1"
29+
v1 "k8s.io/api/core/v1"
30+
rbacv1 "k8s.io/api/rbac/v1"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apiserver/pkg/authentication/serviceaccount"
33+
"k8s.io/kubernetes/pkg/cluster/ports"
34+
"k8s.io/kubernetes/test/e2e/feature"
35+
"k8s.io/kubernetes/test/e2e/framework"
36+
e2eauth "k8s.io/kubernetes/test/e2e/framework/auth"
37+
)
38+
39+
var _ = SIGDescribe("Kubelet Authz", feature.KubeletFineGrainedAuthz, func() {
40+
f := framework.NewDefaultFramework("kubelet-authz-test")
41+
ginkgo.Context("when calling kubelet API", func() {
42+
ginkgo.It("check /healthz enpoint is accessible via nodes/healthz RBAC", func(ctx context.Context) {
43+
sc := runKubeletAuthzTest(ctx, f, "healthz", "healthz")
44+
gomega.Expect(sc).To(gomega.Equal(http.StatusOK))
45+
})
46+
ginkgo.It("check /healthz enpoint is accessible via nodes/proxy RBAC", func(ctx context.Context) {
47+
sc := runKubeletAuthzTest(ctx, f, "healthz", "proxy")
48+
gomega.Expect(sc).To(gomega.Equal(http.StatusOK))
49+
})
50+
ginkgo.It("check /healthz enpoint is not accessible via nodes/configz RBAC", func(ctx context.Context) {
51+
sc := runKubeletAuthzTest(ctx, f, "healthz", "configz")
52+
gomega.Expect(sc).To(gomega.Equal(http.StatusUnauthorized))
53+
})
54+
})
55+
})
56+
57+
func runKubeletAuthzTest(ctx context.Context, f *framework.Framework, endpoint, authzSubresource string) int {
58+
ns := f.Namespace.Name
59+
saName := authzSubresource
60+
crName := authzSubresource
61+
verb := "get"
62+
resource := "nodes"
63+
_, err := f.ClientSet.CoreV1().ServiceAccounts(ns).Create(ctx, &v1.ServiceAccount{
64+
ObjectMeta: metav1.ObjectMeta{
65+
Name: saName,
66+
Namespace: ns,
67+
},
68+
}, metav1.CreateOptions{})
69+
framework.ExpectNoError(err)
70+
71+
_, err = f.ClientSet.RbacV1().ClusterRoles().Create(ctx, &rbacv1.ClusterRole{
72+
ObjectMeta: metav1.ObjectMeta{
73+
Name: crName,
74+
},
75+
Rules: []rbacv1.PolicyRule{
76+
{
77+
Verbs: []string{verb},
78+
Resources: []string{resource + "/" + authzSubresource},
79+
},
80+
},
81+
}, metav1.CreateOptions{})
82+
framework.ExpectNoError(err)
83+
84+
subject := rbacv1.Subject{
85+
Kind: rbacv1.ServiceAccountKind,
86+
Namespace: ns,
87+
Name: saName,
88+
}
89+
90+
err = e2eauth.BindClusterRole(ctx, f.ClientSet.RbacV1(), crName, ns, subject)
91+
framework.ExpectNoError(err)
92+
93+
err = e2eauth.WaitForAuthzUpdate(ctx, f.ClientSet.AuthorizationV1(),
94+
serviceaccount.MakeUsername(ns, saName),
95+
&authorizationv1.ResourceAttributes{
96+
Namespace: ns,
97+
Verb: verb,
98+
Resource: resource,
99+
Subresource: authzSubresource,
100+
},
101+
true,
102+
)
103+
framework.ExpectNoError(err)
104+
105+
tr, err := f.ClientSet.CoreV1().ServiceAccounts(ns).CreateToken(ctx, saName, &authenticationv1.TokenRequest{}, metav1.CreateOptions{})
106+
framework.ExpectNoError(err)
107+
108+
resp, err := healthCheck(fmt.Sprintf("https://127.0.0.1:%d/%s", ports.KubeletPort, endpoint), tr.Status.Token)
109+
framework.ExpectNoError(err)
110+
return resp.StatusCode
111+
}
112+
113+
func healthCheck(url, token string) (*http.Response, error) {
114+
insecureTransport := http.DefaultTransport.(*http.Transport).Clone()
115+
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
116+
insecureHTTPClient := &http.Client{
117+
Transport: insecureTransport,
118+
}
119+
120+
req, err := http.NewRequest(http.MethodGet, url, nil)
121+
if err != nil {
122+
return nil, err
123+
}
124+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
125+
return insecureHTTPClient.Do(req)
126+
}

test/featuregates_linter/test_data/versioned_feature_list.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,10 @@
656656
lockToDefault: false
657657
preRelease: Alpha
658658
version: "1.32"
659+
- default: true
660+
lockToDefault: false
661+
preRelease: Beta
662+
version: "1.33"
659663
- name: KubeletInUserNamespace
660664
versionedSpecs:
661665
- default: false

0 commit comments

Comments
 (0)