343 lines
12 KiB
Go
343 lines
12 KiB
Go
/*
|
|
Copyright 2017 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package noderestriction
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
|
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/util/diff"
|
|
"k8s.io/apiserver/pkg/admission"
|
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
podutil "k8s.io/kubernetes/pkg/api/pod"
|
|
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"
|
|
coreinternalversion "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
|
|
"k8s.io/kubernetes/pkg/features"
|
|
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
|
|
)
|
|
|
|
const (
|
|
PluginName = "NodeRestriction"
|
|
)
|
|
|
|
// Register registers a plugin
|
|
func Register(plugins *admission.Plugins) {
|
|
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
|
return NewPlugin(nodeidentifier.NewDefaultNodeIdentifier()), nil
|
|
})
|
|
}
|
|
|
|
// NewPlugin creates a new NodeRestriction admission plugin.
|
|
// This plugin identifies requests from nodes
|
|
func NewPlugin(nodeIdentifier nodeidentifier.NodeIdentifier) *nodePlugin {
|
|
return &nodePlugin{
|
|
Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete),
|
|
nodeIdentifier: nodeIdentifier,
|
|
}
|
|
}
|
|
|
|
// nodePlugin holds state for and implements the admission plugin.
|
|
type nodePlugin struct {
|
|
*admission.Handler
|
|
nodeIdentifier nodeidentifier.NodeIdentifier
|
|
podsGetter coreinternalversion.PodsGetter
|
|
}
|
|
|
|
var (
|
|
_ = admission.Interface(&nodePlugin{})
|
|
_ = kubeapiserveradmission.WantsInternalKubeClientSet(&nodePlugin{})
|
|
)
|
|
|
|
func (p *nodePlugin) SetInternalKubeClientSet(f internalclientset.Interface) {
|
|
p.podsGetter = f.Core()
|
|
}
|
|
|
|
func (p *nodePlugin) ValidateInitialization() error {
|
|
if p.nodeIdentifier == nil {
|
|
return fmt.Errorf("%s requires a node identifier", PluginName)
|
|
}
|
|
if p.podsGetter == nil {
|
|
return fmt.Errorf("%s requires a pod getter", PluginName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
podResource = api.Resource("pods")
|
|
nodeResource = api.Resource("nodes")
|
|
pvcResource = api.Resource("persistentvolumeclaims")
|
|
)
|
|
|
|
func (c *nodePlugin) Admit(a admission.Attributes) error {
|
|
nodeName, isNode := c.nodeIdentifier.NodeIdentity(a.GetUserInfo())
|
|
|
|
// Our job is just to restrict nodes
|
|
if !isNode {
|
|
return nil
|
|
}
|
|
|
|
if len(nodeName) == 0 {
|
|
// disallow requests we cannot match to a particular node
|
|
return admission.NewForbidden(a, fmt.Errorf("could not determine node from user %q", a.GetUserInfo().GetName()))
|
|
}
|
|
|
|
switch a.GetResource().GroupResource() {
|
|
case podResource:
|
|
switch a.GetSubresource() {
|
|
case "":
|
|
return c.admitPod(nodeName, a)
|
|
case "status":
|
|
return c.admitPodStatus(nodeName, a)
|
|
case "eviction":
|
|
return c.admitPodEviction(nodeName, a)
|
|
default:
|
|
return admission.NewForbidden(a, fmt.Errorf("unexpected pod subresource %q", a.GetSubresource()))
|
|
}
|
|
|
|
case nodeResource:
|
|
return c.admitNode(nodeName, a)
|
|
|
|
case pvcResource:
|
|
switch a.GetSubresource() {
|
|
case "status":
|
|
return c.admitPVCStatus(nodeName, a)
|
|
default:
|
|
return admission.NewForbidden(a, fmt.Errorf("may only update PVC status"))
|
|
}
|
|
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (c *nodePlugin) admitPod(nodeName string, a admission.Attributes) error {
|
|
switch a.GetOperation() {
|
|
case admission.Create:
|
|
// require a pod object
|
|
pod, ok := a.GetObject().(*api.Pod)
|
|
if !ok {
|
|
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
|
|
}
|
|
|
|
// only allow nodes to create mirror pods
|
|
if _, isMirrorPod := pod.Annotations[api.MirrorPodAnnotationKey]; !isMirrorPod {
|
|
return admission.NewForbidden(a, fmt.Errorf("pod does not have %q annotation, node %q can only create mirror pods", api.MirrorPodAnnotationKey, nodeName))
|
|
}
|
|
|
|
// only allow nodes to create a pod bound to itself
|
|
if pod.Spec.NodeName != nodeName {
|
|
return admission.NewForbidden(a, fmt.Errorf("node %q can only create pods with spec.nodeName set to itself", nodeName))
|
|
}
|
|
|
|
// don't allow a node to create a pod that references any other API objects
|
|
if pod.Spec.ServiceAccountName != "" {
|
|
return admission.NewForbidden(a, fmt.Errorf("node %q can not create pods that reference a service account", nodeName))
|
|
}
|
|
hasSecrets := false
|
|
podutil.VisitPodSecretNames(pod, func(name string) (shouldContinue bool) { hasSecrets = true; return false })
|
|
if hasSecrets {
|
|
return admission.NewForbidden(a, fmt.Errorf("node %q can not create pods that reference secrets", nodeName))
|
|
}
|
|
hasConfigMaps := false
|
|
podutil.VisitPodConfigmapNames(pod, func(name string) (shouldContinue bool) { hasConfigMaps = true; return false })
|
|
if hasConfigMaps {
|
|
return admission.NewForbidden(a, fmt.Errorf("node %q can not create pods that reference configmaps", nodeName))
|
|
}
|
|
for _, v := range pod.Spec.Volumes {
|
|
if v.PersistentVolumeClaim != nil {
|
|
return admission.NewForbidden(a, fmt.Errorf("node %q can not create pods that reference persistentvolumeclaims", nodeName))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
|
|
case admission.Delete:
|
|
// get the existing pod from the server cache
|
|
existingPod, err := c.podsGetter.Pods(a.GetNamespace()).Get(a.GetName(), v1.GetOptions{ResourceVersion: "0"})
|
|
if errors.IsNotFound(err) {
|
|
// wasn't found in the server cache, do a live lookup before forbidding
|
|
existingPod, err = c.podsGetter.Pods(a.GetNamespace()).Get(a.GetName(), v1.GetOptions{})
|
|
if errors.IsNotFound(err) {
|
|
return err
|
|
}
|
|
}
|
|
if err != nil {
|
|
return admission.NewForbidden(a, err)
|
|
}
|
|
// only allow a node to delete a pod bound to itself
|
|
if existingPod.Spec.NodeName != nodeName {
|
|
return admission.NewForbidden(a, fmt.Errorf("node %q can only delete pods with spec.nodeName set to itself", nodeName))
|
|
}
|
|
return nil
|
|
|
|
default:
|
|
return admission.NewForbidden(a, fmt.Errorf("unexpected operation %q", a.GetOperation()))
|
|
}
|
|
}
|
|
|
|
func (c *nodePlugin) admitPodStatus(nodeName string, a admission.Attributes) error {
|
|
switch a.GetOperation() {
|
|
case admission.Update:
|
|
// require an existing pod
|
|
pod, ok := a.GetOldObject().(*api.Pod)
|
|
if !ok {
|
|
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetOldObject()))
|
|
}
|
|
// only allow a node to update status of a pod bound to itself
|
|
if pod.Spec.NodeName != nodeName {
|
|
return admission.NewForbidden(a, fmt.Errorf("node %q can only update pod status for pods with spec.nodeName set to itself", nodeName))
|
|
}
|
|
return nil
|
|
|
|
default:
|
|
return admission.NewForbidden(a, fmt.Errorf("unexpected operation %q", a.GetOperation()))
|
|
}
|
|
}
|
|
|
|
func (c *nodePlugin) admitPodEviction(nodeName string, a admission.Attributes) error {
|
|
switch a.GetOperation() {
|
|
case admission.Create:
|
|
// require eviction to an existing pod object
|
|
eviction, ok := a.GetObject().(*policy.Eviction)
|
|
if !ok {
|
|
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
|
|
}
|
|
// use pod name from the admission attributes, if set, rather than from the submitted Eviction object
|
|
podName := a.GetName()
|
|
if len(podName) == 0 {
|
|
if len(eviction.Name) == 0 {
|
|
return admission.NewForbidden(a, fmt.Errorf("could not determine pod from request data"))
|
|
}
|
|
podName = eviction.Name
|
|
}
|
|
// get the existing pod from the server cache
|
|
existingPod, err := c.podsGetter.Pods(a.GetNamespace()).Get(podName, v1.GetOptions{ResourceVersion: "0"})
|
|
if errors.IsNotFound(err) {
|
|
// wasn't found in the server cache, do a live lookup before forbidding
|
|
existingPod, err = c.podsGetter.Pods(a.GetNamespace()).Get(podName, v1.GetOptions{})
|
|
if errors.IsNotFound(err) {
|
|
return err
|
|
}
|
|
}
|
|
if err != nil {
|
|
return admission.NewForbidden(a, err)
|
|
}
|
|
// only allow a node to evict a pod bound to itself
|
|
if existingPod.Spec.NodeName != nodeName {
|
|
return admission.NewForbidden(a, fmt.Errorf("node %s can only evict pods with spec.nodeName set to itself", nodeName))
|
|
}
|
|
return nil
|
|
|
|
default:
|
|
return admission.NewForbidden(a, fmt.Errorf("unexpected operation %s", a.GetOperation()))
|
|
}
|
|
}
|
|
|
|
func (c *nodePlugin) admitPVCStatus(nodeName string, a admission.Attributes) error {
|
|
switch a.GetOperation() {
|
|
case admission.Update:
|
|
if !utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) {
|
|
return admission.NewForbidden(a, fmt.Errorf("node %q may not update persistentvolumeclaim metadata", nodeName))
|
|
}
|
|
|
|
oldPVC, ok := a.GetOldObject().(*api.PersistentVolumeClaim)
|
|
if !ok {
|
|
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetOldObject()))
|
|
}
|
|
|
|
newPVC, ok := a.GetObject().(*api.PersistentVolumeClaim)
|
|
if !ok {
|
|
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
|
|
}
|
|
|
|
// make copies for comparison
|
|
oldPVC = oldPVC.DeepCopy()
|
|
newPVC = newPVC.DeepCopy()
|
|
|
|
// zero out resourceVersion to avoid comparing differences,
|
|
// since the new object could leave it empty to indicate an unconditional update
|
|
oldPVC.ObjectMeta.ResourceVersion = ""
|
|
newPVC.ObjectMeta.ResourceVersion = ""
|
|
|
|
oldPVC.Status.Capacity = nil
|
|
newPVC.Status.Capacity = nil
|
|
|
|
oldPVC.Status.Conditions = nil
|
|
newPVC.Status.Conditions = nil
|
|
|
|
// ensure no metadata changed. nodes should not be able to relabel, add finalizers/owners, etc
|
|
if !apiequality.Semantic.DeepEqual(oldPVC, newPVC) {
|
|
return admission.NewForbidden(a, fmt.Errorf("node %q may not update fields other than status.capacity and status.conditions: %v", nodeName, diff.ObjectReflectDiff(oldPVC, newPVC)))
|
|
}
|
|
|
|
return nil
|
|
|
|
default:
|
|
return admission.NewForbidden(a, fmt.Errorf("unexpected operation %q", a.GetOperation()))
|
|
}
|
|
}
|
|
|
|
func (c *nodePlugin) admitNode(nodeName string, a admission.Attributes) error {
|
|
requestedName := a.GetName()
|
|
if a.GetOperation() == admission.Create {
|
|
node, ok := a.GetObject().(*api.Node)
|
|
if !ok {
|
|
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
|
|
}
|
|
|
|
// Don't allow a node to create its Node API object with the config source set.
|
|
// We scope node access to things listed in the Node.Spec, so allowing this would allow a view escalation.
|
|
if node.Spec.ConfigSource != nil {
|
|
return admission.NewForbidden(a, fmt.Errorf("cannot create with non-nil configSource"))
|
|
}
|
|
|
|
// On create, get name from new object if unset in admission
|
|
if len(requestedName) == 0 {
|
|
requestedName = node.Name
|
|
}
|
|
}
|
|
if requestedName != nodeName {
|
|
return admission.NewForbidden(a, fmt.Errorf("node %q cannot modify node %q", nodeName, requestedName))
|
|
}
|
|
|
|
if a.GetOperation() == admission.Update {
|
|
node, ok := a.GetObject().(*api.Node)
|
|
if !ok {
|
|
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
|
|
}
|
|
oldNode, ok := a.GetOldObject().(*api.Node)
|
|
if !ok {
|
|
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
|
|
}
|
|
|
|
// Don't allow a node to update the config source on its Node API object.
|
|
// We scope node access to things listed in the Node.Spec, so allowing this would allow a view escalation.
|
|
// We only do the check if the new node's configSource is non-nil; old kubelets might drop the field during a status update.
|
|
if node.Spec.ConfigSource != nil && !apiequality.Semantic.DeepEqual(node.Spec.ConfigSource, oldNode.Spec.ConfigSource) {
|
|
return admission.NewForbidden(a, fmt.Errorf("cannot update configSource to a new non-nil configSource"))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|