diff --git a/api/changelog.d/kubernetes-auth-provider-cmd-path.security.md b/api/changelog.d/kubernetes-auth-provider-cmd-path.security.md new file mode 100644 index 0000000000..68ccd007e7 --- /dev/null +++ b/api/changelog.d/kubernetes-auth-provider-cmd-path.security.md @@ -0,0 +1 @@ +Kubernetes kubeconfig validation now rejects legacy `auth-provider.config.cmd-path` command authentication in Prowler Cloud/API diff --git a/api/src/backend/api/tests/test_serializers.py b/api/src/backend/api/tests/test_serializers.py index 8e77d63604..0d5b517f97 100644 --- a/api/src/backend/api/tests/test_serializers.py +++ b/api/src/backend/api/tests/test_serializers.py @@ -309,6 +309,36 @@ current-context: test-context assert not serializer.is_valid() assert "kubeconfig_content" in serializer.errors + def test_kubeconfig_with_auth_provider_cmd_path_is_rejected(self): + kubeconfig_content = """ +apiVersion: v1 +kind: Config +clusters: + - name: test-cluster + cluster: + server: https://kubernetes.example.test +users: + - name: test-user + user: + auth-provider: + name: gcp + config: + cmd-path: /bin/sh +contexts: + - name: test-context + context: + cluster: test-cluster + user: test-user +current-context: test-context +""" + + serializer = KubernetesProviderSecret( + data={"kubeconfig_content": kubeconfig_content} + ) + + assert not serializer.is_valid() + assert "kubeconfig_content" in serializer.errors + def test_malformed_kubeconfig_is_rejected(self): serializer = KubernetesProviderSecret( data={"kubeconfig_content": "apiVersion: ["} diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py index 49b593049f..0d80b47c0a 100644 --- a/api/src/backend/api/v1/serializer_utils/providers.py +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -214,7 +214,7 @@ from rest_framework_json_api import serializers "kubeconfig_content": { "type": "string", "description": "The content of the Kubernetes kubeconfig file, encoded as a string. " - "Kubeconfig exec authentication is not supported in Prowler Cloud for security reasons.", + "Kubeconfig command-based authentication is not supported in Prowler Cloud for security reasons.", } }, "required": ["kubeconfig_content"], diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 7aeaf8d10c..9292a0c633 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1569,14 +1569,14 @@ class FindingMetadataSerializer(BaseSerializerV1): # Provider secrets -KUBERNETES_KUBECONFIG_EXEC_ERROR = ( - "Kubernetes kubeconfig exec authentication is not supported in Prowler Cloud " - "for security reasons." +KUBERNETES_KUBECONFIG_UNSUPPORTED_COMMAND_AUTH_ERROR = ( + "Kubernetes kubeconfig command-based authentication is not supported in " + "Prowler Cloud for security reasons." ) KUBERNETES_KUBECONFIG_INVALID_ERROR = "Invalid Kubernetes kubeconfig content." -def kubeconfig_contains_exec_auth(kubeconfig: dict) -> bool: +def kubeconfig_contains_unsupported_command_auth(kubeconfig: dict) -> bool: users = kubeconfig.get("users", []) if not isinstance(users, list): raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR) @@ -1592,6 +1592,17 @@ def kubeconfig_contains_exec_auth(kubeconfig: dict) -> bool: if "exec" in user: return True + auth_provider = user.get("auth-provider", {}) + if not isinstance(auth_provider, dict): + continue + + auth_provider_config = auth_provider.get("config", {}) + if not isinstance(auth_provider_config, dict): + continue + + if "cmd-path" in auth_provider_config: + return True + return False @@ -1788,8 +1799,10 @@ class KubernetesProviderSecret(serializers.Serializer): if not isinstance(kubeconfig, dict): raise serializers.ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR) - if kubeconfig_contains_exec_auth(kubeconfig): - raise serializers.ValidationError(KUBERNETES_KUBECONFIG_EXEC_ERROR) + if kubeconfig_contains_unsupported_command_auth(kubeconfig): + raise serializers.ValidationError( + KUBERNETES_KUBECONFIG_UNSUPPORTED_COMMAND_AUTH_ERROR + ) return kubeconfig_content diff --git a/ui/changelog.d/kubernetes-auth-provider-cmd-path.security.md b/ui/changelog.d/kubernetes-auth-provider-cmd-path.security.md new file mode 100644 index 0000000000..b7cb540243 --- /dev/null +++ b/ui/changelog.d/kubernetes-auth-provider-cmd-path.security.md @@ -0,0 +1 @@ +Kubernetes credential forms now reject kubeconfig files using legacy `auth-provider.config.cmd-path` command authentication diff --git a/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx index c442b3fbab..2c3e84d5ff 100644 --- a/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx @@ -5,8 +5,8 @@ import { Control, useWatch } from "react-hook-form"; import { WizardTextareaField } from "@/components/providers/workflow/forms/fields"; import { KubernetesCredentials } from "@/types"; import { - KUBECONFIG_EXEC_AUTHENTICATION_ERROR, - kubeconfigContainsExecAuthentication, + KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR, + kubeconfigContainsUnsupportedCommandAuthentication, } from "@/types/formSchemas"; export const KubernetesCredentialsForm = ({ @@ -18,9 +18,8 @@ export const KubernetesCredentialsForm = ({ control, name: "kubeconfig_content", }); - const hasExecAuthentication = kubeconfigContainsExecAuthentication( - kubeconfigContent ?? "", - ); + const hasUnsupportedCommandAuthentication = + kubeconfigContainsUnsupportedCommandAuthentication(kubeconfigContent ?? ""); return ( <> @@ -42,9 +41,9 @@ export const KubernetesCredentialsForm = ({ minRows={10} isRequired /> - {hasExecAuthentication && ( + {hasUnsupportedCommandAuthentication && (
- {KUBECONFIG_EXEC_AUTHENTICATION_ERROR} + {KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR}
)} > diff --git a/ui/types/formSchemas.test.ts b/ui/types/formSchemas.test.ts index 211ee35e55..750de0ed5e 100644 --- a/ui/types/formSchemas.test.ts +++ b/ui/types/formSchemas.test.ts @@ -198,6 +198,32 @@ users: ); }); + it("reports kubeconfig auth-provider cmd-path on kubeconfig_content field", () => { + const schema = addCredentialsFormSchema("kubernetes"); + + const result = schema.safeParse({ + ...BASE_KUBERNETES_VALUES, + [ProviderCredentialFields.KUBECONFIG_CONTENT]: `apiVersion: v1 +kind: Config +users: + - name: test-user + user: + auth-provider: + name: gcp + config: + cmd-path: /bin/sh`, + }); + + expect(result.success).toBe(false); + if (result.success) return; + + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + path: [ProviderCredentialFields.KUBECONFIG_CONTENT], + }), + ); + }); + it("accepts malformed kubeconfig content for backend validation", () => { const schema = addCredentialsFormSchema("kubernetes"); diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index 0e13f67814..a535821682 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -6,14 +6,14 @@ import { validateMutelistYaml, validateYaml } from "@/lib/yaml"; import { PROVIDER_TYPES, ProviderType } from "./providers"; -export const KUBECONFIG_EXEC_AUTHENTICATION_ERROR = - "Kubernetes kubeconfig exec authentication is not supported in Prowler Cloud for security reasons."; +export const KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR = + "Kubernetes kubeconfig command-based authentication is not supported in Prowler Cloud for security reasons."; const isRecord = (value: unknown): value is Record