mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 23:37:01 +00:00
KEP-2862: Graduate to BETA.
This commit is contained in:
parent
7f1abe993c
commit
3a780a1c1b
@ -458,6 +458,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
|
|||||||
|
|
||||||
KubeletFineGrainedAuthz: {
|
KubeletFineGrainedAuthz: {
|
||||||
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
|
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
|
||||||
|
{Version: version.MustParse("1.33"), Default: true, PreRelease: featuregate.Beta},
|
||||||
},
|
},
|
||||||
|
|
||||||
KubeletInUserNamespace: {
|
KubeletInUserNamespace: {
|
||||||
|
@ -534,39 +534,44 @@ func TestAuthzCoverage(t *testing.T) {
|
|||||||
fw := newServerTest()
|
fw := newServerTest()
|
||||||
defer fw.testHTTPServer.Close()
|
defer fw.testHTTPServer.Close()
|
||||||
|
|
||||||
// method:path -> has coverage
|
for _, fineGrained := range []bool{false, true} {
|
||||||
expectedCases := map[string]bool{}
|
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
|
// Test all the non-web-service handlers
|
||||||
for _, path := range fw.serverUnderTest.restfulCont.RegisteredHandlePaths() {
|
for _, path := range fw.serverUnderTest.restfulCont.RegisteredHandlePaths() {
|
||||||
expectedCases["GET:"+path] = false
|
expectedCases["GET:"+path] = false
|
||||||
expectedCases["POST:"+path] = false
|
expectedCases["POST:"+path] = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test all the generated web-service paths
|
// Test all the generated web-service paths
|
||||||
for _, ws := range fw.serverUnderTest.restfulCont.RegisteredWebServices() {
|
for _, ws := range fw.serverUnderTest.restfulCont.RegisteredWebServices() {
|
||||||
for _, r := range ws.Routes() {
|
for _, r := range ws.Routes() {
|
||||||
expectedCases[r.Method+":"+r.Path] = false
|
expectedCases[r.Method+":"+r.Path] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a sanity check that the Handle->HandleWithFilter() delegation is working
|
// 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
|
// Ideally, these would move to registered web services and this list would get shorter
|
||||||
expectedPaths := []string{"/healthz", "/metrics", "/metrics/cadvisor"}
|
expectedPaths := []string{"/healthz", "/metrics", "/metrics/cadvisor"}
|
||||||
for _, expectedPath := range expectedPaths {
|
for _, expectedPath := range expectedPaths {
|
||||||
if _, expected := expectedCases["GET:"+expectedPath]; !expected {
|
if _, expected := expectedCases["GET:"+expectedPath]; !expected {
|
||||||
t.Errorf("Expected registered handle path %s was missing", expectedPath)
|
t.Errorf("Expected registered handle path %s was missing", expectedPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range AuthzTestCases(false) {
|
for _, tc := range AuthzTestCases(fineGrained) {
|
||||||
expectedCases[tc.Method+":"+tc.Path] = true
|
expectedCases[tc.Method+":"+tc.Path] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for tc, found := range expectedCases {
|
for tc, found := range expectedCases {
|
||||||
if !found {
|
if !found {
|
||||||
t.Errorf("Missing authz test case for %s", tc)
|
t.Errorf("Missing authz test case for %s", tc)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -580,43 +585,47 @@ func TestAuthFilters(t *testing.T) {
|
|||||||
|
|
||||||
attributesGetter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
|
attributesGetter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
|
||||||
|
|
||||||
for _, tc := range AuthzTestCases(false) {
|
for _, fineGraned := range []bool{false, true} {
|
||||||
t.Run(tc.Method+":"+tc.Path, func(t *testing.T) {
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletFineGrainedAuthz, fineGraned)
|
||||||
var (
|
for _, tc := range AuthzTestCases(fineGraned) {
|
||||||
expectedUser = AuthzTestUser()
|
t.Run(fmt.Sprintf("method=%v:path=%v:fineGrained=%v", tc.Method, tc.Method, fineGraned), func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
expectedUser = AuthzTestUser()
|
||||||
|
|
||||||
calledAuthenticate = false
|
calledAuthenticate = false
|
||||||
calledAuthorize = false
|
calledAuthorize = false
|
||||||
calledAttributes = false
|
calledAttributes = false
|
||||||
)
|
)
|
||||||
|
|
||||||
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
|
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
|
||||||
calledAuthenticate = true
|
calledAuthenticate = true
|
||||||
return &authenticator.Response{User: expectedUser}, true, nil
|
return &authenticator.Response{User: expectedUser}, true, nil
|
||||||
}
|
}
|
||||||
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) []authorizer.Attributes {
|
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) []authorizer.Attributes {
|
||||||
calledAttributes = true
|
calledAttributes = true
|
||||||
require.Equal(t, expectedUser, u)
|
require.Equal(t, expectedUser, u)
|
||||||
return attributesGetter.GetRequestAttributes(u, req)
|
attrs := attributesGetter.GetRequestAttributes(u, req)
|
||||||
}
|
tc.AssertAttributes(t, attrs)
|
||||||
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
|
return attrs
|
||||||
calledAuthorize = true
|
}
|
||||||
tc.AssertAttributes(t, []authorizer.Attributes{a})
|
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
|
||||||
return authorizer.DecisionNoOpinion, "", nil
|
calledAuthorize = true
|
||||||
}
|
return authorizer.DecisionNoOpinion, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(tc.Method, fw.testHTTPServer.URL+tc.Path, nil)
|
req, err := http.NewRequest(tc.Method, fw.testHTTPServer.URL+tc.Path, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close() //nolint:errcheck
|
||||||
|
|
||||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||||
assert.True(t, calledAuthenticate, "Authenticate was not called")
|
assert.True(t, calledAuthenticate, "Authenticate was not called")
|
||||||
assert.True(t, calledAttributes, "Attributes were not called")
|
assert.True(t, calledAttributes, "Attributes were not called")
|
||||||
assert.True(t, calledAuthorize, "Authorize was not called")
|
assert.True(t, calledAuthorize, "Authorize was not called")
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -916,6 +916,14 @@ items:
|
|||||||
- nodes/stats
|
- nodes/stats
|
||||||
verbs:
|
verbs:
|
||||||
- '*'
|
- '*'
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- nodes/configz
|
||||||
|
- nodes/healthz
|
||||||
|
- nodes/pods
|
||||||
|
verbs:
|
||||||
|
- '*'
|
||||||
- apiVersion: rbac.authorization.k8s.io/v1
|
- apiVersion: rbac.authorization.k8s.io/v1
|
||||||
kind: ClusterRole
|
kind: ClusterRole
|
||||||
metadata:
|
metadata:
|
||||||
|
@ -244,6 +244,8 @@ var (
|
|||||||
// TODO: document the feature (owning SIG, when to use this feature for a test)
|
// TODO: document the feature (owning SIG, when to use this feature for a test)
|
||||||
KubeletCredentialProviders = framework.WithFeature(framework.ValidFeatures.Add("KubeletCredentialProviders"))
|
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)
|
// TODO: document the feature (owning SIG, when to use this feature for a test)
|
||||||
KubeletSecurity = framework.WithFeature(framework.ValidFeatures.Add("KubeletSecurity"))
|
KubeletSecurity = framework.WithFeature(framework.ValidFeatures.Add("KubeletSecurity"))
|
||||||
|
|
||||||
|
@ -43,6 +43,29 @@ type bindingsGetter interface {
|
|||||||
v1rbac.ClusterRolesGetter
|
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.
|
// 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
|
// 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 {
|
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)
|
||||||
|
}
|
@ -670,6 +670,10 @@
|
|||||||
lockToDefault: false
|
lockToDefault: false
|
||||||
preRelease: Alpha
|
preRelease: Alpha
|
||||||
version: "1.32"
|
version: "1.32"
|
||||||
|
- default: true
|
||||||
|
lockToDefault: false
|
||||||
|
preRelease: Beta
|
||||||
|
version: "1.33"
|
||||||
- name: KubeletInUserNamespace
|
- name: KubeletInUserNamespace
|
||||||
versionedSpecs:
|
versionedSpecs:
|
||||||
- default: false
|
- default: false
|
||||||
|
Loading…
Reference in New Issue
Block a user