Skip to content

Commit 6216201

Browse files
authored
Merge pull request kubernetes#130485 from aramase/aramase/f/using_sar_for_node_aud_restriction
Enable dynamic configuration of service account names and audiences for token requests in node audience restriction
2 parents 2effa5e + 3f5d305 commit 6216201

File tree

3 files changed

+588
-41
lines changed

3 files changed

+588
-41
lines changed

plugin/pkg/admission/noderestriction/admission.go

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"k8s.io/apimachinery/pkg/util/sets"
3535
"k8s.io/apiserver/pkg/admission"
3636
apiserveradmission "k8s.io/apiserver/pkg/admission/initializer"
37+
"k8s.io/apiserver/pkg/authorization/authorizer"
3738
"k8s.io/client-go/informers"
3839
corev1lister "k8s.io/client-go/listers/core/v1"
3940
storagelisters "k8s.io/client-go/listers/storage/v1"
@@ -84,6 +85,8 @@ type Plugin struct {
8485
pvGetter corev1lister.PersistentVolumeLister
8586
csiTranslator csitrans.CSITranslator
8687

88+
authz authorizer.Authorizer
89+
8790
expansionRecoveryEnabled bool
8891
dynamicResourceAllocationEnabled bool
8992
allowInsecureKubeletCertificateSigningRequests bool
@@ -94,6 +97,7 @@ var (
9497
_ admission.Interface = &Plugin{}
9598
_ apiserveradmission.WantsExternalKubeInformerFactory = &Plugin{}
9699
_ apiserveradmission.WantsFeatures = &Plugin{}
100+
_ apiserveradmission.WantsAuthorizer = &Plugin{}
97101
)
98102

99103
// InspectFeatureGates allows setting bools without taking a dep on a global variable
@@ -137,10 +141,20 @@ func (p *Plugin) ValidateInitialization() error {
137141
if p.pvGetter == nil {
138142
return fmt.Errorf("%s requires a PV getter", PluginName)
139143
}
144+
if p.authz == nil {
145+
return fmt.Errorf("%s requires an authorizer", PluginName)
146+
}
140147
}
141148
return nil
142149
}
143150

