Use SAR to allow dynamic audiences for node audience restriction

Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com>
This commit is contained in:
Anish Ramasekar 2025-01-10 11:24:30 -06:00
parent 6c445ca18a
commit b09ca8c2c8
No known key found for this signature in database
GPG Key ID: E96F745A34A409C2
2 changed files with 156 additions and 9 deletions

View File

@ -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) {

View File

@ -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
}