k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/plugin/pkg/auth/authorizer/node/node_authorizer.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package node 18 19 import ( 20 "context" 21 "fmt" 22 23 "k8s.io/klog/v2" 24 25 rbacv1 "k8s.io/api/rbac/v1" 26 "k8s.io/apimachinery/pkg/runtime/schema" 27 "k8s.io/apiserver/pkg/authentication/user" 28 "k8s.io/apiserver/pkg/authorization/authorizer" 29 utilfeature "k8s.io/apiserver/pkg/util/feature" 30 "k8s.io/component-base/featuregate" 31 coordapi "k8s.io/kubernetes/pkg/apis/coordination" 32 api "k8s.io/kubernetes/pkg/apis/core" 33 resourceapi "k8s.io/kubernetes/pkg/apis/resource" 34 storageapi "k8s.io/kubernetes/pkg/apis/storage" 35 "k8s.io/kubernetes/pkg/auth/nodeidentifier" 36 "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac" 37 "k8s.io/kubernetes/third_party/forked/gonum/graph" 38 "k8s.io/kubernetes/third_party/forked/gonum/graph/traverse" 39 ) 40 41 // NodeAuthorizer authorizes requests from kubelets, with the following logic: 42 // 1. If a request is not from a node (NodeIdentity() returns isNode=false), reject 43 // 2. If a specific node cannot be identified (NodeIdentity() returns nodeName=""), reject 44 // 3. If a request is for a secret, configmap, persistent volume, resource claim, or persistent volume claim, reject unless the verb is get, and the requested object is related to the requesting node: 45 // node <- configmap 46 // node <- pod 47 // node <- pod <- secret 48 // node <- pod <- configmap 49 // node <- pod <- pvc 50 // node <- pod <- pvc <- pv 51 // node <- pod <- pvc <- pv <- secret 52 // node <- pod <- ResourceClaim 53 // 4. If a request is for a resourceslice, then authorize access if there is an 54 // edge from the existing slice object to the node, which is the case if the 55 // existing object has the node in its NodeName field. For create, the access gets 56 // granted because the noderestriction admission plugin checks that the NodeName 57 // is set to the node. 58 // 5. For other resources, authorize all nodes uniformly using statically defined rules 59 type NodeAuthorizer struct { 60 graph *Graph 61 identifier nodeidentifier.NodeIdentifier 62 nodeRules []rbacv1.PolicyRule 63 64 // allows overriding for testing 65 features featuregate.FeatureGate 66 } 67 68 var _ = authorizer.Authorizer(&NodeAuthorizer{}) 69 var _ = authorizer.RuleResolver(&NodeAuthorizer{}) 70 71 // NewAuthorizer returns a new node authorizer 72 func NewAuthorizer(graph *Graph, identifier nodeidentifier.NodeIdentifier, rules []rbacv1.PolicyRule) *NodeAuthorizer { 73 return &NodeAuthorizer{ 74 graph: graph, 75 identifier: identifier, 76 nodeRules: rules, 77 features: utilfeature.DefaultFeatureGate, 78 } 79 } 80 81 var ( 82 configMapResource = api.Resource("configmaps") 83 secretResource = api.Resource("secrets") 84 resourceSlice = resourceapi.Resource("resourceslices") 85 pvcResource = api.Resource("persistentvolumeclaims") 86 pvResource = api.Resource("persistentvolumes") 87 resourceClaimResource = resourceapi.Resource("resourceclaims") 88 vaResource = storageapi.Resource("volumeattachments") 89 svcAcctResource = api.Resource("serviceaccounts") 90 leaseResource = coordapi.Resource("leases") 91 csiNodeResource = storageapi.Resource("csinodes") 92 ) 93 94 func (r *NodeAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { 95 if _, isNode := r.identifier.NodeIdentity(user); isNode { 96 // indicate nodes do not have fully enumerated permissions 97 return nil, nil, true, fmt.Errorf("node authorizer does not support user rule resolution") 98 } 99 return nil, nil, false, nil 100 } 101 102 func (r *NodeAuthorizer) Authorize(ctx context.Context, attrs authorizer.Attributes) (authorizer.Decision, string, error) { 103 nodeName, isNode := r.identifier.NodeIdentity(attrs.GetUser()) 104 if !isNode { 105 // reject requests from non-nodes 106 return authorizer.DecisionNoOpinion, "", nil 107 } 108 if len(nodeName) == 0 { 109 // reject requests from unidentifiable nodes 110 klog.V(2).Infof("NODE DENY: unknown node for user %q", attrs.GetUser().GetName()) 111 return authorizer.DecisionNoOpinion, fmt.Sprintf("unknown node for user %q", attrs.GetUser().GetName()), nil 112 } 113 114 // subdivide access to specific resources 115 if attrs.IsResourceRequest() { 116 requestResource := schema.GroupResource{Group: attrs.GetAPIGroup(), Resource: attrs.GetResource()} 117 switch requestResource { 118 case secretResource: 119 return r.authorizeReadNamespacedObject(nodeName, secretVertexType, attrs) 120 case configMapResource: 121 return r.authorizeReadNamespacedObject(nodeName, configMapVertexType, attrs) 122 case pvcResource: 123 if attrs.GetSubresource() == "status" { 124 return r.authorizeStatusUpdate(nodeName, pvcVertexType, attrs) 125 } 126 return r.authorizeGet(nodeName, pvcVertexType, attrs) 127 case pvResource: 128 return r.authorizeGet(nodeName, pvVertexType, attrs) 129 case resourceClaimResource: 130 return r.authorizeGet(nodeName, resourceClaimVertexType, attrs) 131 case vaResource: 132 return r.authorizeGet(nodeName, vaVertexType, attrs) 133 case svcAcctResource: 134 return r.authorizeCreateToken(nodeName, serviceAccountVertexType, attrs) 135 case leaseResource: 136 return r.authorizeLease(nodeName, attrs) 137 case csiNodeResource: 138 return r.authorizeCSINode(nodeName, attrs) 139 case resourceSlice: 140 return r.authorizeResourceSlice(nodeName, attrs) 141 } 142 143 } 144 145 // Access to other resources is not subdivided, so just evaluate against the statically defined node rules 146 if rbac.RulesAllow(attrs, r.nodeRules...) { 147 return authorizer.DecisionAllow, "", nil 148 } 149 return authorizer.DecisionNoOpinion, "", nil 150 } 151 152 // authorizeStatusUpdate authorizes get/update/patch requests to status subresources of the specified type if they are related to the specified node 153 func (r *NodeAuthorizer) authorizeStatusUpdate(nodeName string, startingType vertexType, attrs authorizer.Attributes) (authorizer.Decision, string, error) { 154 switch attrs.GetVerb() { 155 case "update", "patch": 156 // ok 157 default: 158 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 159 return authorizer.DecisionNoOpinion, "can only get/update/patch this type", nil 160 } 161 162 if attrs.GetSubresource() != "status" { 163 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 164 return authorizer.DecisionNoOpinion, "can only update status subresource", nil 165 } 166 167 return r.authorize(nodeName, startingType, attrs) 168 } 169 170 // authorizeGet authorizes "get" requests to objects of the specified type if they are related to the specified node 171 func (r *NodeAuthorizer) authorizeGet(nodeName string, startingType vertexType, attrs authorizer.Attributes) (authorizer.Decision, string, error) { 172 if attrs.GetVerb() != "get" { 173 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 174 return authorizer.DecisionNoOpinion, "can only get individual resources of this type", nil 175 } 176 if len(attrs.GetSubresource()) > 0 { 177 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 178 return authorizer.DecisionNoOpinion, "cannot get subresource", nil 179 } 180 return r.authorize(nodeName, startingType, attrs) 181 } 182 183 // authorizeReadNamespacedObject authorizes "get", "list" and "watch" requests to single objects of a 184 // specified types if they are related to the specified node. 185 func (r *NodeAuthorizer) authorizeReadNamespacedObject(nodeName string, startingType vertexType, attrs authorizer.Attributes) (authorizer.Decision, string, error) { 186 switch attrs.GetVerb() { 187 case "get", "list", "watch": 188 //ok 189 default: 190 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 191 return authorizer.DecisionNoOpinion, "can only read resources of this type", nil 192 } 193 194 if len(attrs.GetSubresource()) > 0 { 195 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 196 return authorizer.DecisionNoOpinion, "cannot read subresource", nil 197 } 198 if len(attrs.GetNamespace()) == 0 { 199 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 200 return authorizer.DecisionNoOpinion, "can only read namespaced object of this type", nil 201 } 202 return r.authorize(nodeName, startingType, attrs) 203 } 204 205 func (r *NodeAuthorizer) authorize(nodeName string, startingType vertexType, attrs authorizer.Attributes) (authorizer.Decision, string, error) { 206 if len(attrs.GetName()) == 0 { 207 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 208 return authorizer.DecisionNoOpinion, "No Object name found", nil 209 } 210 211 ok, err := r.hasPathFrom(nodeName, startingType, attrs.GetNamespace(), attrs.GetName()) 212 if err != nil { 213 klog.V(2).InfoS("NODE DENY", "err", err) 214 return authorizer.DecisionNoOpinion, fmt.Sprintf("no relationship found between node '%s' and this object", nodeName), nil 215 } 216 if !ok { 217 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 218 return authorizer.DecisionNoOpinion, fmt.Sprintf("no relationship found between node '%s' and this object", nodeName), nil 219 } 220 return authorizer.DecisionAllow, "", nil 221 } 222 223 // authorizeCreateToken authorizes "create" requests to serviceaccounts 'token' 224 // subresource of pods running on a node 225 func (r *NodeAuthorizer) authorizeCreateToken(nodeName string, startingType vertexType, attrs authorizer.Attributes) (authorizer.Decision, string, error) { 226 if attrs.GetVerb() != "create" || len(attrs.GetName()) == 0 { 227 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 228 return authorizer.DecisionNoOpinion, "can only create tokens for individual service accounts", nil 229 } 230 231 if attrs.GetSubresource() != "token" { 232 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 233 return authorizer.DecisionNoOpinion, "can only create token subresource of serviceaccount", nil 234 } 235 236 ok, err := r.hasPathFrom(nodeName, startingType, attrs.GetNamespace(), attrs.GetName()) 237 if err != nil { 238 klog.V(2).Infof("NODE DENY: %v", err) 239 return authorizer.DecisionNoOpinion, fmt.Sprintf("no relationship found between node '%s' and this object", nodeName), nil 240 } 241 if !ok { 242 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 243 return authorizer.DecisionNoOpinion, fmt.Sprintf("no relationship found between node '%s' and this object", nodeName), nil 244 } 245 return authorizer.DecisionAllow, "", nil 246 } 247 248 // authorizeLease authorizes node requests to coordination.k8s.io/leases. 249 func (r *NodeAuthorizer) authorizeLease(nodeName string, attrs authorizer.Attributes) (authorizer.Decision, string, error) { 250 // allowed verbs: get, create, update, patch, delete 251 verb := attrs.GetVerb() 252 switch verb { 253 case "get", "create", "update", "patch", "delete": 254 //ok 255 default: 256 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 257 return authorizer.DecisionNoOpinion, "can only get, create, update, patch, or delete a node lease", nil 258 } 259 260 // the request must be against the system namespace reserved for node leases 261 if attrs.GetNamespace() != api.NamespaceNodeLease { 262 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 263 return authorizer.DecisionNoOpinion, fmt.Sprintf("can only access leases in the %q system namespace", api.NamespaceNodeLease), nil 264 } 265 266 // the request must come from a node with the same name as the lease 267 // note we skip this check for create, since the authorizer doesn't know the name on create 268 // the noderestriction admission plugin is capable of performing this check at create time 269 if verb != "create" && attrs.GetName() != nodeName { 270 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 271 return authorizer.DecisionNoOpinion, "can only access node lease with the same name as the requesting node", nil 272 } 273 274 return authorizer.DecisionAllow, "", nil 275 } 276 277 // authorizeCSINode authorizes node requests to CSINode storage.k8s.io/csinodes 278 func (r *NodeAuthorizer) authorizeCSINode(nodeName string, attrs authorizer.Attributes) (authorizer.Decision, string, error) { 279 // allowed verbs: get, create, update, patch, delete 280 verb := attrs.GetVerb() 281 switch verb { 282 case "get", "create", "update", "patch", "delete": 283 //ok 284 default: 285 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 286 return authorizer.DecisionNoOpinion, "can only get, create, update, patch, or delete a CSINode", nil 287 } 288 289 if len(attrs.GetSubresource()) > 0 { 290 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 291 return authorizer.DecisionNoOpinion, "cannot authorize CSINode subresources", nil 292 } 293 294 // the request must come from a node with the same name as the CSINode 295 // note we skip this check for create, since the authorizer doesn't know the name on create 296 // the noderestriction admission plugin is capable of performing this check at create time 297 if verb != "create" && attrs.GetName() != nodeName { 298 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 299 return authorizer.DecisionNoOpinion, "can only access CSINode with the same name as the requesting node", nil 300 } 301 302 return authorizer.DecisionAllow, "", nil 303 } 304 305 // authorizeResourceSlice authorizes node requests to ResourceSlice resource.k8s.io/resourceslices 306 func (r *NodeAuthorizer) authorizeResourceSlice(nodeName string, attrs authorizer.Attributes) (authorizer.Decision, string, error) { 307 if len(attrs.GetSubresource()) > 0 { 308 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 309 return authorizer.DecisionNoOpinion, "cannot authorize ResourceSlice subresources", nil 310 } 311 312 // allowed verbs: get, create, update, patch, delete 313 verb := attrs.GetVerb() 314 switch verb { 315 case "get", "create", "update", "patch", "delete": 316 // Okay, but check individual object permission below. 317 case "watch", "list": 318 // Okay. The kubelet is trusted to use a filter for its own objects. 319 return authorizer.DecisionAllow, "", nil 320 default: 321 klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs) 322 return authorizer.DecisionNoOpinion, "can only get, create, update, patch, or delete a ResourceSlice", nil 323 } 324 325 // The request must come from a node with the same name as the ResourceSlice.NodeName field. 326 // 327 // For create, the noderestriction admission plugin is performing this check. 328 // Here we don't have access to the content of the new object. 329 if verb == "create" { 330 return authorizer.DecisionAllow, "", nil 331 } 332 333 // For any other verb, checking the existing object must have established that access 334 // is allowed by recording a graph edge. 335 return r.authorize(nodeName, sliceVertexType, attrs) 336 } 337 338 // hasPathFrom returns true if there is a directed path from the specified type/namespace/name to the specified Node 339 func (r *NodeAuthorizer) hasPathFrom(nodeName string, startingType vertexType, startingNamespace, startingName string) (bool, error) { 340 r.graph.lock.RLock() 341 defer r.graph.lock.RUnlock() 342 343 nodeVertex, exists := r.graph.getVertex_rlocked(nodeVertexType, "", nodeName) 344 if !exists { 345 return false, fmt.Errorf("unknown node '%s' cannot get %s %s/%s", nodeName, vertexTypes[startingType], startingNamespace, startingName) 346 } 347 348 startingVertex, exists := r.graph.getVertex_rlocked(startingType, startingNamespace, startingName) 349 if !exists { 350 return false, fmt.Errorf("node '%s' cannot get unknown %s %s/%s", nodeName, vertexTypes[startingType], startingNamespace, startingName) 351 } 352 353 // Fast check to see if we know of a destination edge 354 if r.graph.destinationEdgeIndex[startingVertex.ID()].has(nodeVertex.ID()) { 355 return true, nil 356 } 357 358 found := false 359 traversal := &traverse.VisitingDepthFirst{ 360 EdgeFilter: func(edge graph.Edge) bool { 361 if destinationEdge, ok := edge.(*destinationEdge); ok { 362 if destinationEdge.DestinationID() != nodeVertex.ID() { 363 // Don't follow edges leading to other nodes 364 return false 365 } 366 // We found an edge leading to the node we want 367 found = true 368 } 369 // Visit this edge 370 return true 371 }, 372 } 373 traversal.Walk(r.graph.graph, startingVertex, func(n graph.Node) bool { 374 if n.ID() == nodeVertex.ID() { 375 // We found the node we want 376 found = true 377 } 378 // Stop visiting if we've found the node we want 379 return found 380 }) 381 if !found { 382 return false, fmt.Errorf("node '%s' cannot get %s %s/%s, no relationship to this object was found in the node authorizer graph", nodeName, vertexTypes[startingType], startingNamespace, startingName) 383 } 384 return true, nil 385 }