From 2cc75f0a5a90208bf3914c3d1791dcb14723ebda Mon Sep 17 00:00:00 2001 From: Mike Danese Date: Thu, 2 Nov 2017 10:26:04 -0700 Subject: [PATCH] auth: allow nodes to create tokones for svcaccts of pods running on them. --- plugin/pkg/auth/authorizer/node/graph.go | 25 ++++++++---- .../auth/authorizer/node/node_authorizer.go | 31 +++++++++++++++ .../authorizer/node/node_authorizer_test.go | 39 +++++++++++++++++++ 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/plugin/pkg/auth/authorizer/node/graph.go b/plugin/pkg/auth/authorizer/node/graph.go index ec3188371a1..38fea6d0323 100644 --- a/plugin/pkg/auth/authorizer/node/graph.go +++ b/plugin/pkg/auth/authorizer/node/graph.go @@ -107,16 +107,18 @@ const ( pvVertexType secretVertexType vaVertexType + serviceAccountVertexType ) var vertexTypes = map[vertexType]string{ - configMapVertexType: "configmap", - nodeVertexType: "node", - podVertexType: "pod", - pvcVertexType: "pvc", - pvVertexType: "pv", - secretVertexType: "secret", - vaVertexType: "volumeattachment", + configMapVertexType: "configmap", + nodeVertexType: "node", + podVertexType: "pod", + pvcVertexType: "pvc", + pvVertexType: "pv", + secretVertexType: "secret", + vaVertexType: "volumeattachment", + serviceAccountVertexType: "serviceAccount", } // must be called under a write lock @@ -204,6 +206,7 @@ func (g *Graph) deleteVertex_locked(vertexType vertexType, namespace, name strin // secret -> pod // configmap -> pod // pvc -> pod +// svcacct -> pod func (g *Graph) AddPod(pod *api.Pod) { g.lock.Lock() defer g.lock.Unlock() @@ -213,6 +216,14 @@ func (g *Graph) AddPod(pod *api.Pod) { nodeVertex := g.getOrCreateVertex_locked(nodeVertexType, "", pod.Spec.NodeName) g.graph.SetEdge(newDestinationEdge(podVertex, nodeVertex, nodeVertex)) + // TODO(mikedanese): If the pod doesn't mount the service account secrets, + // should the node still get access to the service account? + // + // ref https://github.com/kubernetes/kubernetes/issues/58790 + if len(pod.Spec.ServiceAccountName) > 0 { + g.graph.SetEdge(newDestinationEdge(g.getOrCreateVertex_locked(serviceAccountVertexType, pod.Namespace, pod.Spec.ServiceAccountName), podVertex, nodeVertex)) + } + podutil.VisitPodSecretNames(pod, func(secret string) bool { g.graph.SetEdge(newDestinationEdge(g.getOrCreateVertex_locked(secretVertexType, pod.Namespace, secret), podVertex, nodeVertex)) return true diff --git a/plugin/pkg/auth/authorizer/node/node_authorizer.go b/plugin/pkg/auth/authorizer/node/node_authorizer.go index b283eb0c738..b2078d2c6d9 100644 --- a/plugin/pkg/auth/authorizer/node/node_authorizer.go +++ b/plugin/pkg/auth/authorizer/node/node_authorizer.go @@ -70,6 +70,7 @@ var ( pvcResource = api.Resource("persistentvolumeclaims") pvResource = api.Resource("persistentvolumes") vaResource = storageapi.Resource("volumeattachments") + svcAcctResource = api.Resource("serviceaccounts") ) func (r *NodeAuthorizer) Authorize(attrs authorizer.Attributes) (authorizer.Decision, string, error) { @@ -106,6 +107,11 @@ func (r *NodeAuthorizer) Authorize(attrs authorizer.Attributes) (authorizer.Deci return r.authorizeGet(nodeName, vaVertexType, attrs) } return authorizer.DecisionNoOpinion, fmt.Sprintf("disabled by feature gate %s", features.CSIPersistentVolume), nil + case svcAcctResource: + if r.features.Enabled(features.TokenRequest) { + return r.authorizeCreateToken(nodeName, serviceAccountVertexType, attrs) + } + return authorizer.DecisionNoOpinion, fmt.Sprintf("disabled by feature gate %s", features.TokenRequest), nil } } @@ -165,6 +171,31 @@ func (r *NodeAuthorizer) authorize(nodeName string, startingType vertexType, att return authorizer.DecisionAllow, "", nil } +// authorizeCreateToken authorizes "create" requests to serviceaccounts 'token' +// subresource of pods running on a node +func (r *NodeAuthorizer) authorizeCreateToken(nodeName string, startingType vertexType, attrs authorizer.Attributes) (authorizer.Decision, string, error) { + if attrs.GetVerb() != "create" || len(attrs.GetName()) == 0 { + glog.V(2).Infof("NODE DENY: %s %#v", nodeName, attrs) + return authorizer.DecisionNoOpinion, "can only create tokens for individual service accounts", nil + } + + if attrs.GetSubresource() != "token" { + glog.V(2).Infof("NODE DENY: %s %#v", nodeName, attrs) + return authorizer.DecisionNoOpinion, "can only create token subresource of serviceaccount", nil + } + + ok, err := r.hasPathFrom(nodeName, startingType, attrs.GetNamespace(), attrs.GetName()) + if err != nil { + glog.V(2).Infof("NODE DENY: %v", err) + return authorizer.DecisionNoOpinion, "no path found to object", nil + } + if !ok { + glog.V(2).Infof("NODE DENY: %q %#v", nodeName, attrs) + return authorizer.DecisionNoOpinion, "no path found to object", nil + } + return authorizer.DecisionAllow, "", nil +} + // hasPathFrom returns true if there is a directed path from the specified type/namespace/name to the specified Node func (r *NodeAuthorizer) hasPathFrom(nodeName string, startingType vertexType, startingNamespace, startingName string) (bool, error) { r.graph.lock.RLock() diff --git a/plugin/pkg/auth/authorizer/node/node_authorizer_test.go b/plugin/pkg/auth/authorizer/node/node_authorizer_test.go index 669e41f0206..19eb32fdfce 100644 --- a/plugin/pkg/auth/authorizer/node/node_authorizer_test.go +++ b/plugin/pkg/auth/authorizer/node/node_authorizer_test.go @@ -38,6 +38,8 @@ import ( var ( csiEnabledFeature = utilfeature.NewFeatureGate() csiDisabledFeature = utilfeature.NewFeatureGate() + trEnabledFeature = utilfeature.NewFeatureGate() + trDisabledFeature = utilfeature.NewFeatureGate() ) func init() { @@ -47,6 +49,12 @@ func init() { if err := csiDisabledFeature.Add(map[utilfeature.Feature]utilfeature.FeatureSpec{features.CSIPersistentVolume: {Default: false}}); err != nil { panic(err) } + if err := trEnabledFeature.Add(map[utilfeature.Feature]utilfeature.FeatureSpec{features.TokenRequest: {Default: true}}); err != nil { + panic(err) + } + if err := trDisabledFeature.Add(map[utilfeature.Feature]utilfeature.FeatureSpec{features.TokenRequest: {Default: false}}); err != nil { + panic(err) + } } func TestAuthorizer(t *testing.T) { @@ -152,6 +160,36 @@ func TestAuthorizer(t *testing.T) { features: csiEnabledFeature, expect: authorizer.DecisionAllow, }, + { + name: "allowed svcacct token create - feature enabled", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node0", Namespace: "ns0"}, + features: trEnabledFeature, + expect: authorizer.DecisionAllow, + }, + { + name: "disallowed svcacct token create - serviceaccount not attached to node", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node1", Namespace: "ns0"}, + features: trEnabledFeature, + expect: authorizer.DecisionNoOpinion, + }, + { + name: "disallowed svcacct token create - feature disabled", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node0", Namespace: "ns0"}, + features: trDisabledFeature, + expect: authorizer.DecisionNoOpinion, + }, + { + name: "disallowed svcacct token create - no subresource", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Name: "svcacct0-node0", Namespace: "ns0"}, + features: trEnabledFeature, + expect: authorizer.DecisionNoOpinion, + }, + { + name: "disallowed svcacct token create - non create", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node0", Namespace: "ns0"}, + features: trEnabledFeature, + expect: authorizer.DecisionNoOpinion, + }, } for _, tc := range tests { @@ -459,6 +497,7 @@ func generate(opts sampleDataOpts) ([]*api.Pod, []*api.PersistentVolume, []*stor pod.Namespace = fmt.Sprintf("ns%d", p%opts.namespaces) pod.Name = fmt.Sprintf("pod%d-%s", p, nodeName) pod.Spec.NodeName = nodeName + pod.Spec.ServiceAccountName = fmt.Sprintf("svcacct%d-%s", p, nodeName) for i := 0; i < opts.uniqueSecretsPerPod; i++ { pod.Spec.Volumes = append(pod.Spec.Volumes, api.Volume{VolumeSource: api.VolumeSource{