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. // fallback to using it's cgroupDriver option.
KubeletCgroupDriverFromCRI featuregate.Feature = "KubeletCgroupDriverFromCRI" 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 // owner: @AkihiroSuda
// //
// Enables support for running kubelet in a user namespace. // 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}, {Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
}, },
KubeletFineGrainedAuthz: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
KubeletInUserNamespace: { KubeletInUserNamespace: {
{Version: version.MustParse("1.22"), Default: false, PreRelease: featuregate.Alpha}, {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/authenticator"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "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/klog/v2"
"k8s.io/kubernetes/pkg/features"
) )
// KubeletAuth implements AuthInterface // KubeletAuth implements AuthInterface
type KubeletAuth struct { type KubeletAuth struct {
// authenticator identifies the user for requests to the Kubelet API // authenticator identifies the user for requests to the Kubelet API
authenticator.Request authenticator.Request
// authorizerAttributeGetter builds authorization.Attributes for a request to the Kubelet API // KubeletRequestAttributesGetter builds authorization.Attributes for a request to the Kubelet API
authorizer.RequestAttributesGetter NodeRequestAttributesGetter
// authorizer determines whether a given authorization.Attributes is allowed // authorizer determines whether a given authorization.Attributes is allowed
authorizer.Authorizer authorizer.Authorizer
} }
// NewKubeletAuth returns a kubelet.AuthInterface composed of the given authenticator, attribute getter, and 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} return &KubeletAuth{authenticator, authorizerAttributeGetter, authorizer}
} }
// NewNodeAuthorizerAttributesGetter creates a new authorizer.RequestAttributesGetter for the node. // 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} 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} // 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: // More specific verb/resource is set for the following request patterns:
// //
// /stats/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource=stats // /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=metrics // /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=log // /logs/* => verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=log
func (n nodeAuthorizerAttributesGetter) GetRequestAttributes(u user.Info, r *http.Request) authorizer.Attributes { // /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 := "" apiVerb := ""
switch r.Method { switch r.Method {
@ -81,35 +90,54 @@ func (n nodeAuthorizerAttributesGetter) GetRequestAttributes(u user.Info, r *htt
requestPath := r.URL.Path requestPath := r.URL.Path
// Default attributes mirror the API attributes that would allow this access to the kubelet API var subresources []string
attrs := authorizer.AttributesRecord{ 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")
}
}
switch {
case isSubpath(requestPath, statsPath):
subresources = append(subresources, "stats")
case isSubpath(requestPath, metricsPath):
subresources = append(subresources, "metrics")
case isSubpath(requestPath, logsPath):
// "log" to match other log subresources (pods/log, etc)
subresources = append(subresources, "log")
case isSubpath(requestPath, checkpointPath):
subresources = append(subresources, "checkpoint")
default:
subresources = append(subresources, "proxy")
}
var attrs []authorizer.Attributes
for _, subresource := range subresources {
attr := authorizer.AttributesRecord{
User: u, User: u,
Verb: apiVerb, Verb: apiVerb,
Namespace: "", Namespace: "",
APIGroup: "", APIGroup: "",
APIVersion: "v1", APIVersion: "v1",
Resource: "nodes", Resource: "nodes",
Subresource: "proxy", Subresource: subresource,
Name: string(n.nodeName), Name: string(n.nodeName),
ResourceRequest: true, ResourceRequest: true,
Path: requestPath, Path: requestPath,
} }
attrs = append(attrs, attr)
// Override subresource for specific paths
// This allows subdividing access to the kubelet API
switch {
case isSubpath(requestPath, statsPath):
attrs.Subresource = "stats"
case isSubpath(requestPath, metricsPath):
attrs.Subresource = "metrics"
case isSubpath(requestPath, logsPath):
// "log" to match other log subresources (pods/log, etc)
attrs.Subresource = "log"
case isSubpath(requestPath, checkpointPath):
attrs.Subresource = "checkpoint"
} }
klog.V(5).InfoS("Node request attributes", "user", attrs.GetUser().GetName(), "verb", attrs.GetVerb(), "resource", attrs.GetResource(), "subresource", attrs.GetSubresource()) 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 return attrs
} }