151+
// SetAuthorizer sets the authorizer.
152+
func (p *Plugin) SetAuthorizer(authz authorizer.Authorizer) {
153+
if p.serviceAccountNodeAudienceRestriction {
154+
p.authz = authz
155+
}
156+
}
157+
144158
var (
145159
podResource = api.Resource("pods")
146160
nodeResource = api.Resource("nodes")
@@ -624,7 +638,7 @@ func (p *Plugin) admitServiceAccount(ctx context.Context, nodeName string, a adm
624638
}
625639

626640
if p.serviceAccountNodeAudienceRestriction {
627-
if err := p.validateNodeServiceAccountAudience(ctx, tr, pod); err != nil {
641+
if err := p.validateNodeServiceAccountAudience(ctx, tr, pod, a); err != nil {
628642
return admission.NewForbidden(a, err)
629643
}
630644
}
@@ -638,7 +652,7 @@ func (p *Plugin) admitServiceAccount(ctx context.Context, nodeName string, a adm
638652
return nil
639653
}
640654

641-
func (p *Plugin) validateNodeServiceAccountAudience(ctx context.Context, tr *authenticationapi.TokenRequest, pod *v1.Pod) error {
655+
func (p *Plugin) validateNodeServiceAccountAudience(ctx context.Context, tr *authenticationapi.TokenRequest, pod *v1.Pod, a admission.Attributes) error {
642656
// ensure all items in tr.Spec.Audiences are present in a volume mount in the pod
643657
requestedAudience := ""
644658
switch len(tr.Spec.Audiences) {
@@ -654,10 +668,33 @@ func (p *Plugin) validateNodeServiceAccountAudience(ctx context.Context, tr *aut
654668
if err != nil {
655669
return fmt.Errorf("error validating audience %q: %w", requestedAudience, err)
656670
}
657-
if !foundAudiencesInPodSpec {
658-
return fmt.Errorf("audience %q not found in pod spec volume", requestedAudience)
671+
if foundAudiencesInPodSpec {
672+
return nil
659673
}
660-
return nil
674+
675+
userInfo := a.GetUserInfo()
676+
attrs := authorizer.AttributesRecord{
677+
User: userInfo, // this is the user info of the node requesting the token
678+
Verb: "request-serviceaccounts-token-audience",
679+
Namespace: a.GetNamespace(),
680+
APIGroup: "",
681+
APIVersion: "v1",
682+
Resource: requestedAudience, // this gives us the audience for which node is requesting a token for; wildcard will allow all audiences
683+
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
684+
ResourceRequest: true,
685+
}
686+
687+
authorized, _, err := p.authz.Authorize(ctx, attrs)
688+
// an authorizer like RBAC could encounter evaluation errors and still allow the request, so authorizer decision is checked before error here.
689+
// 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)
690+
if authorized == authorizer.DecisionAllow {
691+
return nil
692+
}
693+
if err != nil {
694+
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)
695+
}
696+
697+
return fmt.Errorf("audience %q not found in pod spec volume, %s is not authorized to request tokens for this audience", requestedAudience, userInfo.GetName())
661698
}
662699

663700
func (p *Plugin) podReferencesAudience(ctx context.Context, pod *v1.Pod, audience string) (bool, error) {

plugin/pkg/admission/noderestriction/admission_test.go

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
"k8s.io/apimachinery/pkg/util/sets"
3939
"k8s.io/apiserver/pkg/admission"
4040
"k8s.io/apiserver/pkg/authentication/user"
41+
"k8s.io/apiserver/pkg/authorization/authorizer"
4142
"k8s.io/apiserver/pkg/util/feature"
4243
corev1lister "k8s.io/client-go/listers/core/v1"
4344
storagelisters "k8s.io/client-go/listers/storage/v1"
@@ -110,6 +111,9 @@ func makeTestPodEviction(name string) *policy.Eviction {
110111

111112
func makeTokenRequest(podname string, poduid types.UID, audiences []string) *authenticationapi.TokenRequest {
112113
tr := &authenticationapi.TokenRequest{
114+
ObjectMeta: metav1.ObjectMeta{
115+
Namespace: "ns",
116+
},
113117
Spec: authenticationapi.TokenRequestSpec{
114118
Audiences: audiences,
115119
},
@@ -225,6 +229,7 @@ type admitTestCase struct {
225229
features featuregate.FeatureGate
226230
setupFunc func(t *testing.T)
227231
err string
232+
authz authorizer.Authorizer
228233
}
229234

230235
func (a *admitTestCase) run(t *testing.T) {
@@ -241,6 +246,7 @@ func (a *admitTestCase) run(t *testing.T) {
241246
c.csiDriverGetter = a.csiDriverGetter
242247
c.pvcGetter = a.pvcGetter
243248
c.pvGetter = a.pvGetter
249+
c.authz = a.authz
244250
err := c.Admit(context.TODO(), a.attributes, nil)
245251
if (err == nil) != (len(a.err) == 0) {
246252
t.Errorf("nodePlugin.Admit() error = %v, expected %v", err, a.err)
@@ -1311,7 +1317,14 @@ func Test_nodePlugin_Admit(t *testing.T) {
13111317
featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ServiceAccountNodeAudienceRestriction, true)
13121318
},
13131319
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypod.Name, coremypod.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
1314-
err: `serviceaccounts "mysa" is forbidden: audience "foo" not found in pod spec volume`,
1320+
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`,
1321+
authz: fakeAuthorizer{
1322+
t: t,
1323+
serviceAccountName: "mysa",
1324+
namespace: coremypod.Namespace,
1325+
requestAudience: "foo",
1326+
decision: authorizer.DecisionDeny,
1327+
},
13151328
},
13161329
{
13171330
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) {
13341347
featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ServiceAccountNodeAudienceRestriction, true)
13351348
},
13361349
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodWithCSI.Name, v1mypodWithCSI.UID, []string{"bar"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
1337-
err: `audience "bar" not found in pod spec volume`,
1350+
err: `audience "bar" not found in pod spec volume, system:node:mynode is not authorized to request tokens for this audience`,
1351+
authz: fakeAuthorizer{
1352+
t: t,
1353+
serviceAccountName: "mysa",
1354+
namespace: coremypod.Namespace,
1355+
requestAudience: "bar",
1356+
decision: authorizer.DecisionDeny,
1357+
},
13381358
},
13391359
{
13401360
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) {
13471367
},
13481368
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodWithCSI.Name, v1mypodWithCSI.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
13491369
err: `error validating audience "foo": csidriver.storage.k8s.io "com.example.csi.mydriver" not found`,
1370+
authz: fakeAuthorizer{},
13501371
},
13511372
{
13521373
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) {
13731394
featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ServiceAccountNodeAudienceRestriction, true)
13741395
},
13751396
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodWithPVCRefCSI.Name, v1mypodWithPVCRefCSI.UID, []string{"bar"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
1376-
err: `audience "bar" not found in pod spec volume`,
1397+
err: `audience "bar" not found in pod spec volume, system:node:mynode is not authorized to request tokens for this audience`,
1398+
authz: fakeAuthorizer{
1399+
t: t,
1400+
serviceAccountName: "mysa",
1401+
namespace: coremypod.Namespace,
1402+
requestAudience: "bar",
1403+
decision: authorizer.DecisionDeny,
1404+
},
13771405
},
13781406
{
13791407
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) {
13881416
},
13891417
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodWithPVCRefCSI.Name, v1mypodWithPVCRefCSI.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
13901418
err: `error validating audience "foo": persistentvolumeclaim "pvclaim" not found`,
1419+
authz: fakeAuthorizer{},
13911420
},
13921421
{
13931422
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) {
14021431
},
14031432
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodWithPVCRefCSI.Name, v1mypodWithPVCRefCSI.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
14041433
err: `error validating audience "foo": persistentvolume "pvname" not found`,
1434+
authz: fakeAuthorizer{},
14051435
},
14061436
{
14071437
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) {
14281458
featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ServiceAccountNodeAudienceRestriction, true)
14291459
},
14301460
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodWithEphemeralVolume.Name, v1mypodWithEphemeralVolume.UID, []string{"bar"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
1431-
err: `audience "bar" not found in pod spec volume`,
1461+
err: `audience "bar" not found in pod spec volume, system:node:mynode is not authorized to request tokens for this audience`,
1462+
authz: fakeAuthorizer{
1463+
t: t,
1464+
serviceAccountName: "mysa",
1465+
namespace: coremypod.Namespace,
1466+
requestAudience: "bar",
1467+
decision: authorizer.DecisionDeny,
1468+
},
14321469
},
14331470
{
14341471
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) {
15031540
},
15041541
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypodIntreeInlineVolToCSI.Name, v1mypodIntreeInlineVolToCSI.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
15051542
},
1543+
{
1544+
name: "allow create of token when ServiceAccountNodeAudienceRestriction is enabled, clusterrole and clusterrolebinding are configured",
1545+
podsGetter: existingPods,
1546+
csiDriverGetter: noexistingCSIDriverLister,
1547+
pvcGetter: noexistingPVCLister,
1548+
pvGetter: noexistingPVLister,
1549+
features: feature.DefaultFeatureGate,
1550+
setupFunc: func(t *testing.T) {
1551+
t.Helper()
1552+
featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ServiceAccountNodeAudienceRestriction, true)
1553+
},
1554+
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypod.Name, v1mypod.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
1555+
authz: fakeAuthorizer{
1556+
t: t,
1557+
serviceAccountName: "mysa",
1558+
namespace: coremypod.Namespace,
1559+
requestAudience: "foo",
1560+
decision: authorizer.DecisionAllow,
1561+
},
1562+
},
1563+
{
1564+
name: "forbid create of token when ServiceAccountNodeAudienceRestriction is enabled, clusterrole and clusterrolebinding for audience not configured",
1565+
podsGetter: existingPods,
1566+
csiDriverGetter: noexistingCSIDriverLister,
1567+
pvcGetter: noexistingPVCLister,
1568+
pvGetter: noexistingPVLister,
1569+
features: feature.DefaultFeatureGate,
1570+
setupFunc: func(t *testing.T) {
1571+
t.Helper()
1572+
featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ServiceAccountNodeAudienceRestriction, true)
1573+
},
1574+
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypod.Name, v1mypod.UID, []string{"foo"}), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
1575+
authz: fakeAuthorizer{
1576+
t: t,
1577+
serviceAccountName: "mysa",
1578+
namespace: coremypod.Namespace,
1579+
requestAudience: "foo",
1580+
decision: authorizer.DecisionDeny,
1581+
},
1582+
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`,
1583+
},
15061584

15071585
// Unrelated objects
15081586
{
@@ -2234,3 +2312,35 @@ func checkNilError(t *testing.T, err error) {
22342312
t.Fatalf("unexpected error: %v", err)
22352313
}
22362314
}
2315+
2316+
type fakeAuthorizer struct {
2317+
t *testing.T
2318+
serviceAccountName string
2319+
namespace string
2320+
requestAudience string
2321+
decision authorizer.Decision
2322+
err error
2323+
}
2324+
2325+
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
2326+
if f.err != nil {
2327+
return f.decision, "forced error", f.err
2328+
}
2329+
2330+
expectedAttrs := authorizer.AttributesRecord{
2331+
User: &user.DefaultInfo{Name: "system:node:mynode", Groups: []string{"system:nodes"}},
2332+
Verb: "request-serviceaccounts-token-audience",
2333+
Namespace: f.namespace,
2334+
APIGroup: "",
2335+
APIVersion: "v1",
2336+
Resource: f.requestAudience,
2337+
Name: f.serviceAccountName,
2338+
ResourceRequest: true,
2339+
}
2340+
2341+
if !reflect.DeepEqual(a, expectedAttrs) {
2342+
f.t.Errorf("expected attributes: %v, got: %v", expectedAttrs, a)
2343+
}
2344+
2345+
return f.decision, "", nil
2346+
}

0 commit comments

Comments
 (0)