Skip to content

Commit 28900b8

Browse files
authored
Merge pull request kubernetes#128077 from aramase/aramase/f/kep_4412_sa_node_aud_restriction
Enforce service account node audience restriction
2 parents 9ba42a5 + e93d5d5 commit 28900b8

File tree

7 files changed

+939
-35
lines changed

7 files changed

+939
-35
lines changed

pkg/features/kube_features.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,13 @@ const (
585585
// Decouples Taint Eviction Controller, performing taint-based Pod eviction, from Node Lifecycle Controller.
586586
SeparateTaintEvictionController featuregate.Feature = "SeparateTaintEvictionController"
587587

588+
// owner: @aramase
589+
// kep: https://kep.k8s.io/4412
590+
//
591+
// ServiceAccountNodeAudienceRestriction is used to restrict the audience for which the
592+
// kubelet can request a service account token for.
593+
ServiceAccountNodeAudienceRestriction featuregate.Feature = "ServiceAccountNodeAudienceRestriction"
594+
588595
// owner: @munnerz
589596
// kep: http://kep.k8s.io/4193
590597
//

pkg/features/versioned_kube_features.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
662662
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta},
663663
},
664664

665+
ServiceAccountNodeAudienceRestriction: {
666+
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.Beta},
667+
},
668+
665669
ServiceAccountTokenJTI: {
666670
{Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha},
667671
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta},

plugin/pkg/admission/noderestriction/admission.go

