Unverified Commit 513e67ac authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #55019 from mikedanese/svcacct

Automatic merge from submit-queue (batch tested with PRs 59365, 60446, 60448, 55019, 60431). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. auth: allow nodes to create tokens for svcaccts of pods ref https://github.com/kubernetes/kubernetes/issues/58790 running on them. nodes essentially have the power to do this today but not explicitly. this allows agents using the node identity to take actions on behalf of local pods. @kubernetes/sig-auth-pr-reviews @smarterclayton ```release-note The node authorizer now allows nodes to request service account tokens for the service accounts of pods running on them. ```
parents c7b3e163 363e861a
...@@ -12,6 +12,7 @@ go_library( ...@@ -12,6 +12,7 @@ go_library(
importpath = "k8s.io/kubernetes/plugin/pkg/admission/noderestriction", importpath = "k8s.io/kubernetes/plugin/pkg/admission/noderestriction",
deps = [ deps = [
"//pkg/api/pod:go_default_library", "//pkg/api/pod:go_default_library",
"//pkg/apis/authentication:go_default_library",
"//pkg/apis/core:go_default_library", "//pkg/apis/core:go_default_library",
"//pkg/apis/policy:go_default_library", "//pkg/apis/policy:go_default_library",
"//pkg/auth/nodeidentifier:go_default_library", "//pkg/auth/nodeidentifier:go_default_library",
...@@ -33,14 +34,18 @@ go_test( ...@@ -33,14 +34,18 @@ go_test(
srcs = ["admission_test.go"], srcs = ["admission_test.go"],
embed = [":go_default_library"], embed = [":go_default_library"],
deps = [ deps = [
"//pkg/apis/authentication:go_default_library",
"//pkg/apis/core:go_default_library", "//pkg/apis/core:go_default_library",
"//pkg/apis/policy:go_default_library", "//pkg/apis/policy:go_default_library",
"//pkg/auth/nodeidentifier:go_default_library", "//pkg/auth/nodeidentifier:go_default_library",
"//pkg/client/clientset_generated/internalclientset/fake:go_default_library", "//pkg/client/clientset_generated/internalclientset/fake:go_default_library",
"//pkg/client/clientset_generated/internalclientset/typed/core/internalversion:go_default_library", "//pkg/client/clientset_generated/internalclientset/typed/core/internalversion:go_default_library",
"//pkg/features:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library", "//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
], ],
) )
......
...@@ -27,6 +27,7 @@ import ( ...@@ -27,6 +27,7 @@ import (
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
podutil "k8s.io/kubernetes/pkg/api/pod" podutil "k8s.io/kubernetes/pkg/api/pod"
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
api "k8s.io/kubernetes/pkg/apis/core" api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/policy" "k8s.io/kubernetes/pkg/apis/policy"
"k8s.io/kubernetes/pkg/auth/nodeidentifier" "k8s.io/kubernetes/pkg/auth/nodeidentifier"
...@@ -53,6 +54,7 @@ func NewPlugin(nodeIdentifier nodeidentifier.NodeIdentifier) *nodePlugin { ...@@ -53,6 +54,7 @@ func NewPlugin(nodeIdentifier nodeidentifier.NodeIdentifier) *nodePlugin {
return &nodePlugin{ return &nodePlugin{
Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete), Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete),
nodeIdentifier: nodeIdentifier, nodeIdentifier: nodeIdentifier,
features: utilfeature.DefaultFeatureGate,
} }
} }
...@@ -61,6 +63,8 @@ type nodePlugin struct { ...@@ -61,6 +63,8 @@ type nodePlugin struct {
*admission.Handler *admission.Handler
nodeIdentifier nodeidentifier.NodeIdentifier nodeIdentifier nodeidentifier.NodeIdentifier
podsGetter coreinternalversion.PodsGetter podsGetter coreinternalversion.PodsGetter
// allows overriding for testing
features utilfeature.FeatureGate
} }
var ( var (
...@@ -83,9 +87,10 @@ func (p *nodePlugin) ValidateInitialization() error { ...@@ -83,9 +87,10 @@ func (p *nodePlugin) ValidateInitialization() error {
} }
var ( var (
podResource = api.Resource("pods") podResource = api.Resource("pods")
nodeResource = api.Resource("nodes") nodeResource = api.Resource("nodes")
pvcResource = api.Resource("persistentvolumeclaims") pvcResource = api.Resource("persistentvolumeclaims")
svcacctResource = api.Resource("serviceaccounts")
) )
func (c *nodePlugin) Admit(a admission.Attributes) error { func (c *nodePlugin) Admit(a admission.Attributes) error {
...@@ -125,6 +130,12 @@ func (c *nodePlugin) Admit(a admission.Attributes) error { ...@@ -125,6 +130,12 @@ func (c *nodePlugin) Admit(a admission.Attributes) error {
return admission.NewForbidden(a, fmt.Errorf("may only update PVC status")) return admission.NewForbidden(a, fmt.Errorf("may only update PVC status"))
} }
case svcacctResource:
if c.features.Enabled(features.TokenRequest) {
return c.admitServiceAccount(nodeName, a)
}
return nil
default: default:
return nil return nil
} }
...@@ -256,7 +267,7 @@ func (c *nodePlugin) admitPodEviction(nodeName string, a admission.Attributes) e ...@@ -256,7 +267,7 @@ func (c *nodePlugin) admitPodEviction(nodeName string, a admission.Attributes) e
func (c *nodePlugin) admitPVCStatus(nodeName string, a admission.Attributes) error { func (c *nodePlugin) admitPVCStatus(nodeName string, a admission.Attributes) error {
switch a.GetOperation() { switch a.GetOperation() {
case admission.Update: case admission.Update:
if !utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) { if !c.features.Enabled(features.ExpandPersistentVolumes) {
return admission.NewForbidden(a, fmt.Errorf("node %q may not update persistentvolumeclaim metadata", nodeName)) return admission.NewForbidden(a, fmt.Errorf("node %q may not update persistentvolumeclaim metadata", nodeName))
} }
...@@ -340,3 +351,44 @@ func (c *nodePlugin) admitNode(nodeName string, a admission.Attributes) error { ...@@ -340,3 +351,44 @@ func (c *nodePlugin) admitNode(nodeName string, a admission.Attributes) error {
return nil return nil
} }
func (c *nodePlugin) admitServiceAccount(nodeName string, a admission.Attributes) error {
if a.GetOperation() != admission.Create {
return nil
}
if a.GetSubresource() != "token" {
return nil
}
tr, ok := a.GetObject().(*authenticationapi.TokenRequest)
if !ok {
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
}
// TokenRequests from a node must have a pod binding. That pod must be
// scheduled on the node.
ref := tr.Spec.BoundObjectRef
if ref == nil ||
ref.APIVersion != "v1" ||
ref.Kind != "Pod" ||
ref.Name == "" {
return admission.NewForbidden(a, fmt.Errorf("node requested token not bound to a pod"))
}
if ref.UID == "" {
return admission.NewForbidden(a, fmt.Errorf("node requested token with a pod binding without a uid"))
}
pod, err := c.podsGetter.Pods(a.GetNamespace()).Get(ref.Name, v1.GetOptions{})
if errors.IsNotFound(err) {
return err
}
if err != nil {
return admission.NewForbidden(a, err)
}
if ref.UID != pod.UID {
return admission.NewForbidden(a, fmt.Errorf("the UID in the bound object reference (%s) does not match the UID in record (%s). The object might have been deleted and then recreated", ref.UID, pod.UID))
}
if pod.Spec.NodeName != nodeName {
return admission.NewForbidden(a, fmt.Errorf("node requested token bound to a pod scheduled on a different node"))
}
return nil
}
...@@ -21,18 +21,37 @@ import ( ...@@ -21,18 +21,37 @@ import (
"testing" "testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
utilfeature "k8s.io/apiserver/pkg/util/feature"
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
api "k8s.io/kubernetes/pkg/apis/core" api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/policy" "k8s.io/kubernetes/pkg/apis/policy"
"k8s.io/kubernetes/pkg/auth/nodeidentifier" "k8s.io/kubernetes/pkg/auth/nodeidentifier"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
coreinternalversion "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" coreinternalversion "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
"k8s.io/kubernetes/pkg/features"
) )
var (
trEnabledFeature = utilfeature.NewFeatureGate()
trDisabledFeature = utilfeature.NewFeatureGate()
)
func init() {
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 makeTestPod(namespace, name, node string, mirror bool) *api.Pod { func makeTestPod(namespace, name, node string, mirror bool) *api.Pod {
pod := &api.Pod{} pod := &api.Pod{}
pod.Namespace = namespace pod.Namespace = namespace
pod.UID = types.UID("pod-uid")
pod.Name = name pod.Name = name
pod.Spec.NodeName = node pod.Spec.NodeName = node
if mirror { if mirror {
...@@ -47,6 +66,23 @@ func makeTestPodEviction(name string) *policy.Eviction { ...@@ -47,6 +66,23 @@ func makeTestPodEviction(name string) *policy.Eviction {
return eviction return eviction
} }
func makeTokenRequest(podname string, poduid types.UID) *authenticationapi.TokenRequest {
tr := &authenticationapi.TokenRequest{
Spec: authenticationapi.TokenRequestSpec{
Audiences: []string{"foo"},
},
}
if podname != "" {
tr.Spec.BoundObjectRef = &authenticationapi.BoundObjectReference{
Kind: "Pod",
APIVersion: "v1",
Name: podname,
UID: poduid,
}
}
return tr
}
func Test_nodePlugin_Admit(t *testing.T) { func Test_nodePlugin_Admit(t *testing.T) {
var ( var (
mynode = &user.DefaultInfo{Name: "system:node:mynode", Groups: []string{"system:nodes"}} mynode = &user.DefaultInfo{Name: "system:node:mynode", Groups: []string{"system:nodes"}}
...@@ -86,6 +122,9 @@ func Test_nodePlugin_Admit(t *testing.T) { ...@@ -86,6 +122,9 @@ func Test_nodePlugin_Admit(t *testing.T) {
nodeResource = api.Resource("nodes").WithVersion("v1") nodeResource = api.Resource("nodes").WithVersion("v1")
nodeKind = api.Kind("Node").WithVersion("v1") nodeKind = api.Kind("Node").WithVersion("v1")
svcacctResource = api.Resource("serviceaccounts").WithVersion("v1")
tokenrequestKind = api.Kind("TokenRequest").WithVersion("v1")
noExistingPods = fake.NewSimpleClientset().Core() noExistingPods = fake.NewSimpleClientset().Core()
existingPods = fake.NewSimpleClientset(mymirrorpod, othermirrorpod, unboundmirrorpod, mypod, otherpod, unboundpod).Core() existingPods = fake.NewSimpleClientset(mymirrorpod, othermirrorpod, unboundmirrorpod, mypod, otherpod, unboundpod).Core()
) )
...@@ -106,6 +145,7 @@ func Test_nodePlugin_Admit(t *testing.T) { ...@@ -106,6 +145,7 @@ func Test_nodePlugin_Admit(t *testing.T) {
name string name string
podsGetter coreinternalversion.PodsGetter podsGetter coreinternalversion.PodsGetter
attributes admission.Attributes attributes admission.Attributes
features utilfeature.FeatureGate
err string err string
}{ }{
// Mirror pods bound to us // Mirror pods bound to us
...@@ -653,6 +693,42 @@ func Test_nodePlugin_Admit(t *testing.T) { ...@@ -653,6 +693,42 @@ func Test_nodePlugin_Admit(t *testing.T) {
err: "cannot modify node", err: "cannot modify node",
}, },
// Service accounts
{
name: "forbid create of unbound token",
podsGetter: noExistingPods,
features: trEnabledFeature,
attributes: admission.NewAttributesRecord(makeTokenRequest("", ""), nil, tokenrequestKind, "ns", "mysa", svcacctResource, "token", admission.Create, mynode),
err: "not bound to a pod",
},
{
name: "forbid create of token bound to nonexistant pod",
podsGetter: noExistingPods,
features: trEnabledFeature,
attributes: admission.NewAttributesRecord(makeTokenRequest("nopod", "someuid"), nil, tokenrequestKind, "ns", "mysa", svcacctResource, "token", admission.Create, mynode),
err: "not found",
},
{
name: "forbid create of token bound to pod without uid",
podsGetter: existingPods,
features: trEnabledFeature,
attributes: admission.NewAttributesRecord(makeTokenRequest(mypod.Name, ""), nil, tokenrequestKind, "ns", "mysa", svcacctResource, "token", admission.Create, mynode),
err: "pod binding without a uid",
},
{
name: "forbid create of token bound to pod scheduled on another node",
podsGetter: existingPods,
features: trEnabledFeature,
attributes: admission.NewAttributesRecord(makeTokenRequest(otherpod.Name, otherpod.UID), nil, tokenrequestKind, otherpod.Namespace, "mysa", svcacctResource, "token", admission.Create, mynode),
err: "pod scheduled on a different node",
},
{
name: "allow create of token bound to pod scheduled this node",
podsGetter: existingPods,
features: trEnabledFeature,
attributes: admission.NewAttributesRecord(makeTokenRequest(mypod.Name, mypod.UID), nil, tokenrequestKind, mypod.Namespace, "mysa", svcacctResource, "token", admission.Create, mynode),
},
// Unrelated objects // Unrelated objects
{ {
name: "allow create of unrelated object", name: "allow create of unrelated object",
...@@ -714,6 +790,9 @@ func Test_nodePlugin_Admit(t *testing.T) { ...@@ -714,6 +790,9 @@ func Test_nodePlugin_Admit(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := NewPlugin(nodeidentifier.NewDefaultNodeIdentifier()) c := NewPlugin(nodeidentifier.NewDefaultNodeIdentifier())
if tt.features != nil {
c.features = tt.features
}
c.podsGetter = tt.podsGetter c.podsGetter = tt.podsGetter
err := c.Admit(tt.attributes) err := c.Admit(tt.attributes)
if (err == nil) != (len(tt.err) == 0) { if (err == nil) != (len(tt.err) == 0) {
......
...@@ -107,16 +107,18 @@ const ( ...@@ -107,16 +107,18 @@ const (
pvVertexType pvVertexType
secretVertexType secretVertexType
vaVertexType vaVertexType
serviceAccountVertexType
) )
var vertexTypes = map[vertexType]string{ var vertexTypes = map[vertexType]string{
configMapVertexType: "configmap", configMapVertexType: "configmap",
nodeVertexType: "node", nodeVertexType: "node",
podVertexType: "pod", podVertexType: "pod",
pvcVertexType: "pvc", pvcVertexType: "pvc",
pvVertexType: "pv", pvVertexType: "pv",
secretVertexType: "secret", secretVertexType: "secret",
vaVertexType: "volumeattachment", vaVertexType: "volumeattachment",
serviceAccountVertexType: "serviceAccount",
} }
// must be called under a write lock // must be called under a write lock
...@@ -204,6 +206,7 @@ func (g *Graph) deleteVertex_locked(vertexType vertexType, namespace, name strin ...@@ -204,6 +206,7 @@ func (g *Graph) deleteVertex_locked(vertexType vertexType, namespace, name strin
// secret -> pod // secret -> pod
// configmap -> pod // configmap -> pod
// pvc -> pod // pvc -> pod
// svcacct -> pod
func (g *Graph) AddPod(pod *api.Pod) { func (g *Graph) AddPod(pod *api.Pod) {
g.lock.Lock() g.lock.Lock()
defer g.lock.Unlock() defer g.lock.Unlock()
...@@ -213,6 +216,14 @@ func (g *Graph) AddPod(pod *api.Pod) { ...@@ -213,6 +216,14 @@ func (g *Graph) AddPod(pod *api.Pod) {
nodeVertex := g.getOrCreateVertex_locked(nodeVertexType, "", pod.Spec.NodeName) nodeVertex := g.getOrCreateVertex_locked(nodeVertexType, "", pod.Spec.NodeName)
g.graph.SetEdge(newDestinationEdge(podVertex, nodeVertex, nodeVertex)) 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 { podutil.VisitPodSecretNames(pod, func(secret string) bool {
g.graph.SetEdge(newDestinationEdge(g.getOrCreateVertex_locked(secretVertexType, pod.Namespace, secret), podVertex, nodeVertex)) g.graph.SetEdge(newDestinationEdge(g.getOrCreateVertex_locked(secretVertexType, pod.Namespace, secret), podVertex, nodeVertex))
return true return true
......
...@@ -70,6 +70,7 @@ var ( ...@@ -70,6 +70,7 @@ var (
pvcResource = api.Resource("persistentvolumeclaims") pvcResource = api.Resource("persistentvolumeclaims")
pvResource = api.Resource("persistentvolumes") pvResource = api.Resource("persistentvolumes")
vaResource = storageapi.Resource("volumeattachments") vaResource = storageapi.Resource("volumeattachments")
svcAcctResource = api.Resource("serviceaccounts")
) )
func (r *NodeAuthorizer) Authorize(attrs authorizer.Attributes) (authorizer.Decision, string, error) { func (r *NodeAuthorizer) Authorize(attrs authorizer.Attributes) (authorizer.Decision, string, error) {
...@@ -106,6 +107,11 @@ func (r *NodeAuthorizer) Authorize(attrs authorizer.Attributes) (authorizer.Deci ...@@ -106,6 +107,11 @@ func (r *NodeAuthorizer) Authorize(attrs authorizer.Attributes) (authorizer.Deci
return r.authorizeGet(nodeName, vaVertexType, attrs) return r.authorizeGet(nodeName, vaVertexType, attrs)
} }
return authorizer.DecisionNoOpinion, fmt.Sprintf("disabled by feature gate %s", features.CSIPersistentVolume), nil 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 ...@@ -165,6 +171,31 @@ func (r *NodeAuthorizer) authorize(nodeName string, startingType vertexType, att
return authorizer.DecisionAllow, "", nil 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 // 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) { func (r *NodeAuthorizer) hasPathFrom(nodeName string, startingType vertexType, startingNamespace, startingName string) (bool, error) {
r.graph.lock.RLock() r.graph.lock.RLock()
......
...@@ -38,6 +38,8 @@ import ( ...@@ -38,6 +38,8 @@ import (
var ( var (
csiEnabledFeature = utilfeature.NewFeatureGate() csiEnabledFeature = utilfeature.NewFeatureGate()
csiDisabledFeature = utilfeature.NewFeatureGate() csiDisabledFeature = utilfeature.NewFeatureGate()
trEnabledFeature = utilfeature.NewFeatureGate()
trDisabledFeature = utilfeature.NewFeatureGate()
) )
func init() { func init() {
...@@ -47,6 +49,12 @@ func init() { ...@@ -47,6 +49,12 @@ func init() {
if err := csiDisabledFeature.Add(map[utilfeature.Feature]utilfeature.FeatureSpec{features.CSIPersistentVolume: {Default: false}}); err != nil { if err := csiDisabledFeature.Add(map[utilfeature.Feature]utilfeature.FeatureSpec{features.CSIPersistentVolume: {Default: false}}); err != nil {
panic(err) 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) { func TestAuthorizer(t *testing.T) {
...@@ -152,6 +160,36 @@ func TestAuthorizer(t *testing.T) { ...@@ -152,6 +160,36 @@ func TestAuthorizer(t *testing.T) {
features: csiEnabledFeature, features: csiEnabledFeature,
expect: authorizer.DecisionAllow, 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 { for _, tc := range tests {
...@@ -459,6 +497,7 @@ func generate(opts sampleDataOpts) ([]*api.Pod, []*api.PersistentVolume, []*stor ...@@ -459,6 +497,7 @@ func generate(opts sampleDataOpts) ([]*api.Pod, []*api.PersistentVolume, []*stor
pod.Namespace = fmt.Sprintf("ns%d", p%opts.namespaces) pod.Namespace = fmt.Sprintf("ns%d", p%opts.namespaces)
pod.Name = fmt.Sprintf("pod%d-%s", p, nodeName) pod.Name = fmt.Sprintf("pod%d-%s", p, nodeName)
pod.Spec.NodeName = nodeName pod.Spec.NodeName = nodeName
pod.Spec.ServiceAccountName = fmt.Sprintf("svcacct%d-%s", p, nodeName)
for i := 0; i < opts.uniqueSecretsPerPod; i++ { for i := 0; i < opts.uniqueSecretsPerPod; i++ {
pod.Spec.Volumes = append(pod.Spec.Volumes, api.Volume{VolumeSource: api.VolumeSource{ pod.Spec.Volumes = append(pod.Spec.Volumes, api.Volume{VolumeSource: api.VolumeSource{
......
...@@ -146,6 +146,13 @@ func NodeRules() []rbac.PolicyRule { ...@@ -146,6 +146,13 @@ func NodeRules() []rbac.PolicyRule {
nodePolicyRules = append(nodePolicyRules, pvcStatusPolicyRule) nodePolicyRules = append(nodePolicyRules, pvcStatusPolicyRule)
} }
if utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) {
// Use the Node authorization to limit a node to create tokens for service accounts running on that node
// Use the NodeRestriction admission plugin to limit a node to create tokens bound to pods on that node
tokenRequestRule := rbac.NewRule("create").Groups(legacyGroup).Resources("serviceaccounts/token").RuleOrDie()
nodePolicyRules = append(nodePolicyRules, tokenRequestRule)
}
// CSI // CSI
if utilfeature.DefaultFeatureGate.Enabled(features.CSIPersistentVolume) { if utilfeature.DefaultFeatureGate.Enabled(features.CSIPersistentVolume) {
volAttachRule := rbac.NewRule("get").Groups(storageGroup).Resources("volumeattachments").RuleOrDie() volAttachRule := rbac.NewRule("get").Groups(storageGroup).Resources("volumeattachments").RuleOrDie()
......
...@@ -448,6 +448,8 @@ func TestNodeAuthorizer(t *testing.T) { ...@@ -448,6 +448,8 @@ func TestNodeAuthorizer(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CSIPersistentVolume, true)() defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CSIPersistentVolume, true)()
expectForbidden(t, getVolumeAttachment(node1ClientExternal)) expectForbidden(t, getVolumeAttachment(node1ClientExternal))
expectAllowed(t, getVolumeAttachment(node2ClientExternal)) expectAllowed(t, getVolumeAttachment(node2ClientExternal))
//TODO(mikedanese): integration test node restriction of TokenRequest
} }
// expect executes a function a set number of times until it either returns the // expect executes a function a set number of times until it either returns the
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment