fix(kubernetes): reject exec auth in cloud kubeconfigs

This commit is contained in:
Hugo P.Brito
2026-07-01 11:47:17 +01:00
parent 883ffa1fdb
commit 96d815a855
5 changed files with 200 additions and 3 deletions
+72 -1
View File
@@ -1,6 +1,6 @@
import pytest
from api.v1.serializer_utils.integrations import S3ConfigSerializer
from api.v1.serializers import ImageProviderSecret
from api.v1.serializers import ImageProviderSecret, KubernetesProviderSecret
from rest_framework.exceptions import ValidationError
@@ -132,3 +132,74 @@ class TestImageProviderSecret:
serializer = ImageProviderSecret(data={"registry_password": "pass"})
assert not serializer.is_valid()
assert "non_field_errors" in serializer.errors
class TestKubernetesProviderSecret:
def test_valid_static_kubeconfig_is_accepted(self):
kubeconfig_content = """
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://kubernetes.example.test
users:
- name: test-user
user:
token: test-token
contexts:
- name: test-context
context:
cluster: test-cluster
user: test-user
current-context: test-context
"""
serializer = KubernetesProviderSecret(
data={"kubeconfig_content": kubeconfig_content}
)
assert serializer.is_valid()
def test_kubeconfig_with_exec_authentication_is_rejected(self):
kubeconfig_content = """
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://kubernetes.example.test
users:
- name: test-user
user:
exec:
apiVersion: client.authentication.k8s.io/v1
command: kubectl
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: ["}
)
assert not serializer.is_valid()
assert "kubeconfig_content" in serializer.errors
def test_non_mapping_kubeconfig_is_rejected(self):
serializer = KubernetesProviderSecret(data={"kubeconfig_content": "[]"})
assert not serializer.is_valid()
assert "kubeconfig_content" in serializer.errors
@@ -213,7 +213,8 @@ from rest_framework_json_api import serializers
"properties": {
"kubeconfig_content": {
"type": "string",
"description": "The content of the Kubernetes kubeconfig file, encoded as a 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.",
}
},
"required": ["kubeconfig_content"],
+43
View File
@@ -2,6 +2,7 @@ import base64
import json
from datetime import UTC, datetime, timedelta
import yaml
from api.db_router import MainRouter
from api.exceptions import ConflictException
from api.models import (
@@ -1530,6 +1531,32 @@ class FindingMetadataSerializer(BaseSerializerV1):
# Provider secrets
KUBERNETES_KUBECONFIG_EXEC_ERROR = (
"Kubernetes kubeconfig exec 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:
users = kubeconfig.get("users", [])
if not isinstance(users, list):
raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
for user_entry in users:
if not isinstance(user_entry, dict):
raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
user = user_entry.get("user", {})
if not isinstance(user, dict):
raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
if "exec" in user:
return True
return False
class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
@staticmethod
def validate_secret_based_on_provider(
@@ -1711,6 +1738,22 @@ class MongoDBAtlasProviderSecret(serializers.Serializer):
class KubernetesProviderSecret(serializers.Serializer):
kubeconfig_content = serializers.CharField()
def validate_kubeconfig_content(self, kubeconfig_content):
try:
kubeconfig = yaml.safe_load(kubeconfig_content)
except yaml.YAMLError as exc:
raise serializers.ValidationError(
KUBERNETES_KUBECONFIG_INVALID_ERROR
) from exc
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)
return kubeconfig_content
class Meta:
resource_name = "provider-secrets"
+48
View File
@@ -150,3 +150,51 @@ describe("addCredentialsFormSchema - okta", () => {
);
});
});
describe("addCredentialsFormSchema - kubernetes", () => {
const BASE_KUBERNETES_VALUES = {
[ProviderCredentialFields.PROVIDER_ID]: "provider-kubernetes-1",
[ProviderCredentialFields.PROVIDER_TYPE]: "kubernetes",
} as const;
it("accepts kubeconfig content without exec authentication", () => {
const schema = addCredentialsFormSchema("kubernetes");
const result = schema.safeParse({
...BASE_KUBERNETES_VALUES,
[ProviderCredentialFields.KUBECONFIG_CONTENT]: `apiVersion: v1
kind: Config
users:
- name: test-user
user:
token: test-token`,
});
expect(result.success).toBe(true);
});
it("reports kubeconfig exec authentication 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:
exec:
apiVersion: client.authentication.k8s.io/v1
command: kubectl`,
});
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.issues).toContainEqual(
expect.objectContaining({
path: [ProviderCredentialFields.KUBECONFIG_CONTENT],
}),
);
});
});
+35 -1
View File
@@ -1,3 +1,4 @@
import yaml from "js-yaml";
import { z } from "zod";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
@@ -5,6 +6,33 @@ import { validateMutelistYaml, validateYaml } from "@/lib/yaml";
import { PROVIDER_TYPES, ProviderType } from "./providers";
const KUBECONFIG_EXEC_AUTHENTICATION_ERROR =
"Kubernetes kubeconfig exec authentication is not supported in Prowler Cloud for security reasons.";
const isRecord = (value: unknown): value is Record<string, unknown> => {
return typeof value === "object" && value !== null && !Array.isArray(value);
};
const kubeconfigContainsExecAuthentication = (value: string): boolean => {
try {
const parsed = yaml.load(value);
if (!isRecord(parsed) || !Array.isArray(parsed.users)) {
return false;
}
return parsed.users.some((userEntry) => {
if (!isRecord(userEntry) || !isRecord(userEntry.user)) {
return false;
}
return "exec" in userEntry.user;
});
} catch {
return false;
}
};
export const addRoleFormSchema = z.object({
name: z.string().min(1, "Name is required"),
manage_users: z.boolean().default(false),
@@ -207,7 +235,13 @@ export const addCredentialsFormSchema = (
? {
[ProviderCredentialFields.KUBECONFIG_CONTENT]: z
.string()
.min(1, "Kubeconfig Content is required"),
.min(1, "Kubeconfig Content is required")
.refine(
(value) => !kubeconfigContainsExecAuthentication(value),
{
error: KUBECONFIG_EXEC_AUTHENTICATION_ERROR,
},
),
}
: providerType === "m365"
? {