diff --git a/authz/providers/azure/rbac/checkaccessreqhelper.go b/authz/providers/azure/rbac/checkaccessreqhelper.go index 91f3d363..aea5e93c 100644 --- a/authz/providers/azure/rbac/checkaccessreqhelper.go +++ b/authz/providers/azure/rbac/checkaccessreqhelper.go @@ -49,7 +49,13 @@ const ( NonAADUserNotAllowedVerdict = "Access denied by Azure RBAC for non AAD users. Configure --azure.skip-authz-for-non-aad-users to enable access. If you are an AAD user, please set Extra:oid parameter for impersonated user in the kubeconfig." CheckAccessErrorVerdict = "Access denied due to Azure RBAC check failure. Please retry later." PodsResource = "pods" + ServicesResource = "services" + NodesResource = "nodes" CustomResources = "customresources" + ProxySubresource = "proxy" + AttachSubresource = "attach" + PortForwardSubresource = "portforward" + ExecSubresource = "exec" ReadVerb = "read" WriteVerb = "write" DeleteVerb = "delete" @@ -248,16 +254,40 @@ func getActionName(verb string) string { } } -func getResourceAndAction(resource string, subResource string, verb string) string { - var action string +// securitySensitiveSubresources lists resource/subresource pairs that upstream +// Kubernetes treats as distinct authorization targets. For these, the +// subresource is preserved in the DataAction string +// ("//action") rather than collapsed into the base +// resource action, so the authorization decision keeps the same granularity as +// the upstream Kubernetes RBAC model (see the bootstrappolicy view/edit +// ClusterRoles). +// +// The pods exec/attach/portforward/proxy, services/proxy and nodes/proxy +// subresources are granted separately from base read/write in the upstream +// roles, so they are mapped to their own DataAction rather than to +// /read or /write. +var securitySensitiveSubresources = map[string]map[string]struct{}{ + PodsResource: { + ExecSubresource: {}, + AttachSubresource: {}, + PortForwardSubresource: {}, + ProxySubresource: {}, + }, + ServicesResource: { + ProxySubresource: {}, + }, + NodesResource: { + ProxySubresource: {}, + }, +} - if resource == PodsResource && subResource == "exec" { - action = path.Join(resource, subResource, "action") - } else { - action = path.Join(resource, getActionName(verb)) +func getResourceAndAction(resource string, subResource string, verb string) string { + if subs, ok := securitySensitiveSubresources[resource]; ok && subResource != "" { + if _, sensitive := subs[subResource]; sensitive { + return path.Join(resource, subResource, "action") + } } - - return action + return path.Join(resource, getActionName(verb)) } func getDataActions(ctx context.Context, subRevReq *authzv1.SubjectAccessReviewSpec, clusterType string, allowCustomResourceTypeCheck bool, allowSubresourceTypeCheck bool) ([]azureutils.AuthorizationActionInfo, error) { diff --git a/authz/providers/azure/rbac/checkaccessreqhelper_test.go b/authz/providers/azure/rbac/checkaccessreqhelper_test.go index 988ddb80..36864b3c 100644 --- a/authz/providers/azure/rbac/checkaccessreqhelper_test.go +++ b/authz/providers/azure/rbac/checkaccessreqhelper_test.go @@ -417,6 +417,116 @@ func Test_getDataActions(t *testing.T) { []azureutils.AuthorizationActionInfo{{AuthorizationEntity: azureutils.AuthorizationEntity{Id: "fleet/pods/exec/action"}, IsDataAction: true}}, }, + // pods/proxy, services/proxy and nodes/proxy must produce a distinct + // DataAction so that a GET on the proxy subresource does NOT collapse + // onto /read, keeping the same granularity as the upstream + // Kubernetes RBAC model. + { + "podsProxyGetAKS", + args{ + isWildcardTest: false, + subRevReq: &authzv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authzv1.ResourceAttributes{Group: "", Resource: "pods", Subresource: "proxy", Version: "v1", Name: "test", Verb: "get"}, + }, clusterType: aksClusterType, + }, + []azureutils.AuthorizationActionInfo{{AuthorizationEntity: azureutils.AuthorizationEntity{Id: "aks/pods/proxy/action"}, IsDataAction: true}}, + }, + + { + "podsProxyGetFleet", + args{ + isWildcardTest: false, + subRevReq: &authzv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authzv1.ResourceAttributes{Group: "", Resource: "pods", Subresource: "proxy", Version: "v1", Name: "test", Verb: "get"}, + }, clusterType: "fleet", + }, + []azureutils.AuthorizationActionInfo{{AuthorizationEntity: azureutils.AuthorizationEntity{Id: "fleet/pods/proxy/action"}, IsDataAction: true}}, + }, + + // pods/attach and pods/portforward are distinct authorization targets + // in upstream Kubernetes (bootstrappolicy edit role) and must map to + // their own DataAction rather than collapsing onto pods/read. + { + "podsAttachGetAKS", + args{ + isWildcardTest: false, + subRevReq: &authzv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authzv1.ResourceAttributes{Group: "", Resource: "pods", Subresource: "attach", Version: "v1", Name: "test", Verb: "get"}, + }, clusterType: aksClusterType, + }, + []azureutils.AuthorizationActionInfo{{AuthorizationEntity: azureutils.AuthorizationEntity{Id: "aks/pods/attach/action"}, IsDataAction: true}}, + }, + + { + "podsPortForwardGetAKS", + args{ + isWildcardTest: false, + subRevReq: &authzv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authzv1.ResourceAttributes{Group: "", Resource: "pods", Subresource: "portforward", Version: "v1", Name: "test", Verb: "get"}, + }, clusterType: aksClusterType, + }, + []azureutils.AuthorizationActionInfo{{AuthorizationEntity: azureutils.AuthorizationEntity{Id: "aks/pods/portforward/action"}, IsDataAction: true}}, + }, + + { + "podsPortForwardGetFleet", + args{ + isWildcardTest: false, + subRevReq: &authzv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authzv1.ResourceAttributes{Group: "", Resource: "pods", Subresource: "portforward", Version: "v1", Name: "test", Verb: "get"}, + }, clusterType: "fleet", + }, + []azureutils.AuthorizationActionInfo{{AuthorizationEntity: azureutils.AuthorizationEntity{Id: "fleet/pods/portforward/action"}, IsDataAction: true}}, + }, + + { + "servicesProxyGetAKS", + args{ + isWildcardTest: false, + subRevReq: &authzv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authzv1.ResourceAttributes{Group: "", Resource: "services", Subresource: "proxy", Version: "v1", Name: "test", Verb: "get"}, + }, clusterType: aksClusterType, + }, + []azureutils.AuthorizationActionInfo{{AuthorizationEntity: azureutils.AuthorizationEntity{Id: "aks/services/proxy/action"}, IsDataAction: true}}, + }, + + { + "nodesProxyGetAKS", + args{ + isWildcardTest: false, + subRevReq: &authzv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authzv1.ResourceAttributes{Group: "", Resource: "nodes", Subresource: "proxy", Version: "v1", Name: "test", Verb: "get"}, + }, clusterType: aksClusterType, + }, + []azureutils.AuthorizationActionInfo{{AuthorizationEntity: azureutils.AuthorizationEntity{Id: "aks/nodes/proxy/action"}, IsDataAction: true}}, + }, + + // A non-sensitive pods subresource (e.g. status) must still collapse to + // the base read action so this fix does not over-restrict legitimate reads. + { + "podsStatusSubresourceStillCollapsed", + args{ + isWildcardTest: false, + subRevReq: &authzv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authzv1.ResourceAttributes{Group: "", Resource: "pods", Subresource: "status", Version: "v1", Name: "test", Verb: "get"}, + }, clusterType: aksClusterType, + }, + []azureutils.AuthorizationActionInfo{{AuthorizationEntity: azureutils.AuthorizationEntity{Id: "aks/pods/read"}, IsDataAction: true}}, + }, + + // CSR with a non-sensitive subresource (e.g. approval, status) + // should still collapse to the base action, not preserve the subresource. + { + "csrApprovalSubresourceStillCollapsed", + args{ + isWildcardTest: false, + subRevReq: &authzv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authzv1.ResourceAttributes{Group: "certificates.k8s.io", Resource: "certificatesigningrequests", Subresource: "approval", Version: "v1", Name: "test", Verb: "update"}, + }, clusterType: aksClusterType, + }, + []azureutils.AuthorizationActionInfo{{AuthorizationEntity: azureutils.AuthorizationEntity{Id: "aks/certificates.k8s.io/certificatesigningrequests/write"}, IsDataAction: true}}, + }, + { "allStar", args{