mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 12:15:52 +00:00
Use SAR to allow dynamic audiences for node audience restriction
Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com>
This commit is contained in:
parent
6c445ca18a
commit
b09ca8c2c8
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user