Lines changed: 135 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,21 @@ import (
2424
"strings"
2525

2626
"github.com/google/go-cmp/cmp"
27+
2728
v1 "k8s.io/api/core/v1"
2829
apiequality "k8s.io/apimachinery/pkg/api/equality"
2930
apierrors "k8s.io/apimachinery/pkg/api/errors"
3031
"k8s.io/apimachinery/pkg/api/meta"
3132
"k8s.io/apimachinery/pkg/labels"
33+
utilerrors "k8s.io/apimachinery/pkg/util/errors"
3234
"k8s.io/apimachinery/pkg/util/sets"
3335
"k8s.io/apiserver/pkg/admission"
3436
apiserveradmission "k8s.io/apiserver/pkg/admission/initializer"
3537
"k8s.io/client-go/informers"
3638
corev1lister "k8s.io/client-go/listers/core/v1"
39+
storagelisters "k8s.io/client-go/listers/storage/v1"
3740
"k8s.io/component-base/featuregate"
41+
"k8s.io/component-helpers/storage/ephemeral"
3842
kubeletapis "k8s.io/kubelet/pkg/apis"
3943
podutil "k8s.io/kubernetes/pkg/api/pod"
4044
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
@@ -43,7 +47,7 @@ import (
4347
api "k8s.io/kubernetes/pkg/apis/core"
4448
"k8s.io/kubernetes/pkg/apis/policy"
4549
"k8s.io/kubernetes/pkg/apis/resource"
46-
storage "k8s.io/kubernetes/pkg/apis/storage"
50+
"k8s.io/kubernetes/pkg/apis/storage"
4751
"k8s.io/kubernetes/pkg/auth/nodeidentifier"
4852
"k8s.io/kubernetes/pkg/features"
4953
)
@@ -70,13 +74,17 @@ func NewPlugin(nodeIdentifier nodeidentifier.NodeIdentifier) *Plugin {
7074
// Plugin holds state for and implements the admission plugin.
7175
type Plugin struct {
7276
*admission.Handler
73-
nodeIdentifier nodeidentifier.NodeIdentifier
74-
podsGetter corev1lister.PodLister
75-
nodesGetter corev1lister.NodeLister
77+
nodeIdentifier nodeidentifier.NodeIdentifier
78+
podsGetter corev1lister.PodLister
79+
nodesGetter corev1lister.NodeLister
80+
csiDriverGetter storagelisters.CSIDriverLister
81+
pvcGetter corev1lister.PersistentVolumeClaimLister
82+
pvGetter corev1lister.PersistentVolumeLister
7683

7784
expansionRecoveryEnabled bool
7885
dynamicResourceAllocationEnabled bool
7986
allowInsecureKubeletCertificateSigningRequests bool
87+
serviceAccountNodeAudienceRestriction bool
8088
}
8189

8290
var (
@@ -90,12 +98,18 @@ func (p *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
9098
p.expansionRecoveryEnabled = featureGates.Enabled(features.RecoverVolumeExpansionFailure)
9199
p.dynamicResourceAllocationEnabled = featureGates.Enabled(features.DynamicResourceAllocation)
92100
p.allowInsecureKubeletCertificateSigningRequests = featureGates.Enabled(features.AllowInsecureKubeletCertificateSigningRequests)
101+
p.serviceAccountNodeAudienceRestriction = featureGates.Enabled(features.ServiceAccountNodeAudienceRestriction)
93102
}
94103

95104
// SetExternalKubeInformerFactory registers an informer factory into Plugin
96105
func (p *Plugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
97106
p.podsGetter = f.Core().V1().Pods().Lister()
98107
p.nodesGetter = f.Core().V1().Nodes().Lister()
108+
if p.serviceAccountNodeAudienceRestriction {
109+
p.csiDriverGetter = f.Storage().V1().CSIDrivers().Lister()
110+
p.pvcGetter = f.Core().V1().PersistentVolumeClaims().Lister()
111+
p.pvGetter = f.Core().V1().PersistentVolumes().Lister()
112+
}
99113
}
100114

101115
// ValidateInitialization validates the Plugin was initialized properly
@@ -109,6 +123,17 @@ func (p *Plugin) ValidateInitialization() error {
109123
if p.nodesGetter == nil {
110124
return fmt.Errorf("%s requires a node getter", PluginName)
111125
}
126+
if p.serviceAccountNodeAudienceRestriction {
127+
if p.csiDriverGetter == nil {
128+
return fmt.Errorf("%s requires a CSI driver getter", PluginName)
129+
}
130+
if p.pvcGetter == nil {
131+
return fmt.Errorf("%s requires a PVC getter", PluginName)
132+
}
133+
if p.pvGetter == nil {
134+
return fmt.Errorf("%s requires a PV getter", PluginName)
135+
}
136+
}
112137
return nil
113138
}
114139

@@ -594,6 +619,12 @@ func (p *Plugin) admitServiceAccount(nodeName string, a admission.Attributes) er
594619
return admission.NewForbidden(a, fmt.Errorf("node requested token bound to a pod scheduled on a different node"))
595620
}
596621

622+
if p.serviceAccountNodeAudienceRestriction {
623+
if err := p.validateNodeServiceAccountAudience(tr, pod); err != nil {
624+
return admission.NewForbidden(a, err)
625+
}
626+
}
627+
597628
// Note: A token may only be bound to one object at a time. By requiring
598629
// the Pod binding, noderestriction eliminates the opportunity to spoof
599630
// a Node binding. Instead, kube-apiserver automatically infers and sets
@@ -603,6 +634,106 @@ func (p *Plugin) admitServiceAccount(nodeName string, a admission.Attributes) er
603634
return nil
604635
}
605636

637+
func (p *Plugin) validateNodeServiceAccountAudience(tr *authenticationapi.TokenRequest, pod *v1.Pod) error {
638+
// ensure all items in tr.Spec.Audiences are present in a volume mount in the pod
639+
requestedAudience := ""
640+
switch len(tr.Spec.Audiences) {
641+
case 0:
642+
requestedAudience = ""
643+
case 1:
644+
requestedAudience = tr.Spec.Audiences[0]
645+
default:
646+
return fmt.Errorf("node may only request 0 or 1 audiences")
647+
}
648+
649+
foundAudiencesInPodSpec, err := p.podReferencesAudience(pod, requestedAudience)
650+
if err != nil {
651+
return fmt.Errorf("error validating audience %q: %w", requestedAudience, err)
652+
}
653+
if !foundAudiencesInPodSpec {
654+
return fmt.Errorf("audience %q not found in pod spec volume", requestedAudience)
655+
}
656+
return nil
657+
}
658+
659+
func (p *Plugin) podReferencesAudience(pod *v1.Pod, audience string) (bool, error) {
660+
var errs []error
661+
662+
for _, v := range pod.Spec.Volumes {
663+
if v.Projected != nil {
664+
for _, src := range v.Projected.Sources {
665+
if src.ServiceAccountToken != nil && src.ServiceAccountToken.Audience == audience {
666+
return true, nil
667+
}
668+
}
669+
}
670+
671+
// also allow audiences for CSI token requests
672+
// - pod --> ephemeral --> pvc --> pv --> csi --> driver --> tokenrequest with audience
673+
// - pod --> pvc --> pv --> csi --> driver --> tokenrequest with audience
674+
// - pod --> csi --> driver --> tokenrequest with audience
675+
var driverName string
676+
var err error
677+
switch {
678+
case v.Ephemeral != nil && v.Ephemeral.VolumeClaimTemplate != nil:
679+
pvcName := ephemeral.VolumeClaimName(pod, &v)
680+
driverName, err = p.getCSIFromPVC(pod.Namespace, pvcName)
681+
case v.PersistentVolumeClaim != nil:
682+
driverName, err = p.getCSIFromPVC(pod.Namespace, v.PersistentVolumeClaim.ClaimName)
683+
case v.CSI != nil:
684+
driverName = v.CSI.Driver
685+
}
686+
687+
if err != nil {
688+
errs = append(errs, err)
689+
continue
690+
}
691+
692+
if len(driverName) > 0 {
693+
hasAudience, hasAudienceErr := p.csiDriverHasAudience(driverName, audience)
694+
if hasAudienceErr != nil {
695+
errs = append(errs, hasAudienceErr)
696+
continue
697+
}
698+
if hasAudience {
699+
return true, nil
700+
}
701+
}
702+
}
703+
704+
return false, utilerrors.NewAggregate(errs)
705+
}
706+
707+
// getCSIFromPVC returns the CSI driver name from the PVC->PV->CSI->Driver chain
708+
func (p *Plugin) getCSIFromPVC(namespace, claimName string) (string, error) {
709+
pvc, err := p.pvcGetter.PersistentVolumeClaims(namespace).Get(claimName)
710+
if err != nil {
711+
return "", err
712+
}
713+
pv, err := p.pvGetter.Get(pvc.Spec.VolumeName)
714+
if err != nil {
715+
return "", err
716+
}
717+
if pv.Spec.CSI != nil {
718+
return pv.Spec.CSI.Driver, nil
719+
}
720+
return "", nil
721+
}
722+
723+
func (p *Plugin) csiDriverHasAudience(driverName, audience string) (bool, error) {
724+
driver, err := p.csiDriverGetter.Get(driverName)
725+
if err != nil {
726+
return false, err
727+
}
728+
729+
for _, tokenRequest := range driver.Spec.TokenRequests {
730+
if tokenRequest.Audience == audience {
731+
return true, nil
732+
}
733+
}
734+
return false, nil
735+
}
736+
606737
func (p *Plugin) admitLease(nodeName string, a admission.Attributes) error {
607738
// the request must be against the system namespace reserved for node leases
608739
if a.GetNamespace() != api.NamespaceNodeLease {

0 commit comments

Comments
 (0)