View File

@ -24,6 +24,9 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer" "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) { func TestIsSubPath(t *testing.T) {
@ -61,7 +64,9 @@ func TestIsSubPath(t *testing.T) {
} }
func TestGetRequestAttributes(t *testing.T) { func TestGetRequestAttributes(t *testing.T) {
for _, test := range AuthzTestCases() { 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) { t.Run(test.Method+":"+test.Path, func(t *testing.T) {
getter := NewNodeAuthorizerAttributesGetter(authzTestNodeName) getter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
@ -72,6 +77,7 @@ func TestGetRequestAttributes(t *testing.T) {
test.AssertAttributes(t, attrs) test.AssertAttributes(t, attrs)
}) })
} }
}
} }
const ( const (
@ -82,60 +88,78 @@ const (
type AuthzTestCase struct { type AuthzTestCase struct {
Method, Path string Method, Path string
ExpectedVerb, ExpectedSubresource string ExpectedVerb string
ExpectedSubresources []string
} }
func (a *AuthzTestCase) AssertAttributes(t *testing.T, attrs authorizer.Attributes) { func (a *AuthzTestCase) AssertAttributes(t *testing.T, attrs []authorizer.Attributes) {
expectedAttributes := authorizer.AttributesRecord{ var expectedAttributes []authorizer.AttributesRecord
for _, subresource := range a.ExpectedSubresources {
expectedAttributes = append(expectedAttributes, authorizer.AttributesRecord{
User: AuthzTestUser(), User: AuthzTestUser(),
APIGroup: "", APIGroup: "",
APIVersion: "v1", APIVersion: "v1",
Verb: a.ExpectedVerb, Verb: a.ExpectedVerb,
Resource: "nodes", Resource: "nodes",
Name: authzTestNodeName, Name: authzTestNodeName,
Subresource: a.ExpectedSubresource, Subresource: subresource,
ResourceRequest: true, ResourceRequest: true,
Path: a.Path, 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 { func AuthzTestUser() user.Info {
return &user.DefaultInfo{Name: authzTestUserName} return &user.DefaultInfo{Name: authzTestUserName}
} }
func AuthzTestCases() []AuthzTestCase { func AuthzTestCases(fineGrained bool) []AuthzTestCase {
// Path -> ExpectedSubresource // Path -> ExpectedSubresource
testPaths := map[string]string{ testPaths := map[string][]string{
"/attach/{podNamespace}/{podID}/{containerName}": "proxy", "/attach/{podNamespace}/{podID}/{containerName}": {"proxy"},
"/attach/{podNamespace}/{podID}/{uid}/{containerName}": "proxy", "/attach/{podNamespace}/{podID}/{uid}/{containerName}": {"proxy"},
"/checkpoint/{podNamespace}/{podID}/{containerName}": "checkpoint", "/checkpoint/{podNamespace}/{podID}/{containerName}": {"checkpoint"},
"/configz": "proxy", "/configz": {"proxy"},
"/containerLogs/{podNamespace}/{podID}/{containerName}": "proxy", "/containerLogs/{podNamespace}/{podID}/{containerName}": {"proxy"},
"/debug/flags/v": "proxy", "/debug/flags/v": {"proxy"},
"/debug/pprof/{subpath:*}": "proxy", "/debug/pprof/{subpath:*}": {"proxy"},
"/exec/{podNamespace}/{podID}/{containerName}": "proxy", "/exec/{podNamespace}/{podID}/{containerName}": {"proxy"},
"/exec/{podNamespace}/{podID}/{uid}/{containerName}": "proxy", "/exec/{podNamespace}/{podID}/{uid}/{containerName}": {"proxy"},
"/healthz": "proxy", "/healthz": {"proxy"},
"/healthz/log": "proxy", "/healthz/log": {"proxy"},
"/healthz/ping": "proxy", "/healthz/ping": {"proxy"},
"/healthz/syncloop": "proxy", "/healthz/syncloop": {"proxy"},
"/logs/": "log", "/logs/": {"log"},
"/logs/{logpath:*}": "log", "/logs/{logpath:*}": {"log"},
"/metrics": "metrics", "/metrics": {"metrics"},
"/metrics/cadvisor": "metrics", "/metrics/cadvisor": {"metrics"},
"/metrics/probes": "metrics", "/metrics/probes": {"metrics"},
"/metrics/resource": "metrics", "/metrics/resource": {"metrics"},
"/pods/": "proxy", "/pods/": {"proxy"},
"/portForward/{podNamespace}/{podID}": "proxy", "/portForward/{podNamespace}/{podID}": {"proxy"},
"/portForward/{podNamespace}/{podID}/{uid}": "proxy", "/portForward/{podNamespace}/{podID}/{uid}": {"proxy"},
"/run/{podNamespace}/{podID}/{containerName}": "proxy", "/run/{podNamespace}/{podID}/{containerName}": {"proxy"},
"/run/{podNamespace}/{podID}/{uid}/{containerName}": "proxy", "/run/{podNamespace}/{podID}/{uid}/{containerName}": {"proxy"},
"/runningpods/": "proxy", "/runningpods/": {"proxy"},
"/stats/": "stats", "/stats/": {"stats"},
"/stats/summary": "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{} testCases := []AuthzTestCase{}
for path, subresource := range testPaths { for path, subresource := range testPaths {
testCases = append(testCases, testCases = append(testCases,

View File

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

View File

@ -291,14 +291,14 @@ func (*fakeKubelet) GetCgroupCPUAndMemoryStats(cgroupName string, updateStats bo
type fakeAuth struct { type fakeAuth struct {
authenticateFunc func(*http.Request) (*authenticator.Response, bool, error) 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) authorizeFunc func(authorizer.Attributes) (authorized authorizer.Decision, reason string, err error)
} }
func (f *fakeAuth) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) { func (f *fakeAuth) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
return f.authenticateFunc(req) 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) return f.attributesFunc(u, req)
} }
func (f *fakeAuth) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { 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) { authenticateFunc: func(req *http.Request) (*authenticator.Response, bool, error) {
return &authenticator.Response{User: &user.DefaultInfo{Name: "test"}}, true, nil return &authenticator.Response{User: &user.DefaultInfo{Name: "test"}}, true, nil
}, },
attributesFunc: func(u user.Info, req *http.Request) authorizer.Attributes { attributesFunc: func(u user.Info, req *http.Request) []authorizer.Attributes {
return &authorizer.AttributesRecord{User: u} return []authorizer.Attributes{&authorizer.AttributesRecord{User: u}}
}, },
authorizeFunc: func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) { authorizeFunc: func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
return authorizer.DecisionAllow, "", nil 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 expectedCases[tc.Method+":"+tc.Path] = true
} }
@ -566,7 +566,7 @@ func TestAuthFilters(t *testing.T) {
attributesGetter := NewNodeAuthorizerAttributesGetter(authzTestNodeName) attributesGetter := NewNodeAuthorizerAttributesGetter(authzTestNodeName)
for _, tc := range AuthzTestCases() { for _, tc := range AuthzTestCases(false) {
t.Run(tc.Method+":"+tc.Path, func(t *testing.T) { t.Run(tc.Method+":"+tc.Path, func(t *testing.T) {
var ( var (
expectedUser = AuthzTestUser() expectedUser = AuthzTestUser()
@ -580,14 +580,14 @@ func TestAuthFilters(t *testing.T) {
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) return attributesGetter.GetRequestAttributes(u, req)
} }
fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) { fw.fakeAuth.authorizeFunc = func(a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
calledAuthorize = true calledAuthorize = true
tc.AssertAttributes(t, a) tc.AssertAttributes(t, []authorizer.Attributes{a})
return authorizer.DecisionNoOpinion, "", nil return authorizer.DecisionNoOpinion, "", nil
} }
@ -609,7 +609,7 @@ func TestAuthFilters(t *testing.T) {
func TestAuthenticationError(t *testing.T) { func TestAuthenticationError(t *testing.T) {
var ( var (
expectedUser = &user.DefaultInfo{Name: "test"} expectedUser = &user.DefaultInfo{Name: "test"}
expectedAttributes = &authorizer.AttributesRecord{User: expectedUser} expectedAttributes = []authorizer.Attributes{&authorizer.AttributesRecord{User: expectedUser}}
calledAuthenticate = false calledAuthenticate = false
calledAuthorize = false calledAuthorize = false
@ -622,7 +622,7 @@ func TestAuthenticationError(t *testing.T) {
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
return expectedAttributes return expectedAttributes
} }
@ -647,7 +647,7 @@ func TestAuthenticationError(t *testing.T) {
func TestAuthenticationFailure(t *testing.T) { func TestAuthenticationFailure(t *testing.T) {
var ( var (
expectedUser = &user.DefaultInfo{Name: "test"} expectedUser = &user.DefaultInfo{Name: "test"}
expectedAttributes = &authorizer.AttributesRecord{User: expectedUser} expectedAttributes = []authorizer.Attributes{&authorizer.AttributesRecord{User: expectedUser}}
calledAuthenticate = false calledAuthenticate = false
calledAuthorize = false calledAuthorize = false
@ -660,7 +660,7 @@ func TestAuthenticationFailure(t *testing.T) {
calledAuthenticate = true calledAuthenticate = true
return nil, false, nil 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 calledAttributes = true
return expectedAttributes return expectedAttributes
} }
@ -685,7 +685,7 @@ func TestAuthenticationFailure(t *testing.T) {
func TestAuthorizationSuccess(t *testing.T) { func TestAuthorizationSuccess(t *testing.T) {
var ( var (
expectedUser = &user.DefaultInfo{Name: "test"} expectedUser = &user.DefaultInfo{Name: "test"}
expectedAttributes = &authorizer.AttributesRecord{User: expectedUser} expectedAttributes = []authorizer.Attributes{&authorizer.AttributesRecord{User: expectedUser}}
calledAuthenticate = false calledAuthenticate = false
calledAuthorize = false calledAuthorize = false
@ -698,7 +698,7 @@ func TestAuthorizationSuccess(t *testing.T) {
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
return expectedAttributes return expectedAttributes
} }
@ -1528,3 +1528,107 @@ func TestTrimURLPath(t *testing.T) {
assert.Equalf(t, test.expected, getURLRootPath(test.path), "path is: %s", test.path) 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(), 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 // a role to use for bootstrapping a node's client certificates
ObjectMeta: metav1.ObjectMeta{Name: "system:node-bootstrapper"}, 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. // node-proxier role is used by kube-proxy.
nodeProxierRules := []rbacv1.PolicyRule{ nodeProxierRules := []rbacv1.PolicyRule{
rbacv1helpers.NewRule("list", "watch").Groups(legacyGroup).Resources("services", "endpoints").RuleOrDie(), rbacv1helpers.NewRule("list", "watch").Groups(legacyGroup).Resources("services", "endpoints").RuleOrDie(),

View File

@ -35,6 +35,8 @@ import (
"k8s.io/klog/v2" "k8s.io/klog/v2"
) )
const DefaultHealthzPath = "/healthz"
// HealthChecker is a named healthz checker. // HealthChecker is a named healthz checker.
type HealthChecker interface { type HealthChecker interface {
Name() string 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 // exactly one call to InstallHandler. Calling InstallHandler more
// than once for the same mux will result in a panic. // than once for the same mux will result in a panic.
func InstallHandler(mux mux, checks ...HealthChecker) { func InstallHandler(mux mux, checks ...HealthChecker) {
InstallPathHandler(mux, "/healthz", checks...) InstallPathHandler(mux, DefaultHealthzPath, checks...)
} }
// InstallReadyzHandler registers handlers for health checking on the path // InstallReadyzHandler registers handlers for health checking on the path

View File

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

View File

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