diff --git a/avd_docs/kubernetes/general/AVD-KSV-0041/docs.md b/avd_docs/kubernetes/general/AVD-KSV-0041/docs.md index 2377040b0..7acf6da1b 100644 --- a/avd_docs/kubernetes/general/AVD-KSV-0041/docs.md +++ b/avd_docs/kubernetes/general/AVD-KSV-0041/docs.md @@ -1,5 +1,5 @@ -Check whether role permits managing secrets +Viewing secrets at the cluster-scope is akin to cluster-admin in most clusters as there are typically at least one service accounts (their token stored in a secret) bound to cluster-admin directly or a role/clusterrole that gives similar permissions. ### Impact diff --git a/avd_docs/kubernetes/general/AVD-KSV-0046/docs.md b/avd_docs/kubernetes/general/AVD-KSV-0046/docs.md index 8bc923402..4257a2aec 100644 --- a/avd_docs/kubernetes/general/AVD-KSV-0046/docs.md +++ b/avd_docs/kubernetes/general/AVD-KSV-0046/docs.md @@ -1,5 +1,5 @@ -Check whether role permits specific verb on wildcard resources +Full control of the cluster resources, and therefore also root on all nodes where workloads can run and has access to all pods, secrets, and data. ### Impact diff --git a/avd_docs/kubernetes/general/AVD-KSV-0112/docs.md b/avd_docs/kubernetes/general/AVD-KSV-0112/docs.md new file mode 100644 index 000000000..dac8568b4 --- /dev/null +++ b/avd_docs/kubernetes/general/AVD-KSV-0112/docs.md @@ -0,0 +1,13 @@ + +Full control of the resources within a namespace. In some cluster configurations, this is excessive. In others, this is normal (a gitops deployment operator like flux) + +### Impact + + + +{{ remediationActions }} + +### Links +- https://kubernetes.io/docs/concepts/security/rbac-good-practices/ + + diff --git a/avd_docs/kubernetes/general/AVD-KSV-0113/docs.md b/avd_docs/kubernetes/general/AVD-KSV-0113/docs.md new file mode 100644 index 000000000..4e6afa661 --- /dev/null +++ b/avd_docs/kubernetes/general/AVD-KSV-0113/docs.md @@ -0,0 +1,13 @@ + +Viewing secrets at the namespace scope can lead to escalation if another service account in that namespace has a higher privileged rolebinding or clusterrolebinding bound. + +### Impact + + + +{{ remediationActions }} + +### Links +- https://kubernetes.io/docs/concepts/security/rbac-good-practices/ + + diff --git a/avd_docs/kubernetes/general/AVD-KSV-0114/docs.md b/avd_docs/kubernetes/general/AVD-KSV-0114/docs.md new file mode 100644 index 000000000..e5f0d5a0e --- /dev/null +++ b/avd_docs/kubernetes/general/AVD-KSV-0114/docs.md @@ -0,0 +1,13 @@ + +Webhooks can silently intercept or actively mutate/block resources as they are being created or updated. This includes secrets and pod specs. + +### Impact + + + +{{ remediationActions }} + +### Links +- https://kubernetes.io/docs/concepts/security/rbac-good-practices/ + + diff --git a/avd_docs/kubernetes/general/AVD-KSV-0115/docs.md b/avd_docs/kubernetes/general/AVD-KSV-0115/docs.md new file mode 100644 index 000000000..aa50f17c0 --- /dev/null +++ b/avd_docs/kubernetes/general/AVD-KSV-0115/docs.md @@ -0,0 +1,13 @@ + +Ability to add AWS IAM to RBAC bindings via special EKS configmap. + +### Impact + + + +{{ remediationActions }} + +### Links +- https://kubernetes.io/docs/concepts/security/rbac-good-practices/ + + diff --git a/avd_docs/kubernetes/general/AVD-KSV-0116/docs.md b/avd_docs/kubernetes/general/AVD-KSV-0116/docs.md new file mode 100644 index 000000000..ef60ecb6a --- /dev/null +++ b/avd_docs/kubernetes/general/AVD-KSV-0116/docs.md @@ -0,0 +1,13 @@ + +According to pod security standard 'Non-root groups', containers should be forbidden from running with a root primary or supplementary GID. + +### Impact + + + +{{ remediationActions }} + +### Links +- https://kubesec.io/basics/containers-securitycontext-runasuser/ + + diff --git a/pkg/scanners/helm/test/scanner_test.go b/pkg/scanners/helm/test/scanner_test.go index 7998c4a79..dd4ed2b92 100644 --- a/pkg/scanners/helm/test/scanner_test.go +++ b/pkg/scanners/helm/test/scanner_test.go @@ -47,7 +47,7 @@ func Test_helm_scanner_with_archive(t *testing.T) { require.NotNil(t, results) failed := results.GetFailed() - assert.Equal(t, 13, len(failed)) + assert.Equal(t, 19, len(failed)) visited := make(map[string]bool) var errorCodes []string @@ -58,7 +58,7 @@ func Test_helm_scanner_with_archive(t *testing.T) { errorCodes = append(errorCodes, id) } } - assert.Len(t, errorCodes, 13) + assert.Len(t, errorCodes, 14) sort.Strings(errorCodes) @@ -67,7 +67,7 @@ func Test_helm_scanner_with_archive(t *testing.T) { "AVD-KSV-0011", "AVD-KSV-0012", "AVD-KSV-0014", "AVD-KSV-0015", "AVD-KSV-0016", "AVD-KSV-0018", "AVD-KSV-0020", "AVD-KSV-0021", "AVD-KSV-0030", - "AVD-KSV-0104", "AVD-KSV-0106", + "AVD-KSV-0104", "AVD-KSV-0106", "AVD-KSV-0116", }, errorCodes) } } @@ -127,7 +127,7 @@ func Test_helm_scanner_with_dir(t *testing.T) { require.NotNil(t, results) failed := results.GetFailed() - assert.Equal(t, 13, len(failed)) + assert.Equal(t, 16, len(failed)) visited := make(map[string]bool) var errorCodes []string @@ -146,7 +146,7 @@ func Test_helm_scanner_with_dir(t *testing.T) { "AVD-KSV-0011", "AVD-KSV-0012", "AVD-KSV-0014", "AVD-KSV-0015", "AVD-KSV-0016", "AVD-KSV-0018", "AVD-KSV-0020", "AVD-KSV-0021", "AVD-KSV-0030", - "AVD-KSV-0104", "AVD-KSV-0106", + "AVD-KSV-0104", "AVD-KSV-0106", "AVD-KSV-0116", }, errorCodes) } } @@ -213,7 +213,7 @@ deny[res] { require.NotNil(t, results) failed := results.GetFailed() - assert.Equal(t, 15, len(failed)) + assert.Equal(t, 21, len(failed)) visited := make(map[string]bool) var errorCodes []string @@ -224,7 +224,7 @@ deny[res] { errorCodes = append(errorCodes, id) } } - assert.Len(t, errorCodes, 14) + assert.Len(t, errorCodes, 15) sort.Strings(errorCodes) @@ -233,7 +233,7 @@ deny[res] { "AVD-KSV-0011", "AVD-KSV-0012", "AVD-KSV-0014", "AVD-KSV-0015", "AVD-KSV-0016", "AVD-KSV-0018", "AVD-KSV-0020", "AVD-KSV-0021", "AVD-KSV-0030", - "AVD-KSV-0104", "AVD-KSV-0106", "AVD-USR-ID001", + "AVD-KSV-0104", "AVD-KSV-0106", "AVD-KSV-0116", "AVD-USR-ID001", }, errorCodes) }) } diff --git a/rules/kubernetes/policies/general/any_resource.rego b/rules/kubernetes/policies/general/manage_all_resources.rego similarity index 59% rename from rules/kubernetes/policies/general/any_resource.rego rename to rules/kubernetes/policies/general/manage_all_resources.rego index d256aa664..e0cfca624 100644 --- a/rules/kubernetes/policies/general/any_resource.rego +++ b/rules/kubernetes/policies/general/manage_all_resources.rego @@ -1,6 +1,6 @@ # METADATA -# title: "No wildcard resource roles" -# description: "Check whether role permits specific verb on wildcard resources" +# title: "Manage all resources" +# description: "Full control of the cluster resources, and therefore also root on all nodes where workloads can run and has access to all pods, secrets, and data." # scope: package # schemas: # - input: schema["kubernetes"] @@ -10,8 +10,8 @@ # id: KSV046 # avd_id: AVD-KSV-0046 # severity: CRITICAL -# short_code: no-wildcard-resource-role -# recommended_action: "Create a role which does not permit specific verb on wildcard resources" +# short_code: no-wildcard-resource-clusterrole +# recommended_actions: "Remove '*' from 'rules.resources'. Provide specific list of resources to be managed by cluster role" # input: # selector: # - type: kubernetes @@ -22,7 +22,7 @@ import data.lib.utils readVerbs := ["create", "update", "delete", "deletecollection", "impersonate", "*", "list", "get"] -readKinds := ["Role", "ClusterRole"] +readKinds := ["ClusterRole"] resourceAllowSpecificVerbOnAnyResource[input.rules[ru]] { some ru, r, v @@ -33,6 +33,6 @@ resourceAllowSpecificVerbOnAnyResource[input.rules[ru]] { deny[res] { badRule := resourceAllowSpecificVerbOnAnyResource[_] - msg := "Role permits specific verb on wildcard resource" + msg := kubernetes.format(sprintf("%s '%s' shouldn't manage all resources", [kubernetes.kind, kubernetes.name])) res := result.new(msg, badRule) } diff --git a/rules/kubernetes/policies/general/manage_all_resources_at_namespace.rego b/rules/kubernetes/policies/general/manage_all_resources_at_namespace.rego new file mode 100644 index 000000000..a2c5d0a9a --- /dev/null +++ b/rules/kubernetes/policies/general/manage_all_resources_at_namespace.rego @@ -0,0 +1,38 @@ +# METADATA +# title: "Manage all resources at the namespace" +# description: "Full control of the resources within a namespace. In some cluster configurations, this is excessive. In others, this is normal (a gitops deployment operator like flux)" +# scope: package +# schemas: +# - input: schema["kubernetes"] +# related_resources: +# - https://kubernetes.io/docs/concepts/security/rbac-good-practices/ +# custom: +# id: KSV112 +# avd_id: AVD-KSV-0112 +# severity: CRITICAL +# short_code: no-wildcard-resource-role +# recommended_actions: "Remove '*' from 'rules.resources'. Provide specific list of resources to be managed by role in namespace" +# input: +# selector: +# - type: kubernetes +package builtin.kubernetes.KSV112 + +import data.lib.kubernetes +import data.lib.utils + +readVerbs := ["create", "update", "delete", "deletecollection", "impersonate", "*", "list", "get"] + +readKinds := ["Role"] + +managingAllResourcesAtNamespace[input.rules[ru]] { + some ru, r, v + input.kind == readKinds[_] + input.rules[ru].resources[r] == "*" + input.rules[ru].verbs[v] == readVerbs[_] +} + +deny[res] { + badRule := managingAllResourcesAtNamespace[_] + msg := kubernetes.format(sprintf("%s '%s' shouldn't manage all resources at the namespace '%s'", [kubernetes.kind, kubernetes.name, kubernetes.namespace])) + res := result.new(msg, badRule) +} diff --git a/rules/kubernetes/policies/general/any_resource_test.rego b/rules/kubernetes/policies/general/manage_all_resources_at_the_namespace_test.rego similarity index 98% rename from rules/kubernetes/policies/general/any_resource_test.rego rename to rules/kubernetes/policies/general/manage_all_resources_at_the_namespace_test.rego index 60e9d0dd4..a0dbf43b6 100644 --- a/rules/kubernetes/policies/general/any_resource_test.rego +++ b/rules/kubernetes/policies/general/manage_all_resources_at_the_namespace_test.rego @@ -1,4 +1,4 @@ -package builtin.kubernetes.KSV046 +package builtin.kubernetes.KSV112 test_resource_verb_role_secrets { r := deny with input as { diff --git a/rules/kubernetes/policies/general/manage_all_resources_test.rego b/rules/kubernetes/policies/general/manage_all_resources_test.rego new file mode 100644 index 000000000..ca41938cc --- /dev/null +++ b/rules/kubernetes/policies/general/manage_all_resources_test.rego @@ -0,0 +1,145 @@ +package builtin.kubernetes.KSV046 + +test_resource_verb_role_secrets { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["*"], + "verbs": ["delete"], + }], + } + + count(r) > 0 +} + +test_resource_verb_role_pods { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["*"], + "verbs": ["deletecollection"], + }], + } + + count(r) > 0 +} + +test_resource_verb_role_deployments { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["*"], + "verbs": ["create"], + }], + } + + count(r) > 0 +} + +test_resource_verb_role_daemonsets { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["*"], + "verbs": ["list"], + }], + } + + count(r) > 0 +} + +test_resource_verb_role_statefulsets { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["*"], + "verbs": ["get"], + }], + } + + count(r) > 0 +} + +test_resource_verb_role_replicationcontrollers { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["*"], + "verbs": ["impersonate"], + }], + } + + count(r) > 0 +} + +test_resource_resource_role_no_specific_verb { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["impersonate"], + "verbs": ["aaa"], + }], + } + + count(r) == 0 +} + +test_resource_verb_role_no_any_verb { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["*"], + "verbs": ["*"], + }], + } + + count(r) > 0 +} diff --git a/rules/kubernetes/policies/general/manage_eks_iam_auth_configmap.rego b/rules/kubernetes/policies/general/manage_eks_iam_auth_configmap.rego new file mode 100644 index 000000000..13f5a35da --- /dev/null +++ b/rules/kubernetes/policies/general/manage_eks_iam_auth_configmap.rego @@ -0,0 +1,43 @@ +# METADATA +# title: "Manage EKS IAM Auth ConfigMap" +# description: "Ability to add AWS IAM to RBAC bindings via special EKS configmap." +# scope: package +# schemas: +# - input: schema["kubernetes"] +# related_resources: +# - https://kubernetes.io/docs/concepts/security/rbac-good-practices/ +# custom: +# id: KSV115 +# avd_id: AVD-KSV-0115 +# severity: CRITICAL +# short_code: eks-iam-configmap +# recommended_actions: "Remove write permission verbs for resource 'configmaps' named 'aws-auth'" +# input: +# selector: +# - type: kubernetes +package builtin.kubernetes.KSV115 + +import data.lib.kubernetes +import data.lib.utils + +readVerbs := ["create", "update", "patch", "delete", "deletecollection", "impersonate", "*"] + +readKinds := ["Role", "ClusterRole"] + +readResource = "configmaps" + +resourceName := "aws-auth" + +manageEKSIAMAuthConfigmap[input.rules[ru]] { + some ru, r, v + input.kind == readKinds[_] + input.rules[ru].resources[r] == readResource + input.rules[ru].verbs[v] == readVerbs[_] + input.rules[ru].resourceNames[rn] == resourceName +} + +deny[res] { + badRule := manageEKSIAMAuthConfigmap[_] + msg := kubernetes.format(sprintf("%s '%s' should not have access to resource '%s' named '%s' for verbs %s", [kubernetes.kind, kubernetes.name, readResource, resourceName, readVerbs])) + res := result.new(msg, badRule) +} diff --git a/rules/kubernetes/policies/general/manage_eks_iam_auth_configmap_test.rego b/rules/kubernetes/policies/general/manage_eks_iam_auth_configmap_test.rego new file mode 100644 index 000000000..b9de1d4c1 --- /dev/null +++ b/rules/kubernetes/policies/general/manage_eks_iam_auth_configmap_test.rego @@ -0,0 +1,153 @@ +package builtin.kubernetes.KSV115 + +test_manageEKSIAMAuthConfigmap_verb_create { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["configmaps"], + "verbs": ["create"], + "resourceNames": ["aws-auth"], + }], + } + + count(r) > 0 +} + +test_manageEKSIAMAuthConfigmap_verb_update { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["configmaps"], + "verbs": ["update"], + "resourceNames": ["aws-auth"], + }], + } + + count(r) > 0 +} + +test_manageEKSIAMAuthConfigmap_verb_patch { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["configmaps"], + "verbs": ["patch"], + "resourceNames": ["aws-auth"], + }], + } + + count(r) > 0 +} + +test_manageEKSIAMAuthConfigmap_verb_delete { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["configmaps"], + "verbs": ["delete"], + "resourceNames": ["aws-auth"], + }], + } + + count(r) > 0 +} + +test_manageEKSIAMAuthConfigmap_verb_deletecollection { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["configmaps"], + "verbs": ["deletecollection"], + "resourceNames": ["aws-auth"], + }], + } + + count(r) > 0 +} + +test_manageEKSIAMAuthConfigmap_verb_impersonate { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["configmaps"], + "verbs": ["impersonate"], + "resourceNames": ["aws-auth"], + }], + } + + count(r) > 0 +} + +test_manageEKSIAMAuthConfigmap_verb_all { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["configmaps"], + "verbs": ["*"], + "resourceNames": ["aws-auth"], + }], + } + + count(r) > 0 +} + +test_manageEKSIAMAuthConfigmap_verb_wrong { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["configmaps"], + "verbs": ["just"], + "resourceNames": ["aws-auth"], + }], + } + + count(r) == 0 +} diff --git a/rules/kubernetes/policies/general/manage_namespace_secrets.rego b/rules/kubernetes/policies/general/manage_namespace_secrets.rego new file mode 100644 index 000000000..46bcbfa97 --- /dev/null +++ b/rules/kubernetes/policies/general/manage_namespace_secrets.rego @@ -0,0 +1,38 @@ +# METADATA +# title: "Manage namespace secrets" +# description: "Viewing secrets at the namespace scope can lead to escalation if another service account in that namespace has a higher privileged rolebinding or clusterrolebinding bound." +# scope: package +# schemas: +# - input: schema["kubernetes"] +# related_resources: +# - https://kubernetes.io/docs/concepts/security/rbac-good-practices/ +# custom: +# id: KSV113 +# avd_id: AVD-KSV-0113 +# severity: Medium +# short_code: no-manage-ns-secrets +# recommended_actions: "Manage namespace secrets are not allowed. Remove resource 'secrets' from role" +# input: +# selector: +# - type: kubernetes +package builtin.kubernetes.KSV113 + +import data.lib.kubernetes +import data.lib.utils + +readVerbs := ["get", "list", "watch", "create", "update", "patch", "delete", "deletecollection", "impersonate", "*"] + +readKinds := ["Role"] + +resourceManageSecret[input.rules[ru]] { + some ru, r, v + input.kind == readKinds[_] + input.rules[ru].resources[r] == "secrets" + input.rules[ru].verbs[v] == readVerbs[_] +} + +deny[res] { + badRule := resourceManageSecret[_] + msg := kubernetes.format(sprintf("%s '%s' shouldn't have access to manage secrets in namespace '%s'", [kubernetes.kind, kubernetes.name, kubernetes.namespace])) + res := result.new(msg, badRule) +} diff --git a/rules/kubernetes/policies/general/manage_namespace_secrets_test.rego b/rules/kubernetes/policies/general/manage_namespace_secrets_test.rego new file mode 100644 index 000000000..e164fcc21 --- /dev/null +++ b/rules/kubernetes/policies/general/manage_namespace_secrets_test.rego @@ -0,0 +1,163 @@ +package builtin.kubernetes.KSV113 + +test_manage_secrets { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["secrets"], + "verbs": ["get"], + }], + } + + count(r) > 0 +} + +test_manage_verb_update { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["secrets"], + "verbs": ["update"], + }], + } + + count(r) > 0 +} + +test_manage_verb_list { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["secrets"], + "verbs": ["list"], + }], + } + + count(r) > 0 +} + +test_manage_not_secret_resource { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["secrets1"], + "verbs": ["list"], + }], + } + + count(r) == 0 +} + +test_manage_secret_verb_update { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["secrets"], + "verbs": ["update"], + }], + } + + count(r) > 0 +} + +test_manage_secret_verb_impersonate { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["secrets"], + "verbs": ["impersonate"], + }], + } + + count(r) > 0 +} + +test_manage_secret_verb_deletecollection { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["secrets"], + "verbs": ["deletecollection"], + }], + } + + count(r) > 0 +} + +test_manage_secret_verb_patch { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["secrets"], + "verbs": ["patch"], + }], + } + + count(r) > 0 +} + +test_manage_secret_verb_watch { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["secrets"], + "verbs": ["watch"], + }], + } + + count(r) > 0 +} diff --git a/rules/kubernetes/policies/general/manage_secrets.rego b/rules/kubernetes/policies/general/manage_secrets.rego index dad22a8e0..0087f0d50 100644 --- a/rules/kubernetes/policies/general/manage_secrets.rego +++ b/rules/kubernetes/policies/general/manage_secrets.rego @@ -1,6 +1,6 @@ # METADATA -# title: "Do not allow management of secrets" -# description: "Check whether role permits managing secrets" +# title: "Manage secrets" +# description: "Viewing secrets at the cluster-scope is akin to cluster-admin in most clusters as there are typically at least one service accounts (their token stored in a secret) bound to cluster-admin directly or a role/clusterrole that gives similar permissions." # scope: package # schemas: # - input: schema["kubernetes"] @@ -11,7 +11,7 @@ # avd_id: AVD-KSV-0041 # severity: CRITICAL # short_code: no-manage-secrets -# recommended_action: "Create a role which does not permit to manage secrets if not needed" +# recommended_actions: "Manage secrets are not allowed. Remove resource 'secrets' from cluster role" # input: # selector: # - type: kubernetes @@ -22,7 +22,7 @@ import data.lib.utils readVerbs := ["get", "list", "watch", "create", "update", "patch", "delete", "deletecollection", "impersonate", "*"] -readKinds := ["Role", "ClusterRole"] +readKinds := ["ClusterRole"] resourceManageSecret[input.rules[ru]] { some ru, r, v @@ -33,6 +33,6 @@ resourceManageSecret[input.rules[ru]] { deny[res] { badRule := resourceManageSecret[_] - msg := "Role permits management of secret(s)" + msg := kubernetes.format(sprintf("%s '%s' shouldn't have access to manage resource 'secrets'", [kubernetes.kind, kubernetes.name])) res := result.new(msg, badRule) } diff --git a/rules/kubernetes/policies/general/manage_secrets_test.rego b/rules/kubernetes/policies/general/manage_secrets_test.rego index 67f1f65ab..e384d8382 100644 --- a/rules/kubernetes/policies/general/manage_secrets_test.rego +++ b/rules/kubernetes/policies/general/manage_secrets_test.rego @@ -3,7 +3,7 @@ package builtin.kubernetes.KSV041 test_manage_secrets { r := deny with input as { "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "Role", + "kind": "ClusterRole", "metadata": { "namespace": "default", "name": "pod-reader", @@ -21,7 +21,7 @@ test_manage_secrets { test_manage_verb_update { r := deny with input as { "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "Role", + "kind": "ClusterRole", "metadata": { "namespace": "default", "name": "pod-reader", @@ -39,7 +39,7 @@ test_manage_verb_update { test_manage_verb_list { r := deny with input as { "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "Role", + "kind": "ClusterRole", "metadata": { "namespace": "default", "name": "pod-reader", @@ -57,7 +57,7 @@ test_manage_verb_list { test_manage_not_secret_resource { r := deny with input as { "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "Role", + "kind": "ClusterRole", "metadata": { "namespace": "default", "name": "pod-reader", @@ -75,7 +75,7 @@ test_manage_not_secret_resource { test_manage_secret_verb_update { r := deny with input as { "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "Role", + "kind": "ClusterRole", "metadata": { "namespace": "default", "name": "pod-reader", @@ -93,7 +93,7 @@ test_manage_secret_verb_update { test_manage_secret_verb_impersonate { r := deny with input as { "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "Role", + "kind": "ClusterRole", "metadata": { "namespace": "default", "name": "pod-reader", @@ -111,7 +111,7 @@ test_manage_secret_verb_impersonate { test_manage_secret_verb_deletecollection { r := deny with input as { "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "Role", + "kind": "ClusterRole", "metadata": { "namespace": "default", "name": "pod-reader", @@ -129,7 +129,7 @@ test_manage_secret_verb_deletecollection { test_manage_secret_verb_patch { r := deny with input as { "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "Role", + "kind": "ClusterRole", "metadata": { "namespace": "default", "name": "pod-reader", @@ -147,7 +147,7 @@ test_manage_secret_verb_patch { test_manage_secret_verb_watch { r := deny with input as { "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "Role", + "kind": "ClusterRole", "metadata": { "namespace": "default", "name": "pod-reader", diff --git a/rules/kubernetes/policies/general/manage_webhook_configurations.rego b/rules/kubernetes/policies/general/manage_webhook_configurations.rego new file mode 100644 index 000000000..43c479d8a --- /dev/null +++ b/rules/kubernetes/policies/general/manage_webhook_configurations.rego @@ -0,0 +1,40 @@ +# METADATA +# title: "Manage webhookconfigurations" +# description: "Webhooks can silently intercept or actively mutate/block resources as they are being created or updated. This includes secrets and pod specs." +# scope: package +# schemas: +# - input: schema["kubernetes"] +# related_resources: +# - https://kubernetes.io/docs/concepts/security/rbac-good-practices/ +# custom: +# id: KSV114 +# avd_id: AVD-KSV-0114 +# severity: Critical +# short_code: no-manage-webhook +# recommended_actions: "Remove webhook configuration resouces/verbs, acceptable values for verbs ['get', 'list', 'watch']" +# input: +# selector: +# - type: kubernetes +package builtin.kubernetes.KSV114 + +import data.lib.kubernetes +import data.lib.utils + +readVerbs := ["create", "update", "patch", "delete", "deletecollection", "impersonate", "*"] + +readKinds := ["Role", "ClusterRole"] + +readResource = ["mutatingwebhookconfigurations", "validatingwebhookconfigurations"] + +manageWebhookConfig[input.rules[ru]] { + some ru, r, v + input.kind == readKinds[_] + input.rules[ru].resources[r] == readResource[_] + input.rules[ru].verbs[v] == readVerbs[_] +} + +deny[res] { + badRule := manageWebhookConfig[_] + msg := kubernetes.format(sprintf("%s '%s' should not have access to resources %s for verbs %s", [kubernetes.kind, kubernetes.name, readResource, readVerbs])) + res := result.new(msg, badRule) +} diff --git a/rules/kubernetes/policies/general/manage_webhook_configurations_test.rego b/rules/kubernetes/policies/general/manage_webhook_configurations_test.rego new file mode 100644 index 000000000..20e5707db --- /dev/null +++ b/rules/kubernetes/policies/general/manage_webhook_configurations_test.rego @@ -0,0 +1,163 @@ +package builtin.kubernetes.KSV114 + +test_manageWebhookConfig_verb_create { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["mutatingwebhookconfigurations"], + "verbs": ["create"], + }], + } + + count(r) > 0 +} + +test_manageWebhookConfig_verb_update { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["mutatingwebhookconfigurations"], + "verbs": ["update"], + }], + } + + count(r) > 0 +} + +test_manageWebhookConfig_verb_patch { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["mutatingwebhookconfigurations"], + "verbs": ["patch"], + }], + } + + count(r) > 0 +} + +test_manageWebhookConfig_verb_delete { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["validatingwebhookconfigurations"], + "verbs": ["delete"], + }], + } + + count(r) > 0 +} + +test_manageWebhookConfig_verb_deletecollection { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["validatingwebhookconfigurations"], + "verbs": ["deletecollection"], + }], + } + + count(r) > 0 +} + +test_manageWebhookConfig_verb_impersonate { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["validatingwebhookconfigurations"], + "verbs": ["impersonate"], + }], + } + + count(r) > 0 +} + +test_validatingWebhook_manageWebhookConfig_verb_all { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["validatingwebhookconfigurations"], + "verbs": ["*"], + }], + } + + count(r) > 0 +} + +test_mutatingWebhook_manageWebhookConfig_verb_all { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["mutatingwebhookconfigurations"], + "verbs": ["*"], + }], + } + + count(r) > 0 +} + +test_manageWebhookConfig_verb_wrong { + r := deny with input as { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "namespace": "default", + "name": "pod-reader", + }, + "rules": [{ + "apiGroups": ["*"], + "resources": ["mutatingwebhookconfigurations"], + "verbs": ["just"], + }], + } + + count(r) == 0 +} diff --git a/rules/kubernetes/policies/general/runs_with_a_root_primary_or_supplementary_GID.rego b/rules/kubernetes/policies/general/runs_with_a_root_primary_or_supplementary_GID.rego new file mode 100644 index 000000000..10b37743e --- /dev/null +++ b/rules/kubernetes/policies/general/runs_with_a_root_primary_or_supplementary_GID.rego @@ -0,0 +1,56 @@ +# METADATA +# title: "Runs with a root primary or supplementary GID" +# description: "According to pod security standard 'Non-root groups', containers should be forbidden from running with a root primary or supplementary GID." +# scope: package +# schemas: +# - input: schema["kubernetes"] +# related_resources: +# - https://kubesec.io/basics/containers-securitycontext-runasuser/ +# custom: +# id: KSV116 +# avd_id: AVD-KSV-0116 +# severity: LOW +# short_code: primary-supplementary-gid +# recommended_actions: "Set 'containers[].securityContext.runAsGroup' to a non-zero integer or leave undefined." +# input: +# selector: +# - type: kubernetes +package builtin.kubernetes.KSV116 + +import data.lib.kubernetes +import data.lib.utils + +default failRootGroupId = false + +# getContainersWithRootGroupId returns a list of containers +# with root group id set +getContainersWithRootGroupId[name] { + container := kubernetes.containers[_] + container.securityContext.runAsGroup == 0 + name := container.name +} + +# failRootGroupId is true if root group id is set on pod +failRootGroupId { + pod := kubernetes.pods[_] + pod.spec.securityContext.runAsGroup == 0 +} + +# failRootGroupId is true if root group id is set on pod +failRootGroupId { + pod := kubernetes.pods[_] + gid := pod.spec.securityContext.supplementalGroups[_] + gid == 0 +} + +# failRootGroupId is true if root group id is set on pod +failRootGroupId { + pod := kubernetes.pods[_] + pod.spec.securityContext.fsGroup == 0 +} + +deny[res] { + output := failRootGroupId + msg := kubernetes.format(sprintf("%s %s in %s namespace should set spec.securityContext.runAsGroup, spec.securityContext.supplementalGroups[*] and spec.securityContext.fsGroup to integer greater than 0", [lower(kubernetes.kind), kubernetes.name, kubernetes.namespace])) + res := result.new(msg, output) +} diff --git a/rules/kubernetes/policies/general/runs_with_a_root_primary_or_supplementary_GID_test.rego b/rules/kubernetes/policies/general/runs_with_a_root_primary_or_supplementary_GID_test.rego new file mode 100644 index 000000000..c1f44dcd2 --- /dev/null +++ b/rules/kubernetes/policies/general/runs_with_a_root_primary_or_supplementary_GID_test.rego @@ -0,0 +1,31 @@ +package builtin.kubernetes.KSV116 + +test_failRootGroupId { + r := deny with input as { + "apiVersion": "v1", + "kind": "Pod", + "metadata": {"name": "hello-gid"}, + "spec": {"securityContext": { + "runAsGroup": 0, + "supplementalGroups": [0], + "fsGroup": 0, + }}, + } + + count(r) > 0 +} + +test_failRootGroupId_failed { + r := deny with input as { + "apiVersion": "v1", + "kind": "Pod", + "metadata": {"name": "hello-gid"}, + "spec": {"securityContext": { + "runAsGroup": 1001, # Non-zero value + "supplementalGroups": [1002], # Non-zero value + "fsGroup": 1003, # Non-zero value + }}, + } + + count(r) > 0 +}