diff --git a/avd_docs/aws/iam/AVD-AWS-0342/docs.md b/avd_docs/aws/iam/AVD-AWS-0342/docs.md index 9e3642313..ff507cdb6 100644 --- a/avd_docs/aws/iam/AVD-AWS-0342/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0342/docs.md @@ -1,10 +1,8 @@ -In iam:PassRole the service carrying out the actions is "provided" a role by the calling principal and implicitly takes on that role to carry out the actions (instead of executing sts:AssumeRole). - The privileges attached to the role are distinct from those of the primary ordering the action and may even be larger and can cause security issues. - +Ensures any IAM pass role attched to roles are flagged and warned. ### Impact -Compromise on security of aws resources. + {{ remediationActions }} diff --git a/avd_docs/azure/storage/AVD-AZU-0011/Terraform.md b/avd_docs/azure/storage/AVD-AZU-0011/Terraform.md index 1d55e663e..5f4780943 100644 --- a/avd_docs/azure/storage/AVD-AZU-0011/Terraform.md +++ b/avd_docs/azure/storage/AVD-AZU-0011/Terraform.md @@ -6,6 +6,7 @@ Use a more recent TLS/SSL policy for the load balancer name = "storageaccountname" resource_group_name = azurerm_resource_group.example.name location = azurerm_resource_group.example.location + min_tls_version = "TLS1_2" } ``` diff --git a/avd_docs/google/iam/AVD-GCP-0068/Terraform.md b/avd_docs/google/iam/AVD-GCP-0068/Terraform.md index 7f3559514..59db56dae 100644 --- a/avd_docs/google/iam/AVD-GCP-0068/Terraform.md +++ b/avd_docs/google/iam/AVD-GCP-0068/Terraform.md @@ -1,11 +1,18 @@ -Set conditions on this provider, for example by restricting it to only be allowed from repositories in your GitHub organization. +Set conditions on this provider, for example by restricting it to only be allowed from repositories in your GitHub organization ```hcl - resource "google_iam_workload_identity_pool_provider" "github" { - project = "example-project" - workload_identity_pool_id = "example-pool" - workload_identity_pool_provider_id = "example-provider" + resource "google_iam_workload_identity_pool" "github" { + provider = google + project = data.google_project.project.project_id + workload_identity_pool_id = "github" + } + + resource "google_iam_workload_identity_pool_provider" "github" { + provider = google + project = data.google_project.project.project_id + workload_identity_pool_id = google_iam_workload_identity_pool.github-actions[0].workload_identity_pool_id + workload_identity_pool_provider_id = "github" attribute_condition = "assertion.repository_owner=='your-github-organization'" @@ -15,10 +22,14 @@ Set conditions on this provider, for example by restricting it to only be allowe "attribute.aud" = "assertion.aud" "attribute.repository" = "assertion.repository" } - } + + oidc { + issuer_uri = "https://token.actions.githubusercontent.com" + } + } + ``` #### Remediation Links - -- https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iam_workload_identity_pool_provider#attribute_condition + - https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iam_workload_identity_pool_provider#attribute_condition diff --git a/avd_docs/google/iam/AVD-GCP-0068/docs.md b/avd_docs/google/iam/AVD-GCP-0068/docs.md index 993768913..ed1e8ced3 100644 --- a/avd_docs/google/iam/AVD-GCP-0068/docs.md +++ b/avd_docs/google/iam/AVD-GCP-0068/docs.md @@ -2,10 +2,12 @@ In GitHub Actions, one can authenticate to Google Cloud by setting values for workload_identity_provider and service_account and requesting a short-lived OIDC token which is then used to execute commands as that Service Account. If you don't specify a condition in the workload identity provider pool configuration, then any GitHub Action can assume this role and act as that Service Account. ### Impact -Privilege escalation, impersonation of service accounts +Allows an external attacker to authenticate as the attached service account and act with its permissions {{ remediationActions }} ### Links - https://www.revblock.dev/exploiting-misconfigured-google-cloud-service-accounts-from-github-actions/ + + 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/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_tests.rego b/rules/kubernetes/policies/general/manage_webhook_configurations_tests.rego new file mode 100644 index 000000000..20e5707db --- /dev/null +++ b/rules/kubernetes/policies/general/manage_webhook_configurations_tests.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 +}