Skip to content

Commit f55f278

Browse files
authored
Merge pull request kubernetes#116254 from pohly/dra-node-authorizer
node authorizer: limit kubelet access to ResourceClaim objects
2 parents 6264fd9 + 39207da commit f55f278

File tree

6 files changed

+232
-24
lines changed

6 files changed

+232
-24
lines changed

plugin/pkg/admission/noderestriction/admission.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,13 +288,39 @@ func (p *Plugin) admitPodStatus(nodeName string, a admission.Attributes) error {
288288
if !labels.Equals(oldPod.Labels, newPod.Labels) {
289289
return admission.NewForbidden(a, fmt.Errorf("node %q cannot update labels through pod status", nodeName))
290290
}
291+
if !resourceClaimStatusesEqual(oldPod.Status.ResourceClaimStatuses, newPod.Status.ResourceClaimStatuses) {
292+
return admission.NewForbidden(a, fmt.Errorf("node %q cannot update resource claim statues", nodeName))
293+
}
291294
return nil
292295

293296
default:
294297
return admission.NewForbidden(a, fmt.Errorf("unexpected operation %q", a.GetOperation()))
295298
}
296299
}
297300

301+
func resourceClaimStatusesEqual(statusA, statusB []api.PodResourceClaimStatus) bool {
302+
if len(statusA) != len(statusB) {
303+
return false
304+
}
305+
// In most cases, status entries only get added once and not modified.
306+
// But this cannot be guaranteed, so for the sake of correctness in all
307+
// cases this code here has to check.
308+
for i := range statusA {
309+
if statusA[i].Name != statusB[i].Name {
310+
return false
311+
}
312+
claimNameA := statusA[i].ResourceClaimName
313+
claimNameB := statusB[i].ResourceClaimName
314+
if (claimNameA == nil) != (claimNameB == nil) {
315+
return false
316+
}
317+
if claimNameA != nil && *claimNameA != *claimNameB {
318+
return false
319+
}
320+
}
321+
return true
322+
}
323+
298324
// admitPodEviction allows to evict a pod if it is assigned to the current node.
299325
func (p *Plugin) admitPodEviction(nodeName string, a admission.Attributes) error {
300326
switch a.GetOperation() {

plugin/pkg/auth/authorizer/node/graph.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
corev1 "k8s.io/api/core/v1"
2424
"k8s.io/component-helpers/storage/ephemeral"
25+
"k8s.io/dynamic-resource-allocation/resourceclaim"
2526
pvutil "k8s.io/kubernetes/pkg/api/v1/persistentvolume"
2627
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
2728
"k8s.io/kubernetes/third_party/forked/gonum/graph"
@@ -117,6 +118,7 @@ const (
117118
podVertexType
118119
pvcVertexType
119120
pvVertexType
121+
resourceClaimVertexType
120122
secretVertexType
121123
vaVertexType
122124
serviceAccountVertexType
@@ -128,6 +130,7 @@ var vertexTypes = map[vertexType]string{
128130
podVertexType: "pod",
129131
pvcVertexType: "pvc",
130132
pvVertexType: "pv",
133+
resourceClaimVertexType: "resourceclaim",
131134
secretVertexType: "secret",
132135
vaVertexType: "volumeattachment",
133136
serviceAccountVertexType: "serviceAccount",
@@ -393,6 +396,20 @@ func (g *Graph) AddPod(pod *corev1.Pod) {
393396
g.addEdgeToDestinationIndex_locked(e)
394397
}
395398
}
399+
400+
for _, podResourceClaim := range pod.Spec.ResourceClaims {
401+
claimName, _, err := resourceclaim.Name(pod, &podResourceClaim)
402+
// Do we have a valid claim name? If yes, add an edge that grants
403+
// kubelet access to that claim. An error indicates that a claim
404+
// still needs to be created, nil that intentionally no claim
405+
// was created and never will be because it isn't needed.
406+
if err == nil && claimName != nil {
407+
claimVertex := g.getOrCreateVertex_locked(resourceClaimVertexType, pod.Namespace, *claimName)
408+
e := newDestinationEdge(claimVertex, podVertex, nodeVertex)
409+
g.graph.SetEdge(e)
410+
g.addEdgeToDestinationIndex_locked(e)
411+
}
412+
}
396413
}
397414
func (g *Graph) DeletePod(name, namespace string) {
398415
start := time.Now()

plugin/pkg/auth/authorizer/node/graph_populator.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ func (g *graphPopulator) updatePod(oldObj, obj interface{}) {
7878
return
7979
}
8080
if oldPod, ok := oldObj.(*corev1.Pod); ok && oldPod != nil {
81-
if (pod.Spec.NodeName == oldPod.Spec.NodeName) && (pod.UID == oldPod.UID) {
82-
// Node and uid are unchanged, all object references in the pod spec are immutable
81+
if (pod.Spec.NodeName == oldPod.Spec.NodeName) && (pod.UID == oldPod.UID) &&
82+
resourceClaimStatusesEqual(oldPod.Status.ResourceClaimStatuses, pod.Status.ResourceClaimStatuses) {
83+
// Node and uid are unchanged, all object references in the pod spec are immutable respectively unmodified (claim statuses).
8384
klog.V(5).Infof("updatePod %s/%s, node unchanged", pod.Namespace, pod.Name)
8485
return
8586
}
@@ -91,6 +92,29 @@ func (g *graphPopulator) updatePod(oldObj, obj interface{}) {
9192
klog.V(5).Infof("updatePod %s/%s for node %s completed in %v", pod.Namespace, pod.Name, pod.Spec.NodeName, time.Since(startTime))
9293
}
9394

95+
func resourceClaimStatusesEqual(statusA, statusB []corev1.PodResourceClaimStatus) bool {
96+
if len(statusA) != len(statusB) {
97+
return false
98+
}
99+
// In most cases, status entries only get added once and not modified.
100+
// But this cannot be guaranteed, so for the sake of correctness in all
101+
// cases this code here has to check.
102+
for i := range statusA {
103+
if statusA[i].Name != statusB[i].Name {
104+
return false
105+
}
106+
claimNameA := statusA[i].ResourceClaimName
107+
claimNameB := statusB[i].ResourceClaimName
108+
if (claimNameA == nil) != (claimNameB == nil) {
109+
return false
110+
}
111+
if claimNameA != nil && *claimNameA != *claimNameB {
112+
return false
113+
}
114+
}
115+
return true
116+
}
117+
94118
func (g *graphPopulator) deletePod(obj interface{}) {
95119
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
96120
obj = tombstone.Obj

plugin/pkg/auth/authorizer/node/node_authorizer.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"k8s.io/component-base/featuregate"
3131
coordapi "k8s.io/kubernetes/pkg/apis/coordination"
3232
api "k8s.io/kubernetes/pkg/apis/core"
33+
resourceapi "k8s.io/kubernetes/pkg/apis/resource"
3334
storageapi "k8s.io/kubernetes/pkg/apis/storage"
3435
"k8s.io/kubernetes/pkg/auth/nodeidentifier"
3536
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"
@@ -40,14 +41,15 @@ import (
4041
// NodeAuthorizer authorizes requests from kubelets, with the following logic:
4142
// 1. If a request is not from a node (NodeIdentity() returns isNode=false), reject
4243
// 2. If a specific node cannot be identified (NodeIdentity() returns nodeName=""), reject
43-
// 3. If a request is for a secret, configmap, persistent volume or persistent volume claim, reject unless the verb is get, and the requested object is related to the requesting node:
44+
// 3. If a request is for a secret, configmap, persistent volume, resource claim, or persistent volume claim, reject unless the verb is get, and the requested object is related to the requesting node:
4445
// node <- configmap
4546
// node <- pod
4647
// node <- pod <- secret
4748
// node <- pod <- configmap
4849
// node <- pod <- pvc
4950
// node <- pod <- pvc <- pv
5051
// node <- pod <- pvc <- pv <- secret
52+
// node <- pod <- ResourceClaim
5153
// 4. For other resources, authorize all nodes uniformly using statically defined rules
5254
type NodeAuthorizer struct {
5355
graph *Graph
@@ -72,14 +74,15 @@ func NewAuthorizer(graph *Graph, identifier nodeidentifier.NodeIdentifier, rules
7274
}
7375

7476
var (
75-
configMapResource = api.Resource("configmaps")
76-
secretResource = api.Resource("secrets")
77-
pvcResource = api.Resource("persistentvolumeclaims")
78-
pvResource = api.Resource("persistentvolumes")
79-
vaResource = storageapi.Resource("volumeattachments")
80-
svcAcctResource = api.Resource("serviceaccounts")
81-
leaseResource = coordapi.Resource("leases")
82-
csiNodeResource = storageapi.Resource("csinodes")
77+
configMapResource = api.Resource("configmaps")
78+
secretResource = api.Resource("secrets")
79+
pvcResource = api.Resource("persistentvolumeclaims")
80+
pvResource = api.Resource("persistentvolumes")
81+
resourceClaimResource = resourceapi.Resource("resourceclaims")
82+
vaResource = storageapi.Resource("volumeattachments")
83+
svcAcctResource = api.Resource("serviceaccounts")
84+
leaseResource = coordapi.Resource("leases")
85+
csiNodeResource = storageapi.Resource("csinodes")
8386
)
8487

8588
func (r *NodeAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
@@ -117,6 +120,8 @@ func (r *NodeAuthorizer) Authorize(ctx context.Context, attrs authorizer.Attribu
117120
return r.authorizeGet(nodeName, pvcVertexType, attrs)
118121
case pvResource:
119122
return r.authorizeGet(nodeName, pvVertexType, attrs)
123+
case resourceClaimResource:
124+
return r.authorizeGet(nodeName, resourceClaimVertexType, attrs)
120125
case vaResource:
121126
return r.authorizeGet(nodeName, vaVertexType, attrs)
122127
case svcAcctResource:

plugin/pkg/auth/authorizer/node/node_authorizer_test.go

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,19 @@ func TestAuthorizer(t *testing.T) {
4242
g := NewGraph()
4343

4444
opts := &sampleDataOpts{
45-
nodes: 2,
46-
namespaces: 2,
47-
podsPerNode: 2,
48-
attachmentsPerNode: 1,
49-
sharedConfigMapsPerPod: 0,
50-
uniqueConfigMapsPerPod: 1,
51-
sharedSecretsPerPod: 1,
52-
uniqueSecretsPerPod: 1,
53-
sharedPVCsPerPod: 0,
54-
uniquePVCsPerPod: 1,
45+
nodes: 2,
46+
namespaces: 2,
47+
podsPerNode: 2,
48+
attachmentsPerNode: 1,
49+
sharedConfigMapsPerPod: 0,
50+
uniqueConfigMapsPerPod: 1,
51+
sharedSecretsPerPod: 1,
52+
uniqueSecretsPerPod: 1,
53+
sharedPVCsPerPod: 0,
54+
uniquePVCsPerPod: 1,
55+
uniqueResourceClaimsPerPod: 1,
56+
uniqueResourceClaimTemplatesPerPod: 1,
57+
uniqueResourceClaimTemplatesWithClaimPerPod: 1,
5558
}
5659
nodes, pods, pvs, attachments := generate(opts)
5760
populate(g, nodes, pods, pvs, attachments)
@@ -117,6 +120,16 @@ func TestAuthorizer(t *testing.T) {
117120
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumeclaims", Name: "pvc0-pod0-node0", Namespace: "ns0"},
118121
expect: authorizer.DecisionAllow,
119122
},
123+
{
124+
name: "allowed resource claim",
125+
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceclaims", APIGroup: "resource.k8s.io", Name: "claim0-pod0-node0-ns0", Namespace: "ns0"},
126+
expect: authorizer.DecisionAllow,
127+
},
128+
{
129+
name: "allowed resource claim with template",
130+
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceclaims", APIGroup: "resource.k8s.io", Name: "generated-claim-pod0-node0-ns0-0", Namespace: "ns0"},
131+
expect: authorizer.DecisionAllow,
132+
},
120133
{
121134
name: "allowed pv",
122135
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumes", Name: "pv0-pod0-node0-ns0", Namespace: ""},
@@ -142,6 +155,16 @@ func TestAuthorizer(t *testing.T) {
142155
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumeclaims", Name: "pvc0-pod0-node1", Namespace: "ns0"},
143156
expect: authorizer.DecisionNoOpinion,
144157
},
158+
{
159+
name: "disallowed resource claim, other node",
160+
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceclaims", APIGroup: "resource.k8s.io", Name: "claim0-pod0-node1-ns0", Namespace: "ns0"},
161+
expect: authorizer.DecisionNoOpinion,
162+
},
163+
{
164+
name: "disallowed resource claim with template",
165+
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceclaims", APIGroup: "resource.k8s.io", Name: "pod0-node1-claimtemplate0", Namespace: "ns0"},
166+
expect: authorizer.DecisionNoOpinion,
167+
},
145168
{
146169
name: "disallowed pv",
147170
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumes", Name: "pv0-pod0-node1-ns0", Namespace: ""},
@@ -468,9 +491,12 @@ type sampleDataOpts struct {
468491
sharedSecretsPerPod int
469492
sharedPVCsPerPod int
470493

471-
uniqueSecretsPerPod int
472-
uniqueConfigMapsPerPod int
473-
uniquePVCsPerPod int
494+
uniqueSecretsPerPod int
495+
uniqueConfigMapsPerPod int
496+
uniquePVCsPerPod int
497+
uniqueResourceClaimsPerPod int
498+
uniqueResourceClaimTemplatesPerPod int
499+
uniqueResourceClaimTemplatesWithClaimPerPod int
474500
}
475501

476502
func BenchmarkPopulationAllocation(b *testing.B) {
@@ -845,6 +871,40 @@ func generatePod(name, namespace, nodeName, svcAccountName string, opts *sampleD
845871
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: pv.Spec.ClaimRef.Name},
846872
}})
847873
}
874+
for i := 0; i < opts.uniqueResourceClaimsPerPod; i++ {
875+
claimName := fmt.Sprintf("claim%d-%s-%s", i, pod.Name, pod.Namespace)
876+
pod.Spec.ResourceClaims = append(pod.Spec.ResourceClaims, corev1.PodResourceClaim{
877+
Name: fmt.Sprintf("claim%d", i),
878+
Source: corev1.ClaimSource{
879+
ResourceClaimName: &claimName,
880+
},
881+
})
882+
}
883+
for i := 0; i < opts.uniqueResourceClaimTemplatesPerPod; i++ {
884+
claimTemplateName := fmt.Sprintf("claimtemplate%d-%s-%s", i, pod.Name, pod.Namespace)
885+
podClaimName := fmt.Sprintf("claimtemplate%d", i)
886+
pod.Spec.ResourceClaims = append(pod.Spec.ResourceClaims, corev1.PodResourceClaim{
887+
Name: podClaimName,
888+
Source: corev1.ClaimSource{
889+
ResourceClaimTemplateName: &claimTemplateName,
890+
},
891+
})
892+
}
893+
for i := 0; i < opts.uniqueResourceClaimTemplatesWithClaimPerPod; i++ {
894+
claimTemplateName := fmt.Sprintf("claimtemplate%d-%s-%s", i, pod.Name, pod.Namespace)
895+
podClaimName := fmt.Sprintf("claimtemplate-with-claim%d", i)
896+
claimName := fmt.Sprintf("generated-claim-%s-%s-%d", pod.Name, pod.Namespace, i)
897+
pod.Spec.ResourceClaims = append(pod.Spec.ResourceClaims, corev1.PodResourceClaim{
898+
Name: podClaimName,
899+
Source: corev1.ClaimSource{
900+
ResourceClaimTemplateName: &claimTemplateName,
901+
},
902+
})
903+
pod.Status.ResourceClaimStatuses = append(pod.Status.ResourceClaimStatuses, corev1.PodResourceClaimStatus{
904+
Name: podClaimName,
905+
ResourceClaimName: &claimName,
906+
})
907+
}
848908
// Choose shared pvcs randomly from shared pvcs in a namespace.
849909
subset = randomSubset(opts.sharedPVCsPerPod, opts.sharedPVCsPerNamespace)
850910
for _, i := range subset {

0 commit comments

Comments
 (0)