mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-29 06:44:21 +00:00
fix(api): require inline kubeconfig credentials (#12952)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user