fix(api): require inline kubeconfig credentials (#12952)

Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
Pedro Martínandalejandrobailo authored and GitHub committed 2026-10-08 18:15:10 +02:00
1 parent 27c4113d29
commit f44ea4b6fa
12 files changed
+664 -72

No files matched your search

@@ -0,0 +1 @@
Kubernetes kubeconfig form validation accepts only inline credentials and known cluster fields, rejecting file-path, command-based, `auth-provider`, proxy URL and unknown fields before submission
@@ -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_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR,
kubeconfigContainsUnsupportedCommandAuthentication,
KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
kubeconfigIsInlineOnlyCredentials,
} from "@/types/formSchemas";
export const KubernetesCredentialsForm = ({
@@ -18,8 +18,9 @@ export const KubernetesCredentialsForm = ({
control,
name: "kubeconfig_content",
});
const hasUnsupportedCommandAuthentication =
kubeconfigContainsUnsupportedCommandAuthentication(kubeconfigContent ?? "");
const hasUnsupportedCredentials = !kubeconfigIsInlineOnlyCredentials(
kubeconfigContent ?? "",
);
return (
<>
@@ -41,9 +42,9 @@ export const KubernetesCredentialsForm = ({
minRows={10}
isRequired
/>
{hasUnsupportedCommandAuthentication && (
{hasUnsupportedCredentials && (
<p className="text-text-error-primary text-xs">
{KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR}
{KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR}
</p>
)}
</>
+113 -9
View File
@@ -7,7 +7,7 @@ import {
addCredentialsFormSchema,
addCredentialsRoleFormSchema,
addProviderFormSchema,
KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR,
KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
roleFormSchema,
samlConfigFormSchema,
} from "./formSchemas";
@@ -222,7 +222,7 @@ users:
expect(result.error.issues).toContainEqual(
expect.objectContaining({
path: [ProviderCredentialFields.KUBECONFIG_CONTENT],
message: KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR,
message: KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
}),
);
});
@@ -249,17 +249,42 @@ users:
expect(result.error.issues).toContainEqual(
expect.objectContaining({
path: [ProviderCredentialFields.KUBECONFIG_CONTENT],
message: KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR,
message: KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
}),
);
});
it("accepts kubeconfig auth-provider without cmd-path", () => {
const schema = addCredentialsFormSchema("kubernetes");
const result = schema.safeParse({
...BASE_KUBERNETES_VALUES,
[ProviderCredentialFields.KUBECONFIG_CONTENT]: `apiVersion: v1
it.each([
[
"user tokenFile",
`apiVersion: v1
kind: Config
users:
- name: test-user
user:
tokenFile: /etc/passwd`,
],
[
"user client-certificate",
`apiVersion: v1
kind: Config
users:
- name: test-user
user:
client-certificate: /etc/ssl/cert.pem`,
],
[
"user client-key",
`apiVersion: v1
kind: Config
users:
- name: test-user
user:
client-key: /etc/ssl/key.pem`,
],
[
"user auth-provider directives",
`apiVersion: v1
kind: Config
users:
- name: test-user
@@ -268,6 +293,85 @@ users:
name: oidc
config:
client-id: prowler`,
],
[
"unknown user key",
`apiVersion: v1
kind: Config
users:
- name: test-user
user:
bogus: value`,
],
[
"cluster certificate-authority",
`apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://example.test
certificate-authority: /etc/ssl/ca.pem`,
],
[
"cluster proxy-url",
`apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://example.test
proxy-url: http://proxy.evil.test`,
],
[
"unknown cluster key",
`apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://example.test
bogus: /etc/ssl/ca.pem`,
],
])(
"rejects kubeconfig with %s on kubeconfig_content field",
(_label, kubeconfigContent) => {
const schema = addCredentialsFormSchema("kubernetes");
const result = schema.safeParse({
...BASE_KUBERNETES_VALUES,
[ProviderCredentialFields.KUBECONFIG_CONTENT]: kubeconfigContent,
});
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.issues).toContainEqual(
expect.objectContaining({
path: [ProviderCredentialFields.KUBECONFIG_CONTENT],
message: KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
}),
);
},
);
it("accepts kubeconfig with only inline credentials", () => {
const schema = addCredentialsFormSchema("kubernetes");
const result = schema.safeParse({
...BASE_KUBERNETES_VALUES,
[ProviderCredentialFields.KUBECONFIG_CONTENT]: `apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://example.test
certificate-authority-data: Zm9v
users:
- name: test-user
user:
client-certificate-data: Zm9v
client-key-data: YmFy`,
});
expect(result.success).toBe(true);
+58 -29
View File
@@ -8,42 +8,75 @@ import { MAX_SAML_ADDITIONAL_EMAIL_DOMAINS } from "@/types/saml";
import { isKnownProviderType, PROVIDER_TYPES, ProviderType } from "./providers";
export const KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR =
"Kubernetes kubeconfig command-based authentication is not supported in Prowler Cloud for security reasons.";
export const KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR =
"Only inline Kubernetes credentials are supported. For user authentication use token, username/password, or client-certificate-data with client-key-data; clusters may use certificate-authority-data. File-path fields (tokenFile, client-certificate, client-key, certificate-authority), command-based authentication (exec, cmd-path), auth-provider directives, and proxy URLs are not supported.";
const isRecord = (value: unknown): value is Record<string, unknown> => {
return typeof value === "object" && value !== null && !Array.isArray(value);
};
export const kubeconfigContainsUnsupportedCommandAuthentication = (
value: string,
): boolean => {
const KUBECONFIG_ALLOWED_USER_KEYS = new Set([
"token",
"username",
"password",
"client-certificate-data",
"client-key-data",
"as",
"as-uid",
"as-groups",
"as-user-extra",
]);
const KUBECONFIG_ALLOWED_CLUSTER_KEYS = new Set([
"server",
"certificate-authority-data",
"insecure-skip-tls-verify",
"tls-server-name",
"disable-compression",
"extensions",
]);
// Mirrors the API key checks; structural validation stays in the API.
export const kubeconfigIsInlineOnlyCredentials = (value: string): boolean => {
let parsed: unknown;
try {
const parsed = yaml.load(value);
parsed = yaml.load(value);
} catch {
return true;
}
if (!isRecord(parsed) || !Array.isArray(parsed.users)) {
return false;
}
if (!isRecord(parsed)) {
return true;
}
return parsed.users.some((userEntry) => {
if (Array.isArray(parsed.users)) {
const usersInlineOnly = parsed.users.every((userEntry) => {
if (!isRecord(userEntry) || !isRecord(userEntry.user)) {
return false;
}
if ("exec" in userEntry.user) {
return true;
}
const authProvider = userEntry.user["auth-provider"];
if (!isRecord(authProvider) || !isRecord(authProvider.config)) {
return false;
}
return "cmd-path" in authProvider.config;
return Object.keys(userEntry.user).every((key) =>
KUBECONFIG_ALLOWED_USER_KEYS.has(key),
);
});
} catch {
return false;
if (!usersInlineOnly) {
return false;
}
}
if (Array.isArray(parsed.clusters)) {
const clustersInlineOnly = parsed.clusters.every((clusterEntry) => {
if (!isRecord(clusterEntry) || !isRecord(clusterEntry.cluster)) {
return true;
}
return Object.keys(clusterEntry.cluster).every((key) =>
KUBECONFIG_ALLOWED_CLUSTER_KEYS.has(key),
);
});
if (!clustersInlineOnly) {
return false;
}
}
return true;
};
// Create and edit share the same shape, so a single schema backs both flows.
@@ -263,13 +296,9 @@ export const addCredentialsFormSchema = (
.string()
.min(1, "Kubeconfig Content is required")
.refine(
(value) =>
!kubeconfigContainsUnsupportedCommandAuthentication(
value,
),
(value) => kubeconfigIsInlineOnlyCredentials(value),
{
error:
KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR,
error: KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
},
),
}