noderestriction: restrict nodes TokenRequest permission

nodes should only be able to create TokenRequests if:
* token is bound to a pod
* binding has uid and name
* the pod exists
* the pod is running on that node
pull/6/head
Mike Danese 2018-02-23 11:24:43 -08:00
parent 2cc75f0a5a
commit b43cd7307d
4 changed files with 142 additions and 4 deletions

View File

@ -12,6 +12,7 @@ go_library(
importpath = "k8s.io/kubernetes/plugin/pkg/admission/noderestriction",
deps = [
"//pkg/api/pod:go_default_library",
"//pkg/apis/authentication:go_default_library",
"//pkg/apis/core:go_default_library",
"//pkg/apis/policy:go_default_library",
"//pkg/auth/nodeidentifier:go_default_library",
@ -33,14 +34,18 @@ go_test(
srcs = ["admission_test.go"],
embed = [":go_default_library"],
deps = [
"//pkg/apis/authentication:go_default_library",
"//pkg/apis/core:go_default_library",
"//pkg/apis/policy:go_default_library",
"//pkg/auth/nodeidentifier:go_default_library",
"//pkg/client/clientset_generated/internalclientset/fake: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/types: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/util/feature:go_default_library",
],
)

View File

@ -27,6 +27,7 @@ import (
"k8s.io/apiserver/pkg/admission"
utilfeature "k8s.io/apiserver/pkg/util/feature"
podutil "k8s.io/kubernetes/pkg/api/pod"
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/policy"
"k8s.io/kubernetes/pkg/auth/nodeidentifier"
@ -53,6 +54,7 @@ func NewPlugin(nodeIdentifier nodeidentifier.NodeIdentifier) *nodePlugin {
return &nodePlugin{
Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete),
nodeIdentifier: nodeIdentifier,
features: utilfeature.DefaultFeatureGate,
}
}
@ -61,6 +63,8 @@ type nodePlugin struct {
*admission.Handler
nodeIdentifier nodeidentifier.NodeIdentifier
podsGetter coreinternalversion.PodsGetter
// allows overriding for testing
features utilfeature.FeatureGate
}
var (
@ -83,9 +87,10 @@ func (p *nodePlugin) ValidateInitialization() error {
}
var (
podResource = api.Resource("pods")
nodeResource = api.Resource("nodes")
pvcResource = api.Resource("persistentvolumeclaims")
podResource = api.Resource("pods")
nodeResource = api.Resource("nodes")
pvcResource = api.Resource("persistentvolumeclaims")
svcacctResource = api.Resource("serviceaccounts")
)
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"))
}
case svcacctResource:
if c.features.Enabled(features.TokenRequest) {
return c.admitServiceAccount(nodeName, a)
}
return nil
default:
return nil
}
@ -256,7 +267,7 @@ func (c *nodePlugin) admitPodEviction(nodeName string, a admission.Attributes) e
func (c *nodePlugin) admitPVCStatus(nodeName string, a admission.Attributes) error {
switch a.GetOperation() {
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))
}
@ -340,3 +351,44 @@ func (c *nodePlugin) admitNode(nodeName string, a admission.Attributes) error {
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
}

View File

@ -21,18 +21,37 @@ import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission"
"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"
"k8s.io/kubernetes/pkg/apis/policy"
"k8s.io/kubernetes/pkg/auth/nodeidentifier"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
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 {
pod := &api.Pod{}
pod.Namespace = namespace
pod.UID = types.UID("pod-uid")
pod.Name = name
pod.Spec.NodeName = node
if mirror {
@ -47,6 +66,23 @@ func makeTestPodEviction(name string) *policy.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) {
var (
mynode = &user.DefaultInfo{Name: "system:node:mynode", Groups: []string{"system:nodes"}}
@ -86,6 +122,9 @@ func Test_nodePlugin_Admit(t *testing.T) {
nodeResource = api.Resource("nodes").WithVersion("v1")
nodeKind = api.Kind("Node").WithVersion("v1")
svcacctResource = api.Resource("serviceaccounts").WithVersion("v1")
tokenrequestKind = api.Kind("TokenRequest").WithVersion("v1")
noExistingPods = fake.NewSimpleClientset().Core()
existingPods = fake.NewSimpleClientset(mymirrorpod, othermirrorpod, unboundmirrorpod, mypod, otherpod, unboundpod).Core()
)
@ -106,6 +145,7 @@ func Test_nodePlugin_Admit(t *testing.T) {
name string
podsGetter coreinternalversion.PodsGetter
attributes admission.Attributes
features utilfeature.FeatureGate
err string
}{
// Mirror pods bound to us
@ -653,6 +693,42 @@ func Test_nodePlugin_Admit(t *testing.T) {
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
{
name: "allow create of unrelated object",
@ -714,6 +790,9 @@ func Test_nodePlugin_Admit(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewPlugin(nodeidentifier.NewDefaultNodeIdentifier())
if tt.features != nil {
c.features = tt.features
}
c.podsGetter = tt.podsGetter
err := c.Admit(tt.attributes)
if (err == nil) != (len(tt.err) == 0) {

View File

@ -448,6 +448,8 @@ func TestNodeAuthorizer(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CSIPersistentVolume, true)()
expectForbidden(t, getVolumeAttachment(node1ClientExternal))
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