diff --git a/plugin/pkg/admission/noderestriction/admission.go b/plugin/pkg/admission/noderestriction/admission.go index 79581bebe3f..45f790913e6 100644 --- a/plugin/pkg/admission/noderestriction/admission.go +++ b/plugin/pkg/admission/noderestriction/admission.go @@ -34,6 +34,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/admission" apiserveradmission "k8s.io/apiserver/pkg/admission/initializer" + "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/client-go/informers" corev1lister "k8s.io/client-go/listers/core/v1" storagelisters "k8s.io/client-go/listers/storage/v1" @@ -84,6 +85,8 @@ type Plugin struct { pvGetter corev1lister.PersistentVolumeLister csiTranslator csitrans.CSITranslator + authz authorizer.Authorizer + expansionRecoveryEnabled bool dynamicResourceAllocationEnabled bool allowInsecureKubeletCertificateSigningRequests bool @@ -94,6 +97,7 @@ var ( _ admission.Interface = &Plugin{} _ apiserveradmission.WantsExternalKubeInformerFactory = &Plugin{} _ apiserveradmission.WantsFeatures = &Plugin{} + _ apiserveradmission.WantsAuthorizer = &Plugin{} ) // InspectFeatureGates allows setting bools without taking a dep on a global variable @@ -137,10 +141,20 @@ func (p *Plugin) ValidateInitialization() error { if p.pvGetter == nil { return fmt.Errorf("%s requires a PV getter", PluginName) } + if p.authz == nil { + return fmt.Errorf("%s requires an authorizer", PluginName) + } } return nil } +// SetAuthorizer sets the authorizer. +func (p *Plugin) SetAuthorizer(authz authorizer.Authorizer) { + if p.serviceAccountNodeAudienceRestriction { + p.authz = authz + } +} + var ( podResource = api.Resource("pods") nodeResource = api.Resource("nodes") @@ -624,7 +638,7 @@ func (p *Plugin) admitServiceAccount(ctx context.Context, nodeName string, a adm } if p.serviceAccountNodeAudienceRestriction { - if err := p.validateNodeServiceAccountAudience(ctx, tr, pod); err != nil { + if err := p.validateNodeServiceAccountAudience(ctx, tr, pod, a); err != nil { return admission.NewForbidden(a, err) } } @@ -638,7 +652,7 @@ func (p *Plugin) admitServiceAccount(ctx context.Context, nodeName string, a adm return nil } -func (p *Plugin) validateNodeServiceAccountAudience(ctx context.Context, tr *authenticationapi.TokenRequest, pod *v1.Pod) error { +func (p *Plugin) validateNodeServiceAccountAudience(ctx context.Context, tr *authenticationapi.TokenRequest, pod *v1.Pod, a admission.Attributes) error { // ensure all items in tr.Spec.Audiences are present in a volume mount in the pod requestedAudience := "" switch len(tr.Spec.Audiences) { @@ -654,10 +668,33 @@ func (p *Plugin) validateNodeServiceAccountAudience(ctx context.Context, tr *aut if err != nil { return fmt.Errorf("error validating audience %q: %w", requestedAudience, err) } - if !foundAudiencesInPodSpec { - return fmt.Errorf("audience %q not found in pod spec volume", requestedAudience) + if foundAudiencesInPodSpec { + return nil } - return nil + + userInfo := a.GetUserInfo() + attrs := authorizer.AttributesRecord{ + User: userInfo, // this is the user info of the node requesting the token + Verb: "request-serviceaccounts-token-audience", + Namespace: a.GetNamespace(), + APIGroup: "", + APIVersion: "v1", + Resource: requestedAudience, // this gives us the audience for which node is requesting a token for; wildcard will allow all audiences + Name: a.GetName(), // this gives us the service account name for which node is requesting a token for; if not set, default will allow all service accounts + ResourceRequest: true, + } + + authorized, _, err := p.authz.Authorize(ctx, attrs) + // an authorizer like RBAC could encounter evaluation errors and still allow the request, so authorizer decision is checked before error here. + // following the same pattern as withAuthorization (ref: https://github.com/kubernetes/kubernetes/blob/2b025e645975d6d51bf38c008f972c632cf49657/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authorization.go#L71-L91) + if authorized == authorizer.DecisionAllow { + return nil + } + if err != nil { + return fmt.Errorf("audience %q not found in pod spec volume, error authorizing %s to request tokens for this audience: %w", requestedAudience, userInfo.GetName(), err) + } + + return fmt.Errorf("audience %q not found in pod spec volume, %s is not authorized to request tokens for this audience", requestedAudience, userInfo.GetName()) } func (p *Plugin) podReferencesAudience(ctx context.Context, pod *v1.Pod, audience string) (bool, error) { diff --git a/plugin/pkg/admission/noderestriction/admission_test.go b/plugin/pkg/admission/noderestriction/admission_test.go index fa8b194b437..d3b3da0b2c5 100644 --- a/plugin/pkg/admission/noderestriction/admission_test.go +++ b/plugin/pkg/admission/noderestriction/admission_test.go @@ -38,6 +38,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/util/feature" corev1lister "k8s.io/client-go/listers/core/v1" storagelisters "k8s.io/client-go/listers/storage/v1" @@ -110,6 +111,9 @@ func makeTestPodEviction(name string) *policy.Eviction { func makeTokenRequest(podname string, poduid types.UID, audiences []string) *authenticationapi.TokenRequest { tr := &authenticationapi.TokenRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns", + }, Spec: authenticationapi.TokenRequestSpec{ Audiences: audiences, }, @@ -225,6 +229,7 @@ type admitTestCase struct { features featuregate.FeatureGate setupFunc func(t *testing.T) err string + authz authorizer.Authorizer } func (a *admitTestCase) run(t *testing.T) { @@ -241,6 +246,7 @@ func (a *admitTestCase) run(t *testing.T) { c.csiDriverGetter = a.csiDriverGetter c.pvcGetter = a.pvcGetter c.pvGetter = a.pvGetter + c.authz = a.authz err := c.Admit(context.TODO(), a.attributes, nil) if (err == nil) != (len(a.err) == 0) { t.Errorf("nodePlugin.Admit() error = %v, expected %v", err, a.err) @@ -1311,7 +1317,14 @@ func Test_nodePlugin_Admit(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ServiceAccountNodeAudienceRestriction, true) }, attributes: admission.NewAttributesRecord(makeTokenRequest(coremypod.Name, coremypod.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), - err: `serviceaccounts "mysa" is forbidden: audience "foo" not found in pod spec volume`, + err: `serviceaccounts "mysa" is forbidden: audience "foo" not found in pod spec volume, system:node:mynode is not authorized to request tokens for this audience`, + authz: fakeAuthorizer{ + t: t, + serviceAccountName: "mysa", + namespace: coremypod.Namespace, + requestAudience: "foo", + decision: authorizer.DecisionDeny, + }, }, { name: "allow create of token when audience in pod --> csi --> driver --> tokenrequest with audience and ServiceAccountNodeAudienceRestriction is enabled", @@ -1334,7 +1347,14 @@ func Test_nodePlugin_Admit(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ServiceAccountNodeAudienceRestriction, true) }, attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodWithCSI.Name, v1mypodWithCSI.UID, []string{"bar"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), - err: `audience "bar" not found in pod spec volume`, + err: `audience "bar" not found in pod spec volume, system:node:mynode is not authorized to request tokens for this audience`, + authz: fakeAuthorizer{ + t: t, + serviceAccountName: "mysa", + namespace: coremypod.Namespace, + requestAudience: "bar", + decision: authorizer.DecisionDeny, + }, }, { name: "forbid create of token when audience in pod --> csi --> driver --> tokenrequest with audience and ServiceAccountNodeAudienceRestriction is enabled, csidriver not found", @@ -1347,6 +1367,7 @@ func Test_nodePlugin_Admit(t *testing.T) { }, attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodWithCSI.Name, v1mypodWithCSI.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), err: `error validating audience "foo": csidriver.storage.k8s.io "com.example.csi.mydriver" not found`, + authz: fakeAuthorizer{}, }, { name: "allow create of token when audience in pod --> pvc --> pv --> csi --> driver --> tokenrequest with audience and ServiceAccountNodeAudienceRestriction is enabled", @@ -1373,7 +1394,14 @@ func Test_nodePlugin_Admit(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ServiceAccountNodeAudienceRestriction, true) }, attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodWithPVCRefCSI.Name, v1mypodWithPVCRefCSI.UID, []string{"bar"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), - err: `audience "bar" not found in pod spec volume`, + err: `audience "bar" not found in pod spec volume, system:node:mynode is not authorized to request tokens for this audience`, + authz: fakeAuthorizer{ + t: t, + serviceAccountName: "mysa", + namespace: coremypod.Namespace, + requestAudience: "bar", + decision: authorizer.DecisionDeny, + }, }, { name: "forbid create of token when audience in pod --> pvc --> pv --> csi --> driver --> tokenrequest with audience and ServiceAccountNodeAudienceRestriction is enabled, pvc not found", @@ -1388,6 +1416,7 @@ func Test_nodePlugin_Admit(t *testing.T) { }, attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodWithPVCRefCSI.Name, v1mypodWithPVCRefCSI.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), err: `error validating audience "foo": persistentvolumeclaim "pvclaim" not found`, + authz: fakeAuthorizer{}, }, { name: "forbid create of token when audience in pod --> pvc --> pv --> csi --> driver --> tokenrequest with audience and ServiceAccountNodeAudienceRestriction is enabled, pv not found", @@ -1402,6 +1431,7 @@ func Test_nodePlugin_Admit(t *testing.T) { }, attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodWithPVCRefCSI.Name, v1mypodWithPVCRefCSI.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), err: `error validating audience "foo": persistentvolume "pvname" not found`, + authz: fakeAuthorizer{}, }, { name: "allow create of token when audience in pod --> ephemeral --> pvc --> pv --> csi --> driver --> tokenrequest with audience and ServiceAccountNodeAudienceRestriction is enabled", @@ -1428,7 +1458,14 @@ func Test_nodePlugin_Admit(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ServiceAccountNodeAudienceRestriction, true) }, attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodWithEphemeralVolume.Name, v1mypodWithEphemeralVolume.UID, []string{"bar"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), - err: `audience "bar" not found in pod spec volume`, + err: `audience "bar" not found in pod spec volume, system:node:mynode is not authorized to request tokens for this audience`, + authz: fakeAuthorizer{ + t: t, + serviceAccountName: "mysa", + namespace: coremypod.Namespace, + requestAudience: "bar", + decision: authorizer.DecisionDeny, + }, }, { name: "allow create of token when ServiceAccountNodeAudienceRestriction is disabled, pvc not found should not be checked", @@ -1503,6 +1540,47 @@ func Test_nodePlugin_Admit(t *testing.T) { }, attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodIntreeInlineVolToCSI.Name, v1mypodIntreeInlineVolToCSI.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), }, + { + name: "allow create of token when ServiceAccountNodeAudienceRestriction is enabled, clusterrole and clusterrolebinding are configured", + podsGetter: existingPods, + csiDriverGetter: noexistingCSIDriverLister, + pvcGetter: noexistingPVCLister, + pvGetter: noexistingPVLister, + features: feature.DefaultFeatureGate, + setupFunc: func(t *testing.T) { + t.Helper() + featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ServiceAccountNodeAudienceRestriction, true) + }, + attributes: admission.NewAttributesRecord(makeTokenRequest(coremypod.Name, v1mypod.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), + authz: fakeAuthorizer{ + t: t, + serviceAccountName: "mysa", + namespace: coremypod.Namespace, + requestAudience: "foo", + decision: authorizer.DecisionAllow, + }, + }, + { + name: "forbid create of token when ServiceAccountNodeAudienceRestriction is enabled, clusterrole and clusterrolebinding for audience not configured", + podsGetter: existingPods, + csiDriverGetter: noexistingCSIDriverLister, + pvcGetter: noexistingPVCLister, + pvGetter: noexistingPVLister, + features: feature.DefaultFeatureGate, + setupFunc: func(t *testing.T) { + t.Helper() + featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ServiceAccountNodeAudienceRestriction, true) + }, + attributes: admission.NewAttributesRecord(makeTokenRequest(coremypod.Name, v1mypod.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), + authz: fakeAuthorizer{ + t: t, + serviceAccountName: "mysa", + namespace: coremypod.Namespace, + requestAudience: "foo", + decision: authorizer.DecisionDeny, + }, + err: `serviceaccounts "mysa" is forbidden: audience "foo" not found in pod spec volume, system:node:mynode is not authorized to request tokens for this audience`, + }, // Unrelated objects { @@ -2234,3 +2312,35 @@ func checkNilError(t *testing.T, err error) { t.Fatalf("unexpected error: %v", err) } } + +type fakeAuthorizer struct { + t *testing.T + serviceAccountName string + namespace string + requestAudience string + decision authorizer.Decision + err error +} + +func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + if f.err != nil { + return f.decision, "forced error", f.err + } + + expectedAttrs := authorizer.AttributesRecord{ + User: &user.DefaultInfo{Name: "system:node:mynode", Groups: []string{"system:nodes"}}, + Verb: "request-serviceaccounts-token-audience", + Namespace: f.namespace, + APIGroup: "", + APIVersion: "v1", + Resource: f.requestAudience, + Name: f.serviceAccountName, + ResourceRequest: true, + } + + if !reflect.DeepEqual(a, expectedAttrs) { + f.t.Errorf("expected attributes: %v, got: %v", expectedAttrs, a) + } + + return f.decision, "", nil +}