mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-05 02:09:56 +00:00
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
This commit is contained in:
parent
2cc75f0a5a
commit
b43cd7307d
@ -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(
|
|||||||
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 (
|
|||||||
"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 {
|
|||||||
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 {
|
|||||||
*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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
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
|
|||||||
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 {
|
|||||||
|
|
||||||
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 (
|
|||||||
"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 {
|
|||||||
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) {
|
|||||||
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) {
|
|||||||
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) {
|
|||||||
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) {
|
|||||||
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) {
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user