mirror of
https://github.com/kubernetes/kubernetes.git
synced 2025-02-06 09:45:15 +00:00
Merge pull request #129656 from vinayakankugoyal/kep2862beta
KEP-2862: Graduate to BETA.
This commit is contained in:
commit
2bda5dd8c7
@ -452,6 +452,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
|
||||
|
||||
KubeletFineGrainedAuthz: {
|
||||
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
|
||||
{Version: version.MustParse("1.33"), Default: true, PreRelease: featuregate.Beta},
|
||||
},
|
||||
|
||||
KubeletInUserNamespace: {
|
||||
|
@ -534,39 +534,44 @@ func TestAuthzCoverage(t *testing.T) {
|
||||
fw := newServerTest()
|
||||
defer fw.testHTTPServer.Close()
|
||||
|
||||
// method:path -> has coverage
|
||||
expectedCases := map[string]bool{}
|
||||
for _, fineGrained := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("fineGrained=%v", fineGrained), func(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletFineGrainedAuthz, fineGrained)
|
||||
// method:path -> has coverage
|
||||
expectedCases := map[string]bool{}
|
||||
|
||||
// Test all the non-web-service handlers
|
||||
for _, path := range fw.serverUnderTest.restfulCont.RegisteredHandlePaths() {
|
||||
expectedCases["GET:"+path] = false
|
||||
expectedCases["POST:"+path] = false
|
||||
}
|
||||
// Test all the non-web-service handlers
|
||||
for _, path := range fw.serverUnderTest.restfulCont.RegisteredHandlePaths() {
|
||||
expectedCases["GET:"+path] = false
|
||||
expectedCases["POST:"+path] = false
|
||||
}
|
||||
|
||||
// Test all the generated web-service paths
|
||||
for _, ws := range fw.serverUnderTest.restfulCont.RegisteredWebServices() {
|
||||
for _, r := range ws.Routes() {
|
||||
expectedCases[r.Method+":"+r.Path] = false
|
||||
}
|
||||
}
|
||||
// Test all the generated web-service paths
|
||||
for _, ws := range fw.serverUnderTest.restfulCont.RegisteredWebServices() {
|
||||
for _, r := range ws.Routes() {
|
||||
expectedCases[r.Method+":"+r.Path] = false
|
||||
}
|
||||
}
|
||||
|
||||
// This is a sanity check that the Handle->HandleWithFilter() delegation is working
|
||||
// Ideally, these would move to registered web services and this list would get shorter
|
||||
expectedPaths := []string{"/healthz", "/metrics", "/metrics/cadvisor"}
|
||||
for _, expectedPath := range expectedPaths {
|
||||
if _, expected := expectedCases["GET:"+expectedPath]; !expected {
|
||||
t.Errorf("Expected registered handle path %s was missing", expectedPath)
|
||||
}
|
||||
}
|
||||
// This is a sanity check that the Handle->HandleWithFilter() delegation is working
|
||||
// Ideally, these would move to registered web services and this list would get shorter
|
||||
expectedPaths := []string{"/healthz", "/metrics", "/metrics/cadvisor"}
|
||||
for _, expectedPath := range expectedPaths {
|
||||
if _, expected := expectedCases["GET:"+expectedPath]; !expected {
|
||||
t.Errorf("Expected registered handle path %s was missing", expectedPath)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range AuthzTestCases(false) {
|
||||
expectedCases[tc.Method+":"+tc.Path] = true
|
||||
}
|
||||
for _, tc := range AuthzTestCases(fineGrained) {
|
||||
expectedCases[tc.Method+":"+tc.Path] = true
|
||||
}
|
||||
|
||||
for tc, found := range expectedCases {
|
||||
if !found {
|
||||
t.Errorf("Missing authz test case for %s", tc)
|
||||
}
|
||||
for tc, found := range expectedCases {
|
||||
if !found {
|
||||
t.Errorf("Missing authz test case for %s", tc)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -580,43 +585,47 @@ func TestAuthFilters(t *testing.T) {
|
||||
|
||||
attributesGetter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
|
||||
|
||||
for _, tc := range AuthzTestCases(false) {
|
||||
t.Run(tc.Method+":"+tc.Path, func(t *testing.T) {
|
||||
var (
|
||||
expectedUser = AuthzTestUser()
|
||||
for _, fineGraned := range []bool{false, true} {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletFineGrainedAuthz, fineGraned)
|
||||
for _, tc := range AuthzTestCases(fineGraned) {
|
||||
t.Run(fmt.Sprintf("method=%v:path=%v:fineGrained=%v", tc.Method, tc.Method, fineGraned), func(t *testing.T) {
|
||||
var (
|
||||
expectedUser = AuthzTestUser()
|
||||
|
||||
calledAuthenticate = false
|
||||
calledAuthorize = false
|
||||
calledAttributes = false
|
||||
)
|
||||
calledAuthenticate = false
|
||||
calledAuthorize = false
|
||||
calledAttributes = false
|
||||
)
|
||||
|
||||
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
|
||||
calledAuthenticate = true
|
||||
return &authenticator.Response{User: expectedUser}, true, nil
|
||||
}
|
||||
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) []authorizer.Attributes {
|
||||
calledAttributes = true
|
||||
require.Equal(t, expectedUser, u)
|
||||
return attributesGetter.GetRequestAttributes(u, req)
|
||||
}
|
||||
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
|
||||
calledAuthorize = true
|
||||
tc.AssertAttributes(t, []authorizer.Attributes{a})
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
|
||||
calledAuthenticate = true
|
||||
return &authenticator.Response{User: expectedUser}, true, nil
|
||||
}
|
||||
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) []authorizer.Attributes {
|
||||
calledAttributes = true
|
||||
require.Equal(t, expectedUser, u)
|
||||
attrs := attributesGetter.GetRequestAttributes(u, req)
|
||||
tc.AssertAttributes(t, attrs)
|
||||
return attrs
|
||||
}
|
||||
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
|
||||
calledAuthorize = true
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(tc.Method, fw.testHTTPServer.URL+tc.Path, nil)
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest(tc.Method, fw.testHTTPServer.URL+tc.Path, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
assert.True(t, calledAuthenticate, "Authenticate was not called")
|
||||
assert.True(t, calledAttributes, "Attributes were not called")
|
||||
assert.True(t, calledAuthorize, "Authorize was not called")
|
||||
})
|
||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
assert.True(t, calledAuthenticate, "Authenticate was not called")
|
||||
assert.True(t, calledAttributes, "Attributes were not called")
|
||||
assert.True(t, calledAuthorize, "Authorize was not called")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -916,6 +916,14 @@ items:
|
||||
- nodes/stats
|
||||
verbs:
|
||||
- '*'
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- nodes/configz
|
||||
- nodes/healthz
|
||||
- nodes/pods
|
||||
verbs:
|
||||
- '*'
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
|
@ -245,6 +245,8 @@ var (
|
||||
// TODO: document the feature (owning SIG, when to use this feature for a test)
|
||||
KubeletCredentialProviders = framework.WithFeature(framework.ValidFeatures.Add("KubeletCredentialProviders"))
|
||||
|
||||
KubeletFineGrainedAuthz = framework.WithFeature(framework.ValidFeatures.Add("KubeletFineGrainedAuthz"))
|
||||
|
||||
// TODO: document the feature (owning SIG, when to use this feature for a test)
|
||||
KubeletSecurity = framework.WithFeature(framework.ValidFeatures.Add("KubeletSecurity"))
|
||||
|
||||
|
@ -43,6 +43,29 @@ type bindingsGetter interface {
|
||||
v1rbac.ClusterRolesGetter
|
||||
}
|
||||
|
||||
// WaitForAuthzUpdate checks if the give user can perform named verb and action
|
||||
// on a resource or subresource.
|
||||
func WaitForAuthzUpdate(ctx context.Context, c v1authorization.SubjectAccessReviewsGetter, user string, ra *authorizationv1.ResourceAttributes, allowed bool) error {
|
||||
review := &authorizationv1.SubjectAccessReview{
|
||||
Spec: authorizationv1.SubjectAccessReviewSpec{
|
||||
ResourceAttributes: ra,
|
||||
User: user,
|
||||
},
|
||||
}
|
||||
|
||||
err := wait.PollUntilContextTimeout(ctx, policyCachePollInterval, policyCachePollTimeout, false, func(ctx context.Context) (bool, error) {
|
||||
response, err := c.SubjectAccessReviews().Create(ctx, review, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if response.Status.Allowed != allowed {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// WaitForAuthorizationUpdate checks if the given user can perform the named verb and action.
|
||||
// If policyCachePollTimeout is reached without the expected condition matching, an error is returned
|
||||
func WaitForAuthorizationUpdate(ctx context.Context, c v1authorization.SubjectAccessReviewsGetter, user, namespace, verb string, resource schema.GroupResource, allowed bool) error {
|
||||
|
126
test/e2e_node/kubelet_authz_test.go
Normal file
126
test/e2e_node/kubelet_authz_test.go
Normal file
@ -0,0 +1,126 @@
|
||||
/*
|
||||
Copyright 2025 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 e2enode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
"github.com/onsi/gomega"
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||
"k8s.io/kubernetes/pkg/cluster/ports"
|
||||
"k8s.io/kubernetes/test/e2e/feature"
|
||||
"k8s.io/kubernetes/test/e2e/framework"
|
||||
e2eauth "k8s.io/kubernetes/test/e2e/framework/auth"
|
||||
)
|
||||
|
||||
var _ = SIGDescribe("Kubelet Authz", feature.KubeletFineGrainedAuthz, func() {
|
||||
f := framework.NewDefaultFramework("kubelet-authz-test")
|
||||
ginkgo.Context("when calling kubelet API", func() {
|
||||
ginkgo.It("check /healthz enpoint is accessible via nodes/healthz RBAC", func(ctx context.Context) {
|
||||
sc := runKubeletAuthzTest(ctx, f, "healthz", "healthz")
|
||||
gomega.Expect(sc).To(gomega.Equal(http.StatusOK))
|
||||
})
|
||||
ginkgo.It("check /healthz enpoint is accessible via nodes/proxy RBAC", func(ctx context.Context) {
|
||||
sc := runKubeletAuthzTest(ctx, f, "healthz", "proxy")
|
||||
gomega.Expect(sc).To(gomega.Equal(http.StatusOK))
|
||||
})
|
||||
ginkgo.It("check /healthz enpoint is not accessible via nodes/configz RBAC", func(ctx context.Context) {
|
||||
sc := runKubeletAuthzTest(ctx, f, "healthz", "configz")
|
||||
gomega.Expect(sc).To(gomega.Equal(http.StatusUnauthorized))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func runKubeletAuthzTest(ctx context.Context, f *framework.Framework, endpoint, authzSubresource string) int {
|
||||
ns := f.Namespace.Name
|
||||
saName := authzSubresource
|
||||
crName := authzSubresource
|
||||
verb := "get"
|
||||
resource := "nodes"
|
||||
_, err := f.ClientSet.CoreV1().ServiceAccounts(ns).Create(ctx, &v1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: saName,
|
||||
Namespace: ns,
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
framework.ExpectNoError(err)
|
||||
|
||||
_, err = f.ClientSet.RbacV1().ClusterRoles().Create(ctx, &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: crName,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
Verbs: []string{verb},
|
||||
Resources: []string{resource + "/" + authzSubresource},
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
framework.ExpectNoError(err)
|
||||
|
||||
subject := rbacv1.Subject{
|
||||
Kind: rbacv1.ServiceAccountKind,
|
||||
Namespace: ns,
|
||||
Name: saName,
|
||||
}
|
||||
|
||||
err = e2eauth.BindClusterRole(ctx, f.ClientSet.RbacV1(), crName, ns, subject)
|
||||
framework.ExpectNoError(err)
|
||||
|
||||
err = e2eauth.WaitForAuthzUpdate(ctx, f.ClientSet.AuthorizationV1(),
|
||||
serviceaccount.MakeUsername(ns, saName),
|
||||
&authorizationv1.ResourceAttributes{
|
||||
Namespace: ns,
|
||||
Verb: verb,
|
||||
Resource: resource,
|
||||
Subresource: authzSubresource,
|
||||
},
|
||||
true,
|
||||
)
|
||||
framework.ExpectNoError(err)
|
||||
|
||||
tr, err := f.ClientSet.CoreV1().ServiceAccounts(ns).CreateToken(ctx, saName, &authenticationv1.TokenRequest{}, metav1.CreateOptions{})
|
||||
framework.ExpectNoError(err)
|
||||
|
||||
resp, err := healthCheck(fmt.Sprintf("https://127.0.0.1:%d/%s", ports.KubeletPort, endpoint), tr.Status.Token)
|
||||
framework.ExpectNoError(err)
|
||||
return resp.StatusCode
|
||||
}
|
||||
|
||||
func healthCheck(url, token string) (*http.Response, error) {
|
||||
insecureTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
insecureHTTPClient := &http.Client{
|
||||
Transport: insecureTransport,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
return insecureHTTPClient.Do(req)
|
||||
}
|
@ -656,6 +656,10 @@
|
||||
lockToDefault: false
|
||||
preRelease: Alpha
|
||||
version: "1.32"
|
||||
- default: true
|
||||
lockToDefault: false
|
||||
preRelease: Beta
|
||||
version: "1.33"
|
||||
- name: KubeletInUserNamespace
|
||||
versionedSpecs:
|
||||
- default: false
|
||||
|
Loading…
x
Reference in New Issue
Block a user