Merge pull request #126347 from vinayakankugoyal/kep2862impl

KEP-2862: Fine-grained Kubelet API Authorization
This commit is contained in:
Kubernetes Prow Robot 2024-10-18 03:53:04 +01:00 committed by GitHub
commit f5ae0413ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 327 additions and 119 deletions

View File

@ -293,6 +293,13 @@ const (
// fallback to using it's cgroupDriver option.
KubeletCgroupDriverFromCRI featuregate.Feature = "KubeletCgroupDriverFromCRI"
// owner: @vinayakankugoyal
// kep: http://kep.k8s.io/2862
//
// Enable fine-grained kubelet API authorization for webhook based
// authorization.
KubeletFineGrainedAuthz featuregate.Feature = "KubeletFineGrainedAuthz"
// owner: @AkihiroSuda
//
// Enables support for running kubelet in a user namespace.

View File

@ -412,6 +412,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
},
KubeletFineGrainedAuthz: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
KubeletInUserNamespace: {
{Version: version.MustParse("1.22"), Default: false, PreRelease: featuregate.Alpha},
},

View File

@ -24,26 +24,30 @@ import (
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/server/healthz"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/component-base/configz"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/features"
)
// KubeletAuth implements AuthInterface
type KubeletAuth struct {
// authenticator identifies the user for requests to the Kubelet API
authenticator.Request
// authorizerAttributeGetter builds authorization.Attributes for a request to the Kubelet API
authorizer.RequestAttributesGetter
// KubeletRequestAttributesGetter builds authorization.Attributes for a request to the Kubelet API
NodeRequestAttributesGetter
// authorizer determines whether a given authorization.Attributes is allowed
authorizer.Authorizer
}
// NewKubeletAuth returns a kubelet.AuthInterface composed of the given authenticator, attribute getter, and authorizer
func NewKubeletAuth(authenticator authenticator.Request, authorizerAttributeGetter authorizer.RequestAttributesGetter, authorizer authorizer.Authorizer) AuthInterface {
func NewKubeletAuth(authenticator authenticator.Request, authorizerAttributeGetter NodeRequestAttributesGetter, authorizer authorizer.Authorizer) AuthInterface {
return &KubeletAuth{authenticator, authorizerAttributeGetter, authorizer}
}
// NewNodeAuthorizerAttributesGetter creates a new authorizer.RequestAttributesGetter for the node.
func NewNodeAuthorizerAttributesGetter(nodeName types.NodeName) authorizer.RequestAttributesGetter {
func NewNodeAuthorizerAttributesGetter(nodeName types.NodeName) NodeRequestAttributesGetter {
return nodeAuthorizerAttributesGetter{nodeName: nodeName}
}
@ -60,10 +64,15 @@ func isSubpath(subpath, path string) bool {
// Default attributes are: {apiVersion=v1,verb=<http verb from request>,resource=nodes,name=<node name>,subresource=proxy}
// More specific verb/resource is set for the following request patterns:
//
// /stats/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource=stats
// /metrics/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource=metrics
// /logs/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource=log
func (n nodeAuthorizerAttributesGetter) GetRequestAttributes(u user.Info, r *http.Request) authorizer.Attributes {
// /stats/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=stats
// /metrics/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=metrics
// /logs/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=log
// /checkpoint/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=checkpoint
// /pods/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=pods,proxy
// /runningPods/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=pods,proxy
// /healthz/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=healthz,proxy
// /configz => verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=configz,proxy
func (n nodeAuthorizerAttributesGetter) GetRequestAttributes(u user.Info, r *http.Request) []authorizer.Attributes {
apiVerb := ""
switch r.Method {
@ -81,35 +90,54 @@ func (n nodeAuthorizerAttributesGetter) GetRequestAttributes(u user.Info, r *htt
requestPath := r.URL.Path
// Default attributes mirror the API attributes that would allow this access to the kubelet API
attrs := authorizer.AttributesRecord{
User: u,
Verb: apiVerb,
Namespace: "",
APIGroup: "",
APIVersion: "v1",
Resource: "nodes",
Subresource: "proxy",
Name: string(n.nodeName),
ResourceRequest: true,
Path: requestPath,
var subresources []string
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletFineGrainedAuthz) {
switch {
case isSubpath(requestPath, podsPath):
subresources = append(subresources, "pods")
case isSubpath(requestPath, healthz.DefaultHealthzPath):
subresources = append(subresources, "healthz")
case isSubpath(requestPath, configz.DefaultConfigzPath):
subresources = append(subresources, "configz")
// We put runningpods last since it will allocate a new string on every
// check since the handler path has a trailing slash.
case isSubpath(requestPath, runningPodsPath):
subresources = append(subresources, "pods")
}
}
// Override subresource for specific paths
// This allows subdividing access to the kubelet API
switch {
case isSubpath(requestPath, statsPath):
attrs.Subresource = "stats"
subresources = append(subresources, "stats")
case isSubpath(requestPath, metricsPath):
attrs.Subresource = "metrics"
subresources = append(subresources, "metrics")
case isSubpath(requestPath, logsPath):
// "log" to match other log subresources (pods/log, etc)
attrs.Subresource = "log"
subresources = append(subresources, "log")
case isSubpath(requestPath, checkpointPath):
attrs.Subresource = "checkpoint"
subresources = append(subresources, "checkpoint")
default:
subresources = append(subresources, "proxy")
}
klog.V(5).InfoS("Node request attributes", "user", attrs.GetUser().GetName(), "verb", attrs.GetVerb(), "resource", attrs.GetResource(), "subresource", attrs.GetSubresource())
var attrs []authorizer.Attributes
for _, subresource := range subresources {
attr := authorizer.AttributesRecord{
User: u,
Verb: apiVerb,
Namespace: "",
APIGroup: "",
APIVersion: "v1",
Resource: "nodes",
Subresource: subresource,
Name: string(n.nodeName),
ResourceRequest: true,
Path: requestPath,
}
attrs = append(attrs, attr)
}
klog.V(5).InfoS("Node request attributes", "user", attrs[0].GetUser().GetName(), "verb", attrs[0].GetVerb(), "resource", attrs[0].GetResource(), "subresource(s)", subresources)
return attrs
}

View File

@ -24,6 +24,9 @@ import (
"github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/pkg/features"
)
func TestIsSubPath(t *testing.T) {
@ -61,16 +64,19 @@ func TestIsSubPath(t *testing.T) {
}
func TestGetRequestAttributes(t *testing.T) {
for _, test := range AuthzTestCases() {
t.Run(test.Method+":"+test.Path, func(t *testing.T) {
getter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
for _, fineGrained := range []bool{false, true} {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletFineGrainedAuthz, fineGrained)
for _, test := range AuthzTestCases(fineGrained) {
t.Run(test.Method+":"+test.Path, func(t *testing.T) {
getter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
req, err := http.NewRequest(test.Method, "https://localhost:1234"+test.Path, nil)
require.NoError(t, err)
attrs := getter.GetRequestAttributes(AuthzTestUser(), req)
req, err := http.NewRequest(test.Method, "https://localhost:1234"+test.Path, nil)
require.NoError(t, err)
attrs := getter.GetRequestAttributes(AuthzTestUser(), req)
test.AssertAttributes(t, attrs)
})
test.AssertAttributes(t, attrs)
})
}
}
}
@ -82,60 +88,78 @@ const (
type AuthzTestCase struct {
Method, Path string
ExpectedVerb, ExpectedSubresource string
ExpectedVerb string
ExpectedSubresources []string
}
func (a *AuthzTestCase) AssertAttributes(t *testing.T, attrs authorizer.Attributes) {
expectedAttributes := authorizer.AttributesRecord{
User: AuthzTestUser(),
APIGroup: "",
APIVersion: "v1",
Verb: a.ExpectedVerb,
Resource: "nodes",
Name: authzTestNodeName,
Subresource: a.ExpectedSubresource,
ResourceRequest: true,
Path: a.Path,
func (a *AuthzTestCase) AssertAttributes(t *testing.T, attrs []authorizer.Attributes) {
var expectedAttributes []authorizer.AttributesRecord
for _, subresource := range a.ExpectedSubresources {
expectedAttributes = append(expectedAttributes, authorizer.AttributesRecord{
User: AuthzTestUser(),
APIGroup: "",
APIVersion: "v1",
Verb: a.ExpectedVerb,
Resource: "nodes",
Name: authzTestNodeName,
Subresource: subresource,
ResourceRequest: true,
Path: a.Path,
})
}
assert.Equal(t, expectedAttributes, attrs)
assert.Equal(t, len(attrs), len(expectedAttributes))
for i := range attrs {
assert.Equal(t, attrs[i], expectedAttributes[i])
}
}
func AuthzTestUser() user.Info {
return &user.DefaultInfo{Name: authzTestUserName}
}
func AuthzTestCases() []AuthzTestCase {
func AuthzTestCases(fineGrained bool) []AuthzTestCase {
// Path -> ExpectedSubresource
testPaths := map[string]string{
"/attach/{podNamespace}/{podID}/{containerName}": "proxy",
"/attach/{podNamespace}/{podID}/{uid}/{containerName}": "proxy",
"/checkpoint/{podNamespace}/{podID}/{containerName}": "checkpoint",
"/configz": "proxy",
"/containerLogs/{podNamespace}/{podID}/{containerName}": "proxy",
"/debug/flags/v": "proxy",
"/debug/pprof/{subpath:*}": "proxy",
"/exec/{podNamespace}/{podID}/{containerName}": "proxy",
"/exec/{podNamespace}/{podID}/{uid}/{containerName}": "proxy",
"/healthz": "proxy",
"/healthz/log": "proxy",
"/healthz/ping": "proxy",
"/healthz/syncloop": "proxy",
"/logs/": "log",
"/logs/{logpath:*}": "log",
"/metrics": "metrics",
"/metrics/cadvisor": "metrics",
"/metrics/probes": "metrics",
"/metrics/resource": "metrics",
"/pods/": "proxy",
"/portForward/{podNamespace}/{podID}": "proxy",
"/portForward/{podNamespace}/{podID}/{uid}": "proxy",
"/run/{podNamespace}/{podID}/{containerName}": "proxy",
"/run/{podNamespace}/{podID}/{uid}/{containerName}": "proxy",
"/runningpods/": "proxy",
"/stats/": "stats",
"/stats/summary": "stats",
testPaths := map[string][]string{
"/attach/{podNamespace}/{podID}/{containerName}": {"proxy"},
"/attach/{podNamespace}/{podID}/{uid}/{containerName}": {"proxy"},
"/checkpoint/{podNamespace}/{podID}/{containerName}": {"checkpoint"},
"/configz": {"proxy"},
"/containerLogs/{podNamespace}/{podID}/{containerName}": {"proxy"},
"/debug/flags/v": {"proxy"},
"/debug/pprof/{subpath:*}": {"proxy"},
"/exec/{podNamespace}/{podID}/{containerName}": {"proxy"},
"/exec/{podNamespace}/{podID}/{uid}/{containerName}": {"proxy"},
"/healthz": {"proxy"},
"/healthz/log": {"proxy"},
"/healthz/ping": {"proxy"},
"/healthz/syncloop": {"proxy"},
"/logs/": {"log"},
"/logs/{logpath:*}": {"log"},
"/metrics": {"metrics"},
"/metrics/cadvisor": {"metrics"},
"/metrics/probes": {"metrics"},
"/metrics/resource": {"metrics"},
"/pods/": {"proxy"},
"/portForward/{podNamespace}/{podID}": {"proxy"},
"/portForward/{podNamespace}/{podID}/{uid}": {"proxy"},
"/run/{podNamespace}/{podID}/{containerName}": {"proxy"},
"/run/{podNamespace}/{podID}/{uid}/{containerName}": {"proxy"},
"/runningpods/": {"proxy"},
"/stats/": {"stats"},
"/stats/summary": {"stats"},
}
if fineGrained {
testPaths["/healthz"] = append([]string{"healthz"}, testPaths["/healthz"]...)
testPaths["/healthz/log"] = append([]string{"healthz"}, testPaths["/healthz/log"]...)
testPaths["/healthz/ping"] = append([]string{"healthz"}, testPaths["/healthz/ping"]...)
testPaths["/healthz/syncloop"] = append([]string{"healthz"}, testPaths["/healthz/syncloop"]...)
testPaths["/pods/"] = append([]string{"pods"}, testPaths["/pods/"]...)
testPaths["/runningpods/"] = append([]string{"pods"}, testPaths["/runningpods/"]...)
testPaths["/configz"] = append([]string{"configz"}, testPaths["/configz"]...)
}
testCases := []AuthzTestCase{}
for path, subresource := range testPaths {
testCases = append(testCases,

View File

@ -54,6 +54,7 @@ import (
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/server/healthz"
"k8s.io/apiserver/pkg/server/httplog"
@ -101,6 +102,8 @@ const (
checkpointPath = "/checkpoint/"
pprofBasePath = "/debug/pprof/"
debugFlagPath = "/debug/flags/v"
podsPath = "/pods"
runningPodsPath = "/runningpods/"
)
// Server is a http.Handler which exposes kubelet functionality over HTTP.
@ -240,10 +243,14 @@ func ListenAndServePodResources(endpoint string, providers podresources.PodResou
}
}
type NodeRequestAttributesGetter interface {
GetRequestAttributes(u user.Info, r *http.Request) []authorizer.Attributes
}
// AuthInterface contains all methods required by the auth filters
type AuthInterface interface {
authenticator.Request
authorizer.RequestAttributesGetter
NodeRequestAttributesGetter
authorizer.Authorizer
}
@ -317,19 +324,35 @@ func (s *Server) InstallAuthFilter() {
// Get authorization attributes
attrs := s.auth.GetRequestAttributes(info.User, req.Request)
var allowed bool
var msg string
var subresources []string
for _, attr := range attrs {
subresources = append(subresources, attr.GetSubresource())
decision, _, err := s.auth.Authorize(req.Request.Context(), attr)
if err != nil {
klog.ErrorS(err, "Authorization error", "user", attr.GetUser().GetName(), "verb", attr.GetVerb(), "resource", attr.GetResource(), "subresource", attr.GetSubresource())
msg = fmt.Sprintf("Authorization error (user=%s, verb=%s, resource=%s, subresource=%s)", attr.GetUser().GetName(), attr.GetVerb(), attr.GetResource(), attr.GetSubresource())
resp.WriteErrorString(http.StatusInternalServerError, msg)
return
// Authorize
decision, _, err := s.auth.Authorize(req.Request.Context(), attrs)
if err != nil {
klog.ErrorS(err, "Authorization error", "user", attrs.GetUser().GetName(), "verb", attrs.GetVerb(), "resource", attrs.GetResource(), "subresource", attrs.GetSubresource())
msg := fmt.Sprintf("Authorization error (user=%s, verb=%s, resource=%s, subresource=%s)", attrs.GetUser().GetName(), attrs.GetVerb(), attrs.GetResource(), attrs.GetSubresource())
resp.WriteErrorString(http.StatusInternalServerError, msg)
return
}
if decision == authorizer.DecisionAllow {
allowed = true
break
}
}
if decision != authorizer.DecisionAllow {
klog.V(2).InfoS("Forbidden", "user", attrs.GetUser().GetName(), "verb", attrs.GetVerb(), "resource", attrs.GetResource(), "subresource", attrs.GetSubresource())
msg := fmt.Sprintf("Forbidden (user=%s, verb=%s, resource=%s, subresource=%s)", attrs.GetUser().GetName(), attrs.GetVerb(), attrs.GetResource(), attrs.GetSubresource())
resp.WriteErrorString(http.StatusForbidden, msg)
if !allowed {
if len(attrs) == 0 {
klog.ErrorS(fmt.Errorf("could not determine attributes for request"), "Authorization error")
resp.WriteErrorString(http.StatusForbidden, "Authorization error: could not determine attributes for request")
return
}
// The attributes only differ by subresource so we just use the first one.
klog.V(2).InfoS("Forbidden", "user", attrs[0].GetUser().GetName(), "verb", attrs[0].GetVerb(), "resource", attrs[0].GetResource(), "subresource(s)", subresources)
resp.WriteErrorString(http.StatusForbidden, fmt.Sprintf("Forbidden (user=%s, verb=%s, resource=%s, subresource(s)=%v)\n", attrs[0].GetUser().GetName(), attrs[0].GetVerb(), attrs[0].GetResource(), subresources))
return
}
@ -381,7 +404,7 @@ func (s *Server) InstallDefaultHandlers() {
s.addMetricsBucketMatcher("pods")
ws := new(restful.WebService)
ws.
Path("/pods").
Path(podsPath).
Produces(restful.MIME_JSON)
ws.Route(ws.GET("").
To(s.getPods).
@ -541,7 +564,7 @@ func (s *Server) InstallDebuggingHandlers() {
s.addMetricsBucketMatcher("runningpods")
ws = new(restful.WebService)
ws.
Path("/runningpods/").
Path(runningPodsPath).
Produces(restful.MIME_JSON)
ws.Route(ws.GET("").
To(s.getRunningPods).
@ -565,7 +588,7 @@ func (s *Server) InstallDebuggingDisabledHandlers() {
s.addMetricsBucketMatcher("logs")
paths := []string{
"/run/", "/exec/", "/attach/", "/portForward/", "/containerLogs/",
"/runningpods/", pprofBasePath, logsPath}
runningPodsPath, pprofBasePath, logsPath}
for _, p := range paths {
s.restfulCont.Handle(p, h)
}

View File

@ -291,14 +291,14 @@ func (*fakeKubelet) GetCgroupCPUAndMemoryStats(cgroupName string, updateStats bo
type fakeAuth struct {
authenticateFunc func(*http.Request) (*authenticator.Response, bool, error)
attributesFunc func(user.Info, *http.Request) authorizer.Attributes
attributesFunc func(user.Info, *http.Request) []authorizer.Attributes
authorizeFunc func(authorizer.Attributes) (authorized authorizer.Decision, reason string, err error)
}
func (f *fakeAuth) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
return f.authenticateFunc(req)
}
func (f *fakeAuth) GetRequestAttributes(u user.Info, req *http.Request) authorizer.Attributes {
func (f *fakeAuth) GetRequestAttributes(u user.Info, req *http.Request) []authorizer.Attributes {
return f.attributesFunc(u, req)
}
func (f *fakeAuth) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
@ -349,8 +349,8 @@ func newServerTestWithDebuggingHandlers(kubeCfg *kubeletconfiginternal.KubeletCo
authenticateFunc: func(req *http.Request) (*authenticator.Response, bool, error) {
return &authenticator.Response{User: &user.DefaultInfo{Name: "test"}}, true, nil
},
attributesFunc: func(u user.Info, req *http.Request) authorizer.Attributes {
return &authorizer.AttributesRecord{User: u}
attributesFunc: func(u user.Info, req *http.Request) []authorizer.Attributes {
return []authorizer.Attributes{&authorizer.AttributesRecord{User: u}}
},
authorizeFunc: func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
return authorizer.DecisionAllow, "", nil
@ -546,7 +546,7 @@ func TestAuthzCoverage(t *testing.T) {
}
}
for _, tc := range AuthzTestCases() {
for _, tc := range AuthzTestCases(false) {
expectedCases[tc.Method+":"+tc.Path] = true
}
@ -566,7 +566,7 @@ func TestAuthFilters(t *testing.T) {
attributesGetter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
for _, tc := range AuthzTestCases() {
for _, tc := range AuthzTestCases(false) {
t.Run(tc.Method+":"+tc.Path, func(t *testing.T) {
var (
expectedUser = AuthzTestUser()
@ -580,14 +580,14 @@ func TestAuthFilters(t *testing.T) {
calledAuthenticate = true
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
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, a)
tc.AssertAttributes(t, []authorizer.Attributes{a})
return authorizer.DecisionNoOpinion, "", nil
}
@ -609,7 +609,7 @@ func TestAuthFilters(t *testing.T) {
func TestAuthenticationError(t *testing.T) {
var (
expectedUser = &user.DefaultInfo{Name: "test"}
expectedAttributes = &authorizer.AttributesRecord{User: expectedUser}
expectedAttributes = []authorizer.Attributes{&authorizer.AttributesRecord{User: expectedUser}}
calledAuthenticate = false
calledAuthorize = false
@ -622,7 +622,7 @@ func TestAuthenticationError(t *testing.T) {
calledAuthenticate = true
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
return expectedAttributes
}
@ -647,7 +647,7 @@ func TestAuthenticationError(t *testing.T) {
func TestAuthenticationFailure(t *testing.T) {
var (
expectedUser = &user.DefaultInfo{Name: "test"}
expectedAttributes = &authorizer.AttributesRecord{User: expectedUser}
expectedAttributes = []authorizer.Attributes{&authorizer.AttributesRecord{User: expectedUser}}
calledAuthenticate = false
calledAuthorize = false
@ -660,7 +660,7 @@ func TestAuthenticationFailure(t *testing.T) {
calledAuthenticate = true
return nil, false, 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
return expectedAttributes
}
@ -685,7 +685,7 @@ func TestAuthenticationFailure(t *testing.T) {
func TestAuthorizationSuccess(t *testing.T) {
var (
expectedUser = &user.DefaultInfo{Name: "test"}
expectedAttributes = &authorizer.AttributesRecord{User: expectedUser}
expectedAttributes = []authorizer.Attributes{&authorizer.AttributesRecord{User: expectedUser}}
calledAuthenticate = false
calledAuthorize = false
@ -698,7 +698,7 @@ func TestAuthorizationSuccess(t *testing.T) {
calledAuthenticate = true
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
return expectedAttributes
}
@ -1528,3 +1528,107 @@ func TestTrimURLPath(t *testing.T) {
assert.Equalf(t, test.expected, getURLRootPath(test.path), "path is: %s", test.path)
}
}
func TestFineGrainedAuthz(t *testing.T) {
// Enable features.ContainerCheckpoint during test
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletFineGrainedAuthz, true)
fw := newServerTest()
defer fw.testHTTPServer.Close()
attributesGetter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
testCases := []struct {
name string
path string
expectedSubResources []string
authorizer func(authorizer.Attributes) (authorized authorizer.Decision, reason string, err error)
wantStatusCode int
wantCalledAuthorizeCount int
}{
{
name: "both subresources rejected",
path: "/configz",
expectedSubResources: []string{"configz", "proxy"},
authorizer: func(authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return authorizer.DecisionNoOpinion, "", nil
},
wantStatusCode: 403,
wantCalledAuthorizeCount: 2,
},
{
name: "fine grained rejected, proxy accepted",
path: "/configz",
expectedSubResources: []string{"configz", "proxy"},
authorizer: func(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
switch a.GetSubresource() {
case "configz":
return authorizer.DecisionNoOpinion, "", nil
case "proxy":
return authorizer.DecisionAllow, "", nil
default:
return authorizer.DecisionNoOpinion, "", fmt.Errorf("unexpected subresource %v", a.GetSubresource())
}
},
wantStatusCode: 200,
wantCalledAuthorizeCount: 2,
},
{
name: "fine grained accepted",
path: "/configz",
expectedSubResources: []string{"configz", "proxy"},
authorizer: func(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
switch a.GetSubresource() {
case "configz":
return authorizer.DecisionAllow, "", nil
case "proxy":
return authorizer.DecisionNoOpinion, "", fmt.Errorf("did not expect code to reach here")
default:
return authorizer.DecisionNoOpinion, "", fmt.Errorf("unexpected subresource %v", a.GetSubresource())
}
},
wantStatusCode: 200,
wantCalledAuthorizeCount: 1,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
calledAuthenticate := false
calledAuthorizeCount := 0
calledAttributes := false
fw.fakeAuth.authenticateFunc = func(req *http.Request) (*authenticator.Response, bool, error) {
calledAuthenticate = true
return &authenticator.Response{User: AuthzTestUser()}, true, nil
}
fw.fakeAuth.attributesFunc = func(u user.Info, req *http.Request) []authorizer.Attributes {
calledAttributes = true
attrs := attributesGetter.GetRequestAttributes(u, req)
var gotSubresources []string
for _, attr := range attrs {
gotSubresources = append(gotSubresources, attr.GetSubresource())
}
require.Equal(t, tc.expectedSubResources, gotSubresources)
return attrs
}
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
calledAuthorizeCount += 1
return tc.authorizer(a)
}
req, err := http.NewRequest("GET", fw.testHTTPServer.URL+tc.path, nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, tc.wantStatusCode, resp.StatusCode)
assert.True(t, calledAuthenticate, "Authenticate was not called")
assert.True(t, calledAttributes, "Attributes were not called")
assert.Equal(t, tc.wantCalledAuthorizeCount, calledAuthorizeCount)
})
}
}

View File

@ -388,17 +388,6 @@ func ClusterRoles() []rbacv1.ClusterRole {
eventsRule(),
},
},
{
// a role to use for full access to the kubelet API
ObjectMeta: metav1.ObjectMeta{Name: "system:kubelet-api-admin"},
Rules: []rbacv1.PolicyRule{
// Allow read-only access to the Node API objects
rbacv1helpers.NewRule("get", "list", "watch").Groups(legacyGroup).Resources("nodes").RuleOrDie(),
// Allow all API calls to the nodes
rbacv1helpers.NewRule("proxy").Groups(legacyGroup).Resources("nodes").RuleOrDie(),
rbacv1helpers.NewRule("*").Groups(legacyGroup).Resources("nodes/proxy", "nodes/metrics", "nodes/stats", "nodes/log").RuleOrDie(),
},
},
{
// a role to use for bootstrapping a node's client certificates
ObjectMeta: metav1.ObjectMeta{Name: "system:node-bootstrapper"},
@ -530,6 +519,25 @@ func ClusterRoles() []rbacv1.ClusterRole {
},
})
// Add the cluster role system:kubelet-api-admin
kubeletAPIAdminRules := []rbacv1.PolicyRule{
// Allow read-only access to the Node API objects
rbacv1helpers.NewRule("get", "list", "watch").Groups(legacyGroup).Resources("nodes").RuleOrDie(),
// Allow all API calls to the nodes
rbacv1helpers.NewRule("proxy").Groups(legacyGroup).Resources("nodes").RuleOrDie(),
rbacv1helpers.NewRule("*").Groups(legacyGroup).Resources("nodes/proxy", "nodes/metrics", "nodes/stats", "nodes/log").RuleOrDie(),
}
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletFineGrainedAuthz) {
kubeletAPIAdminRules = append(kubeletAPIAdminRules, rbacv1helpers.NewRule("*").Groups(legacyGroup).Resources("nodes/pods", "nodes/healthz", "nodes/configz").RuleOrDie())
}
roles = append(roles, rbacv1.ClusterRole{
// a role to use for full access to the kubelet API
ObjectMeta: metav1.ObjectMeta{Name: "system:kubelet-api-admin"},
Rules: kubeletAPIAdminRules,
})
// node-proxier role is used by kube-proxy.
nodeProxierRules := []rbacv1.PolicyRule{
rbacv1helpers.NewRule("list", "watch").Groups(legacyGroup).Resources("services", "endpoints").RuleOrDie(),

View File

@ -35,6 +35,8 @@ import (
"k8s.io/klog/v2"
)
const DefaultHealthzPath = "/healthz"
// HealthChecker is a named healthz checker.
type HealthChecker interface {
Name() string
@ -154,7 +156,7 @@ func NamedCheck(name string, check func(r *http.Request) error) HealthChecker {
// exactly one call to InstallHandler. Calling InstallHandler more
// than once for the same mux will result in a panic.
func InstallHandler(mux mux, checks ...HealthChecker) {
InstallPathHandler(mux, "/healthz", checks...)
InstallPathHandler(mux, DefaultHealthzPath, checks...)
}
// InstallReadyzHandler registers handlers for health checking on the path

View File

@ -47,6 +47,8 @@ import (
"sync"
)
const DefaultConfigzPath = "/configz"
var (
configsGuard sync.RWMutex
configs = map[string]*Config{}
@ -61,7 +63,7 @@ type Config struct {
// InstallHandler adds an HTTP handler on the given mux for the "/configz"
// endpoint which serves all registered ComponentConfigs in JSON format.
func InstallHandler(m mux) {
m.Handle("/configz", http.HandlerFunc(handle))
m.Handle(DefaultConfigzPath, http.HandlerFunc(handle))
}
type mux interface {

View File

@ -546,6 +546,12 @@
lockToDefault: false
preRelease: Beta
version: "1.31"
- name: KubeletFineGrainedAuthz
versionedSpecs:
- default: false
lockToDefault: false
preRelease: Alpha
version: "1.32"
- name: KubeletInUserNamespace
versionedSpecs:
- default: false