mirror of
https://github.com/optim-enterprises-bv/kubernetes.git
synced 2025-11-02 03:08:15 +00:00
node authorizer: lock down access for NodeResourceSlice
The kubelet running on one node should not be allowed to access NodeResourceSlice objects belonging to some other node, as defined by the NodeResourceSlice.NodeName field.
This commit is contained in:
@@ -114,6 +114,7 @@ type vertexType byte
|
||||
|
||||
const (
|
||||
configMapVertexType vertexType = iota
|
||||
sliceVertexType
|
||||
nodeVertexType
|
||||
podVertexType
|
||||
pvcVertexType
|
||||
@@ -126,6 +127,7 @@ const (
|
||||
|
||||
var vertexTypes = map[vertexType]string{
|
||||
configMapVertexType: "configmap",
|
||||
sliceVertexType: "noderesourceslice",
|
||||
nodeVertexType: "node",
|
||||
podVertexType: "pod",
|
||||
pvcVertexType: "pvc",
|
||||
@@ -492,3 +494,34 @@ func (g *Graph) DeleteVolumeAttachment(name string) {
|
||||
defer g.lock.Unlock()
|
||||
g.deleteVertex_locked(vaVertexType, "", name)
|
||||
}
|
||||
|
||||
// AddNodeResourceSlice sets up edges for the following relationships:
|
||||
//
|
||||
// node resource slice -> node
|
||||
func (g *Graph) AddNodeResourceSlice(sliceName, nodeName string) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
graphActionsDuration.WithLabelValues("AddNodeResourceSlice").Observe(time.Since(start).Seconds())
|
||||
}()
|
||||
g.lock.Lock()
|
||||
defer g.lock.Unlock()
|
||||
|
||||
// clear existing edges
|
||||
g.deleteVertex_locked(sliceVertexType, "", sliceName)
|
||||
|
||||
// if we have a node, establish new edges
|
||||
if len(nodeName) > 0 {
|
||||
sliceVertex := g.getOrCreateVertex_locked(sliceVertexType, "", sliceName)
|
||||
nodeVertex := g.getOrCreateVertex_locked(nodeVertexType, "", nodeName)
|
||||
g.graph.SetEdge(newDestinationEdge(sliceVertex, nodeVertex, nodeVertex))
|
||||
}
|
||||
}
|
||||
func (g *Graph) DeleteNodeResourceSlice(sliceName string) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
graphActionsDuration.WithLabelValues("DeleteNodeResourceSlice").Observe(time.Since(start).Seconds())
|
||||
}()
|
||||
g.lock.Lock()
|
||||
defer g.lock.Unlock()
|
||||
g.deleteVertex_locked(sliceVertexType, "", sliceName)
|
||||
}
|
||||
|
||||
@@ -22,9 +22,11 @@ import (
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
resourcev1alpha2 "k8s.io/api/resource/v1alpha2"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
resourcev1alpha2informers "k8s.io/client-go/informers/resource/v1alpha2"
|
||||
storageinformers "k8s.io/client-go/informers/storage/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
@@ -39,6 +41,7 @@ func AddGraphEventHandlers(
|
||||
pods corev1informers.PodInformer,
|
||||
pvs corev1informers.PersistentVolumeInformer,
|
||||
attachments storageinformers.VolumeAttachmentInformer,
|
||||
slices resourcev1alpha2informers.NodeResourceSliceInformer,
|
||||
) {
|
||||
g := &graphPopulator{
|
||||
graph: graph,
|
||||
@@ -62,8 +65,20 @@ func AddGraphEventHandlers(
|
||||
DeleteFunc: g.deleteVolumeAttachment,
|
||||
})
|
||||
|
||||
go cache.WaitForNamedCacheSync("node_authorizer", wait.NeverStop,
|
||||
podHandler.HasSynced, pvsHandler.HasSynced, attachHandler.HasSynced)
|
||||
synced := []cache.InformerSynced{
|
||||
podHandler.HasSynced, pvsHandler.HasSynced, attachHandler.HasSynced,
|
||||
}
|
||||
|
||||
if slices != nil {
|
||||
sliceHandler, _ := slices.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: g.addNodeResourceSlice,
|
||||
UpdateFunc: nil, // Not needed, NodeName is immutable.
|
||||
DeleteFunc: g.deleteNodeResourceSlice,
|
||||
})
|
||||
synced = append(synced, sliceHandler.HasSynced)
|
||||
}
|
||||
|
||||
go cache.WaitForNamedCacheSync("node_authorizer", wait.NeverStop, synced...)
|
||||
}
|
||||
|
||||
func (g *graphPopulator) addPod(obj interface{}) {
|
||||
@@ -184,3 +199,24 @@ func (g *graphPopulator) deleteVolumeAttachment(obj interface{}) {
|
||||
}
|
||||
g.graph.DeleteVolumeAttachment(attachment.Name)
|
||||
}
|
||||
|
||||
func (g *graphPopulator) addNodeResourceSlice(obj interface{}) {
|
||||
slice, ok := obj.(*resourcev1alpha2.NodeResourceSlice)
|
||||
if !ok {
|
||||
klog.Infof("unexpected type %T", obj)
|
||||
return
|
||||
}
|
||||
g.graph.AddNodeResourceSlice(slice.Name, slice.NodeName)
|
||||
}
|
||||
|
||||
func (g *graphPopulator) deleteNodeResourceSlice(obj interface{}) {
|
||||
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
|
||||
obj = tombstone.Obj
|
||||
}
|
||||
slice, ok := obj.(*resourcev1alpha2.NodeResourceSlice)
|
||||
if !ok {
|
||||
klog.Infof("unexpected type %T", obj)
|
||||
return
|
||||
}
|
||||
g.graph.DeleteNodeResourceSlice(slice.Name)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,12 @@ import (
|
||||
// node <- pod <- pvc <- pv
|
||||
// node <- pod <- pvc <- pv <- secret
|
||||
// node <- pod <- ResourceClaim
|
||||
// 4. For other resources, authorize all nodes uniformly using statically defined rules
|
||||
// 4. If a request is for a noderesourceslice, then authorize access if there is an
|
||||
// edge from the existing slice object to the node, which is the case if the
|
||||
// existing object has the node in its NodeName field. For create, the access gets
|
||||
// granted because the noderestriction admission plugin checks that the NodeName
|
||||
// is set to the node.
|
||||
// 5. For other resources, authorize all nodes uniformly using statically defined rules
|
||||
type NodeAuthorizer struct {
|
||||
graph *Graph
|
||||
identifier nodeidentifier.NodeIdentifier
|
||||
@@ -76,6 +81,7 @@ func NewAuthorizer(graph *Graph, identifier nodeidentifier.NodeIdentifier, rules
|
||||
var (
|
||||
configMapResource = api.Resource("configmaps")
|
||||
secretResource = api.Resource("secrets")
|
||||
nodeResourceSlice = resourceapi.Resource("noderesourceslices")
|
||||
pvcResource = api.Resource("persistentvolumeclaims")
|
||||
pvResource = api.Resource("persistentvolumes")
|
||||
resourceClaimResource = resourceapi.Resource("resourceclaims")
|
||||
@@ -130,6 +136,8 @@ func (r *NodeAuthorizer) Authorize(ctx context.Context, attrs authorizer.Attribu
|
||||
return r.authorizeLease(nodeName, attrs)
|
||||
case csiNodeResource:
|
||||
return r.authorizeCSINode(nodeName, attrs)
|
||||
case nodeResourceSlice:
|
||||
return r.authorizeNodeResourceSlice(nodeName, attrs)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -294,6 +302,39 @@ func (r *NodeAuthorizer) authorizeCSINode(nodeName string, attrs authorizer.Attr
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
// authorizeNodeResourceSlice authorizes node requests to NodeResourceSlice resource.k8s.io/noderesourceslices
|
||||
func (r *NodeAuthorizer) authorizeNodeResourceSlice(nodeName string, attrs authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
if len(attrs.GetSubresource()) > 0 {
|
||||
klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs)
|
||||
return authorizer.DecisionNoOpinion, "cannot authorize NodeResourceSlice subresources", nil
|
||||
}
|
||||
|
||||
// allowed verbs: get, create, update, patch, delete
|
||||
verb := attrs.GetVerb()
|
||||
switch verb {
|
||||
case "get", "create", "update", "patch", "delete":
|
||||
// Okay, but check individual object permission below.
|
||||
case "watch", "list":
|
||||
// Okay. The kubelet is trusted to use a filter for its own objects.
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
default:
|
||||
klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs)
|
||||
return authorizer.DecisionNoOpinion, "can only get, create, update, patch, or delete a NodeResourceSlice", nil
|
||||
}
|
||||
|
||||
// The request must come from a node with the same name as the NodeResourceSlice.NodeName field.
|
||||
//
|
||||
// For create, the noderestriction admission plugin is performing this check.
|
||||
// Here we don't have access to the content of the new object.
|
||||
if verb == "create" {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
// For any other verb, checking the existing object must have established that access
|
||||
// is allowed by recording a graph edge.
|
||||
return r.authorize(nodeName, sliceVertexType, attrs)
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
resourcev1alpha2 "k8s.io/api/resource/v1alpha2"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
@@ -55,9 +56,10 @@ func TestAuthorizer(t *testing.T) {
|
||||
uniqueResourceClaimsPerPod: 1,
|
||||
uniqueResourceClaimTemplatesPerPod: 1,
|
||||
uniqueResourceClaimTemplatesWithClaimPerPod: 1,
|
||||
nodeResourceCapacitiesPerNode: 2,
|
||||
}
|
||||
nodes, pods, pvs, attachments := generate(opts)
|
||||
populate(g, nodes, pods, pvs, attachments)
|
||||
nodes, pods, pvs, attachments, slices := generate(opts)
|
||||
populate(g, nodes, pods, pvs, attachments, slices)
|
||||
|
||||
identifier := nodeidentifier.NewDefaultNodeIdentifier()
|
||||
authz := NewAuthorizer(g, identifier, bootstrappolicy.NodeRules())
|
||||
@@ -336,6 +338,67 @@ func TestAuthorizer(t *testing.T) {
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "csinodes", APIGroup: "storage.k8s.io", Name: "node0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
// NodeResourceSlice
|
||||
{
|
||||
name: "disallowed NodeResourceSlice with subresource",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "noderesourceslices", Subresource: "status", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
},
|
||||
{
|
||||
name: "disallowed get another node's NodeResourceSlice",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "noderesourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node1"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
},
|
||||
{
|
||||
name: "disallowed update another node's NodeResourceSlice",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "noderesourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node1"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
},
|
||||
{
|
||||
name: "disallowed patch another node's NodeResourceSlice",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "noderesourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node1"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
},
|
||||
{
|
||||
name: "disallowed delete another node's NodeResourceSlice",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "noderesourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node1"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
},
|
||||
{
|
||||
name: "allowed list NodeResourceSlices",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "noderesourceslices", APIGroup: "resource.k8s.io"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "allowed watch NodeResourceSlices",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "noderesourceslices", APIGroup: "resource.k8s.io"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "allowed get NodeResourceSlice",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "noderesourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "allowed create NodeResourceSlice",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "noderesourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "allowed update NodeResourceSlice",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "noderesourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "allowed patch NodeResourceSlice",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "noderesourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "allowed delete NodeResourceSlice",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "noderesourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@@ -497,6 +560,8 @@ type sampleDataOpts struct {
|
||||
uniqueResourceClaimsPerPod int
|
||||
uniqueResourceClaimTemplatesPerPod int
|
||||
uniqueResourceClaimTemplatesWithClaimPerPod int
|
||||
|
||||
nodeResourceCapacitiesPerNode int
|
||||
}
|
||||
|
||||
func BenchmarkPopulationAllocation(b *testing.B) {
|
||||
@@ -513,12 +578,12 @@ func BenchmarkPopulationAllocation(b *testing.B) {
|
||||
uniquePVCsPerPod: 1,
|
||||
}
|
||||
|
||||
nodes, pods, pvs, attachments := generate(opts)
|
||||
nodes, pods, pvs, attachments, slices := generate(opts)
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
g := NewGraph()
|
||||
populate(g, nodes, pods, pvs, attachments)
|
||||
populate(g, nodes, pods, pvs, attachments, slices)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,14 +609,14 @@ func BenchmarkPopulationRetention(b *testing.B) {
|
||||
uniquePVCsPerPod: 1,
|
||||
}
|
||||
|
||||
nodes, pods, pvs, attachments := generate(opts)
|
||||
nodes, pods, pvs, attachments, slices := generate(opts)
|
||||
// Garbage collect before the first iteration
|
||||
runtime.GC()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
g := NewGraph()
|
||||
populate(g, nodes, pods, pvs, attachments)
|
||||
populate(g, nodes, pods, pvs, attachments, slices)
|
||||
|
||||
if i == 0 {
|
||||
f, _ := os.Create("BenchmarkPopulationRetention.profile")
|
||||
@@ -582,9 +647,9 @@ func BenchmarkWriteIndexMaintenance(b *testing.B) {
|
||||
sharedPVCsPerPod: 0,
|
||||
uniquePVCsPerPod: 1,
|
||||
}
|
||||
nodes, pods, pvs, attachments := generate(opts)
|
||||
nodes, pods, pvs, attachments, slices := generate(opts)
|
||||
g := NewGraph()
|
||||
populate(g, nodes, pods, pvs, attachments)
|
||||
populate(g, nodes, pods, pvs, attachments, slices)
|
||||
// Garbage collect before the first iteration
|
||||
runtime.GC()
|
||||
b.ResetTimer()
|
||||
@@ -616,8 +681,8 @@ func BenchmarkAuthorization(b *testing.B) {
|
||||
sharedPVCsPerPod: 0,
|
||||
uniquePVCsPerPod: 1,
|
||||
}
|
||||
nodes, pods, pvs, attachments := generate(opts)
|
||||
populate(g, nodes, pods, pvs, attachments)
|
||||
nodes, pods, pvs, attachments, slices := generate(opts)
|
||||
populate(g, nodes, pods, pvs, attachments, slices)
|
||||
|
||||
identifier := nodeidentifier.NewDefaultNodeIdentifier()
|
||||
authz := NewAuthorizer(g, identifier, bootstrappolicy.NodeRules())
|
||||
@@ -766,7 +831,7 @@ func BenchmarkAuthorization(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func populate(graph *Graph, nodes []*corev1.Node, pods []*corev1.Pod, pvs []*corev1.PersistentVolume, attachments []*storagev1.VolumeAttachment) {
|
||||
func populate(graph *Graph, nodes []*corev1.Node, pods []*corev1.Pod, pvs []*corev1.PersistentVolume, attachments []*storagev1.VolumeAttachment, slices []*resourcev1alpha2.NodeResourceSlice) {
|
||||
p := &graphPopulator{}
|
||||
p.graph = graph
|
||||
for _, pod := range pods {
|
||||
@@ -778,6 +843,9 @@ func populate(graph *Graph, nodes []*corev1.Node, pods []*corev1.Pod, pvs []*cor
|
||||
for _, attachment := range attachments {
|
||||
p.addVolumeAttachment(attachment)
|
||||
}
|
||||
for _, slice := range slices {
|
||||
p.addNodeResourceSlice(slice)
|
||||
}
|
||||
}
|
||||
|
||||
func randomSubset(a, b int) []int {
|
||||
@@ -791,11 +859,12 @@ func randomSubset(a, b int) []int {
|
||||
// the secret/configmap/pvc/node references in the pod and pv objects are named to indicate the connections between the objects.
|
||||
// for example, secret0-pod0-node0 is a secret referenced by pod0 which is bound to node0.
|
||||
// when populated into the graph, the node authorizer should allow node0 to access that secret, but not node1.
|
||||
func generate(opts *sampleDataOpts) ([]*corev1.Node, []*corev1.Pod, []*corev1.PersistentVolume, []*storagev1.VolumeAttachment) {
|
||||
func generate(opts *sampleDataOpts) ([]*corev1.Node, []*corev1.Pod, []*corev1.PersistentVolume, []*storagev1.VolumeAttachment, []*resourcev1alpha2.NodeResourceSlice) {
|
||||
nodes := make([]*corev1.Node, 0, opts.nodes)
|
||||
pods := make([]*corev1.Pod, 0, opts.nodes*opts.podsPerNode)
|
||||
pvs := make([]*corev1.PersistentVolume, 0, (opts.nodes*opts.podsPerNode*opts.uniquePVCsPerPod)+(opts.sharedPVCsPerPod*opts.namespaces))
|
||||
attachments := make([]*storagev1.VolumeAttachment, 0, opts.nodes*opts.attachmentsPerNode)
|
||||
slices := make([]*resourcev1alpha2.NodeResourceSlice, 0, opts.nodes*opts.nodeResourceCapacitiesPerNode)
|
||||
|
||||
rand.Seed(12345)
|
||||
|
||||
@@ -821,8 +890,17 @@ func generate(opts *sampleDataOpts) ([]*corev1.Node, []*corev1.Pod, []*corev1.Pe
|
||||
ObjectMeta: metav1.ObjectMeta{Name: nodeName},
|
||||
Spec: corev1.NodeSpec{},
|
||||
})
|
||||
|
||||
for p := 0; p <= opts.nodeResourceCapacitiesPerNode; p++ {
|
||||
name := fmt.Sprintf("slice%d-%s", p, nodeName)
|
||||
slice := &resourcev1alpha2.NodeResourceSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name},
|
||||
NodeName: nodeName,
|
||||
}
|
||||
slices = append(slices, slice)
|
||||
}
|
||||
}
|
||||
return nodes, pods, pvs, attachments
|
||||
return nodes, pods, pvs, attachments, slices
|
||||
}
|
||||
|
||||
func generatePod(name, namespace, nodeName, svcAccountName string, opts *sampleDataOpts) (*corev1.Pod, []*corev1.PersistentVolume) {
|
||||
|
||||
Reference in New Issue
Block a user