Merge pull request #5002 from Infisical/enhancement/k8s-auth-validation-and-errors

improvement(kubernetes-auth): k8s auth validation and errors
This commit is contained in:
Sheen
2026-01-05 21:44:50 +08:00
committed by GitHub
4 changed files with 662 additions and 39 deletions

View File

@@ -0,0 +1,200 @@
import { AxiosError } from "axios";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
type ErrorContext = {
host?: string;
port?: number;
kubernetesHost?: string;
};
export enum KubernetesAuthErrorContext {
KubernetesHost = "kubernetes-host",
KubernetesApiServer = "kubernetes-api-server",
GatewayProxy = "gateway-proxy"
}
type ErrorContextConfig = {
serviceName: string;
errorNamePrefix: string;
defaultErrorName: string;
default401Message: string;
default403Message: string;
};
const COMMON_KUBERNETES_MESSAGES = {
default401Message:
"Token reviewer JWT is invalid or expired. Please verify the token reviewer JWT is correct and has not expired.",
default403Message:
"Token reviewer JWT does not have permission to perform TokenReviews. Ensure the service account has the 'system:auth-delegator' ClusterRole binding."
} as const;
const ERROR_CONTEXT_CONFIGS: Record<KubernetesAuthErrorContext, ErrorContextConfig> = {
[KubernetesAuthErrorContext.KubernetesHost]: {
serviceName: "Kubernetes host",
errorNamePrefix: "KubernetesHost",
defaultErrorName: "KubernetesHostConnectionError",
...COMMON_KUBERNETES_MESSAGES
},
[KubernetesAuthErrorContext.KubernetesApiServer]: {
serviceName: "Kubernetes API server",
errorNamePrefix: "Kubernetes",
defaultErrorName: "KubernetesConnectionError",
...COMMON_KUBERNETES_MESSAGES
},
[KubernetesAuthErrorContext.GatewayProxy]: {
serviceName: "gateway proxy",
errorNamePrefix: "Gateway",
defaultErrorName: "GatewayConnectionError",
default401Message:
"Gateway service account is not authorized to perform TokenReviews. Verify the gateway has the 'system:auth-delegator' ClusterRole binding.",
default403Message:
"Gateway service account does not have permission to perform TokenReviews. Ensure it has the 'system:auth-delegator' ClusterRole binding."
}
};
/**
* Handles Axios network-level errors (connection refused, DNS failures, timeouts, etc.)
* Returns a BadRequestError with a descriptive message, or null if the error is not a network error.
*/
export const handleAxiosNetworkError = (
err: AxiosError,
context: ErrorContext,
contextType: KubernetesAuthErrorContext
): BadRequestError | null => {
const { host, kubernetesHost } = context;
const target = host || kubernetesHost || "server";
const { errorNamePrefix: prefix, serviceName } = ERROR_CONTEXT_CONFIGS[contextType];
if (err.code === "ECONNREFUSED") {
return new BadRequestError({
name: `${prefix}ConnectionRefused`,
message: `Failed to connect to ${serviceName} at ${target}: Connection refused. Verify the host URL and ensure the ${serviceName.toLowerCase()} is accessible.`
});
}
if (err.code === "ENOTFOUND") {
return new BadRequestError({
name: `${prefix}HostNotFound`,
message: `Failed to resolve ${serviceName} hostname: ${target}. Verify the hostname is correct.`
});
}
if (err.code === "ETIMEDOUT" || err.code === "ECONNABORTED") {
return new BadRequestError({
name: `${prefix}ConnectionTimeout`,
message: `Connection to ${serviceName} at ${target} timed out. Verify network connectivity and firewall rules.`
});
}
if (err.code === "DEPTH_ZERO_SELF_SIGNED_CERT" || err.code === "SELF_SIGNED_CERT_IN_CHAIN") {
return new BadRequestError({
name: `${prefix}CertificateError`,
message: `SSL certificate verification failed for ${serviceName} at ${target}. The server uses a self-signed certificate. Please provide the CA certificate in the configuration.`
});
}
if (err.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE" || err.code === "CERT_HAS_EXPIRED") {
return new BadRequestError({
name: `${prefix}CertificateError`,
message: `SSL certificate verification failed for ${serviceName} at ${target}. Verify the CA certificate is correct and the server certificate is valid.`
});
}
return null;
};
/**
* Handles Axios HTTP response errors (401, 403, etc.)
* Returns an appropriate error, or null if not an HTTP error.
*/
export const handleAxiosHttpError = (
err: AxiosError,
contextType: KubernetesAuthErrorContext
): UnauthorizedError | BadRequestError | null => {
if (!err.response) {
return null;
}
let message = (err.response.data as { message?: string })?.message;
const statusCode = err.response.status;
const { errorNamePrefix: prefix, default401Message, default403Message } = ERROR_CONTEXT_CONFIGS[contextType];
if (!message && typeof err.response.data === "string") {
message = err.response.data;
}
if (statusCode === 401) {
return new UnauthorizedError({
message: message || default401Message,
name: `${prefix}TokenReviewerUnauthorized`
});
}
if (statusCode === 403) {
return new UnauthorizedError({
message: message || default403Message,
name: `${prefix}TokenReviewerForbidden`
});
}
if (message) {
return new BadRequestError({
message,
name: `${prefix}TokenReviewRequestError`
});
}
// Generic HTTP error
return new BadRequestError({
name: `${prefix}TokenReviewRequestError`,
message: `${prefix} returned HTTP ${statusCode}: ${err.response.statusText || "Unknown error"}`
});
};
/**
* Handles generic Axios errors (fallback when network/HTTP handlers don't match)
*/
export const handleAxiosGenericError = (
err: AxiosError,
context: ErrorContext,
contextType: KubernetesAuthErrorContext
): BadRequestError => {
const { host, kubernetesHost } = context;
const target = host || kubernetesHost || "server";
const { defaultErrorName, serviceName } = ERROR_CONTEXT_CONFIGS[contextType];
return new BadRequestError({
name: defaultErrorName,
message: `Failed to communicate with ${serviceName} at ${target}: ${err.message}`
});
};
/**
* Checks if an error is a known error type that should be re-thrown as-is.
*/
export const isKnownError = (err: unknown): boolean => {
return err instanceof UnauthorizedError || err instanceof BadRequestError || err instanceof NotFoundError;
};
/**
* Comprehensive Axios error handler that processes network, HTTP, and generic errors.
* Returns an error to throw.
*/
export const handleAxiosError = (
err: AxiosError,
context: ErrorContext,
contextType: KubernetesAuthErrorContext
): BadRequestError | UnauthorizedError => {
const networkError = handleAxiosNetworkError(err, context, contextType);
if (networkError) {
return networkError;
}
const httpError = handleAxiosHttpError(err, contextType);
if (httpError) {
return httpError;
}
return handleAxiosGenericError(err, context, contextType);
};

View File

@@ -1,3 +1,5 @@
import { BadRequestError } from "@app/lib/errors";
/**
* Extracts the K8s service account name and namespace
* from the username in this format: system:serviceaccount:default:infisical-auth
@@ -11,5 +13,8 @@ export const extractK8sUsername = (username: string) => {
name: parts[3]
};
}
throw new Error("Invalid username format");
throw new BadRequestError({
name: "KubernetesUsernameParseError",
message: `Invalid Kubernetes service account username format: "${username}". Expected format: system:serviceaccount:<namespace>:<name>`
});
};

View File

@@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import axios, { AxiosError } from "axios";
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
import https from "https";
import RE2 from "re2";
@@ -28,6 +28,7 @@ import {
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { crypto } from "@app/lib/crypto";
import {
BadRequestError,
@@ -52,6 +53,7 @@ import { TMembershipIdentityDALFactory } from "../membership-identity/membership
import { TOrgDALFactory } from "../org/org-dal";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal";
import { handleAxiosError, isKnownError, KubernetesAuthErrorContext } from "./identity-kubernetes-auth-error-handlers";
import { extractK8sUsername } from "./identity-kubernetes-auth-fns";
import {
IdentityKubernetesAuthTokenReviewMode,
@@ -62,6 +64,11 @@ import {
TRevokeKubernetesAuthDTO,
TUpdateKubernetesAuthDTO
} from "./identity-kubernetes-auth-types";
import {
GatewayRequestExecutor,
validateKubernetesHostConnectivity,
validateTokenReviewerPermissions
} from "./identity-kubernetes-auth-validators";
type TIdentityKubernetesAuthServiceFactoryDep = {
identityDAL: Pick<TIdentityDALFactory, "findById">;
@@ -185,6 +192,70 @@ export const identityKubernetesAuthServiceFactory = ({
return callbackResult;
};
/**
* Supports two modes:
* - Gateway reviewer mode: Gateway uses its own service account (no kubernetesHost option)
* - API mode through gateway: Gateway proxies TCP connection to kubernetesHost (kubernetesHost option provided)
*/
const $createGatewayValidationRequest = (
gatewayId: string,
options?: { kubernetesHost?: string; caCert?: string }
): GatewayRequestExecutor => {
const useGatewayServiceAccount = !options?.kubernetesHost;
let targetHost: string | undefined;
let targetPort: number | undefined;
if (options?.kubernetesHost) {
const parsedUrl = new URL(options.kubernetesHost);
targetHost = parsedUrl.hostname;
targetPort = parsedUrl.port ? Number(parsedUrl.port) : 443;
}
return async <T>(method: "get" | "post", url: string, body?: object, headers?: Record<string, string>) => {
let response: AxiosResponse<T> | undefined;
await $gatewayProxyWrapper(
{
gatewayId,
reviewTokenThroughGateway: useGatewayServiceAccount,
targetHost,
targetPort,
caCert: options?.caCert
},
async (host: string, port: number, httpsAgent?: https.Agent) => {
const config: AxiosRequestConfig = {
headers: {
"Content-Type": "application/json",
...(useGatewayServiceAccount
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: headers)
},
timeout: 10000,
signal: AbortSignal.timeout(10000),
validateStatus: () => true,
...(httpsAgent ? { httpsAgent } : {})
};
if (method === "get") {
response = await request.get<T>(`${host}:${port}${url}`, config);
} else {
response = await request.post<T>(`${host}:${port}${url}`, body, config);
}
return response.data;
}
);
if (!response) {
throw new BadRequestError({
name: "GatewayConnectionError",
message: "Failed to get response from gateway"
});
}
return response;
};
};
const login = async ({ identityId, jwt: serviceAccountJwt, subOrganizationName }: TLoginKubernetesAuthDTO) => {
const appCfg = getConfig();
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
@@ -276,28 +347,37 @@ export const identityKubernetesAuthServiceFactory = ({
.catch((err) => {
const tokenReviewerJwtSnippet = `${tokenReviewerJwt?.substring?.(0, 10) || ""}...${tokenReviewerJwt?.substring?.(tokenReviewerJwt.length - 10) || ""}`;
const serviceAccountJwtSnippet = `${serviceAccountJwt?.substring?.(0, 10) || ""}...${serviceAccountJwt?.substring?.(serviceAccountJwt.length - 10) || ""}`;
if (err instanceof AxiosError) {
logger.error(
{ response: err.response, host, port, tokenReviewerJwtSnippet, serviceAccountJwtSnippet },
{
response: err.response,
host,
port,
tokenReviewerJwtSnippet,
serviceAccountJwtSnippet,
code: err.code
},
"tokenReviewCallbackRaw: Kubernetes token review request error (request error)"
);
if (err.response) {
const { message } = err?.response?.data as unknown as { message?: string };
if (message) {
throw new UnauthorizedError({
message,
name: "KubernetesTokenReviewRequestError"
});
}
}
} else {
logger.error(
{ error: err as Error, host, port, tokenReviewerJwtSnippet, serviceAccountJwtSnippet },
"tokenReviewCallbackRaw: Kubernetes token review request error (non-request error)"
);
throw handleAxiosError(err, { host, port }, KubernetesAuthErrorContext.KubernetesApiServer);
}
throw err;
logger.error(
{ error: err as Error, host, port, tokenReviewerJwtSnippet, serviceAccountJwtSnippet },
"tokenReviewCallbackRaw: Kubernetes token review request error (non-request error)"
);
if (isKnownError(err)) {
throw err;
}
throw new BadRequestError({
name: "KubernetesTokenReviewError",
message: (err as Error).message || "Unexpected error during token review",
error: err
});
});
return res.data;
@@ -335,23 +415,24 @@ export const identityKubernetesAuthServiceFactory = ({
}
)
.catch((err) => {
logger.error(
{ error: err as Error, host, port },
"tokenReviewCallbackThroughGateway: Kubernetes token review request error"
);
if (err instanceof AxiosError) {
if (err.response) {
let { message } = err?.response?.data as unknown as { message?: string };
if (!message && typeof err.response.data === "string") {
message = err.response.data;
}
if (message) {
throw new UnauthorizedError({
message,
name: "KubernetesTokenReviewRequestError"
});
}
}
throw handleAxiosError(err, { host, port }, KubernetesAuthErrorContext.GatewayProxy);
}
throw err;
if (isKnownError(err)) {
throw err;
}
throw new BadRequestError({
name: "GatewayTokenReviewError",
message: (err as Error).message || "Unexpected error during gateway token review",
error: err
});
});
return res.data;
@@ -571,7 +652,18 @@ export const identityKubernetesAuthServiceFactory = ({
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
if (isKnownError(error)) {
throw error;
}
logger.error({ error, identityId }, "Unexpected error during Kubernetes auth login");
throw new BadRequestError({
name: "KubernetesAuthLoginError",
message: (error as Error).message || "An unexpected error occurred during Kubernetes authentication",
error
});
}
};
@@ -691,6 +783,39 @@ export const identityKubernetesAuthServiceFactory = ({
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
if (tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) {
const gatewayExecutor = $createGatewayValidationRequest(gatewayId);
logger.info({ gatewayId }, "Validating gateway connectivity to Kubernetes");
await validateKubernetesHostConnectivity({ gatewayExecutor });
await validateTokenReviewerPermissions({ gatewayExecutor });
} else if (tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api && kubernetesHost) {
// API mode through gateway: gateway proxies requests with user's JWT
const gatewayExecutor = $createGatewayValidationRequest(gatewayId, {
kubernetesHost,
caCert: caCert || undefined
});
logger.info({ gatewayId, kubernetesHost }, "Validating Kubernetes connectivity through gateway");
await validateKubernetesHostConnectivity({ gatewayExecutor });
if (tokenReviewerJwt) {
await validateTokenReviewerPermissions({ gatewayExecutor, tokenReviewerJwt });
}
}
} else if (tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api && kubernetesHost) {
logger.info({ kubernetesHost }, "Validating Kubernetes host connectivity for new auth method");
await validateKubernetesHostConnectivity({
kubernetesHost,
caCert: caCert || undefined
});
if (tokenReviewerJwt) {
logger.info({ kubernetesHost }, "Validating token reviewer JWT permissions for new auth method");
await validateTokenReviewerPermissions({
kubernetesHost,
tokenReviewerJwt,
caCert: caCert || undefined
});
}
}
const { encryptor } = await kmsService.createCipherPairWithDataKey({
@@ -850,6 +975,75 @@ export const identityKubernetesAuthServiceFactory = ({
const gatewayIdValue = isGatewayV1 ? gatewayId : null;
const gatewayV2IdValue = isGatewayV1 ? null : gatewayId;
const effectiveTokenReviewMode = tokenReviewMode ?? identityKubernetesAuth.tokenReviewMode;
const effectiveKubernetesHost =
kubernetesHost !== undefined ? kubernetesHost : identityKubernetesAuth.kubernetesHost;
const effectiveGatewayId =
gatewayId !== undefined ? gatewayId : (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId);
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identityMembershipOrg.scopeOrgId
});
let effectiveCaCert: string | undefined;
if (caCert !== undefined) {
effectiveCaCert = caCert;
} else if (identityKubernetesAuth.encryptedKubernetesCaCertificate) {
effectiveCaCert = decryptor({
cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate
}).toString();
} else {
effectiveCaCert = undefined;
}
if (effectiveGatewayId) {
if (effectiveTokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) {
const gatewayExecutor = $createGatewayValidationRequest(effectiveGatewayId);
logger.info(
{ gatewayId: effectiveGatewayId },
"Validating gateway connectivity to Kubernetes for auth method update"
);
await validateKubernetesHostConnectivity({ gatewayExecutor });
await validateTokenReviewerPermissions({ gatewayExecutor });
} else if (effectiveTokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api && effectiveKubernetesHost) {
const gatewayExecutor = $createGatewayValidationRequest(effectiveGatewayId, {
kubernetesHost: effectiveKubernetesHost,
caCert: effectiveCaCert
});
logger.info(
{ gatewayId: effectiveGatewayId, kubernetesHost: effectiveKubernetesHost },
"Validating Kubernetes connectivity through gateway for auth method update"
);
await validateKubernetesHostConnectivity({ gatewayExecutor });
if (tokenReviewerJwt) {
await validateTokenReviewerPermissions({ gatewayExecutor, tokenReviewerJwt });
}
}
} else if (effectiveTokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) {
if (kubernetesHost) {
logger.info({ kubernetesHost }, "Validating Kubernetes host connectivity for auth method update");
await validateKubernetesHostConnectivity({
kubernetesHost,
caCert: effectiveCaCert
});
}
if (tokenReviewerJwt && effectiveKubernetesHost) {
logger.info(
{ kubernetesHost: effectiveKubernetesHost },
"Validating token reviewer JWT permissions for auth method update"
);
await validateTokenReviewerPermissions({
kubernetesHost: effectiveKubernetesHost,
tokenReviewerJwt,
caCert: effectiveCaCert
});
}
}
const updateQuery: TIdentityKubernetesAuthsUpdate = {
kubernetesHost,
tokenReviewMode,
@@ -866,11 +1060,6 @@ export const identityKubernetesAuthServiceFactory = ({
: undefined
};
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identityMembershipOrg.scopeOrgId
});
if (caCert !== undefined) {
updateQuery.encryptedKubernetesCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob;
}

View File

@@ -0,0 +1,229 @@
import { AxiosError, AxiosResponse } from "axios";
import https from "https";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { handleAxiosError, KubernetesAuthErrorContext } from "./identity-kubernetes-auth-error-handlers";
export type GatewayRequestExecutor = <T>(
method: "get" | "post",
url: string,
body?: object,
headers?: Record<string, string>
) => Promise<AxiosResponse<T>>;
/**
* Validates that the Kubernetes host is reachable by making a simple HTTPS request.
* This does not validate credentials, just network connectivity.
*
* Supports two modes:
* - API mode: Direct call to Kubernetes API (default)
* - Gateway mode: Call through gateway using gatewayExecutor
*/
export const validateKubernetesHostConnectivity = async ({
kubernetesHost,
caCert,
gatewayExecutor
}: {
kubernetesHost?: string;
caCert?: string;
gatewayExecutor?: GatewayRequestExecutor;
}): Promise<void> => {
const isGatewayMode = Boolean(gatewayExecutor);
const logContext = isGatewayMode ? { context: "gateway" } : { kubernetesHost };
const errorContext = isGatewayMode
? KubernetesAuthErrorContext.GatewayProxy
: KubernetesAuthErrorContext.KubernetesHost;
try {
let response: AxiosResponse;
if (gatewayExecutor) {
response = await gatewayExecutor("get", "/version");
} else {
if (!kubernetesHost) {
throw new BadRequestError({
name: "KubernetesHostConnectionError",
message: "Kubernetes host is required for API mode validation"
});
}
const httpsAgent = new https.Agent({
ca: caCert || undefined,
rejectUnauthorized: Boolean(caCert)
});
await blockLocalAndPrivateIpAddresses(kubernetesHost);
response = await request.get(`${kubernetesHost}/version`, {
httpsAgent,
timeout: 10000,
signal: AbortSignal.timeout(10000),
validateStatus: () => true
});
}
if (response.status >= 500) {
throw new BadRequestError({
name: isGatewayMode ? "GatewayConnectionError" : "KubernetesHostConnectionError",
message: `Kubernetes API returned server error: ${response.status} - ${response.statusText}`
});
}
logger.info(logContext, "Kubernetes host connectivity validated successfully");
} catch (err) {
if (err instanceof BadRequestError) {
throw err;
}
const error = err as Error;
logger.error({ error, ...logContext }, "Failed to connect to Kubernetes host");
if (err instanceof AxiosError) {
throw handleAxiosError(err, { kubernetesHost }, errorContext);
}
throw new BadRequestError({
name: isGatewayMode ? "GatewayConnectionError" : "KubernetesHostConnectionError",
message: isGatewayMode
? `Failed to connect to Kubernetes through gateway: ${error.message}`
: `Failed to connect to Kubernetes host at ${kubernetesHost}: ${error.message}`,
error
});
}
};
/**
* Validates that the token reviewer has the necessary permissions to perform token reviews.
* This is done by making a TokenReview request with a fake token to verify RBAC permissions
* without authenticating a real workload.
*
* Supports three modes:
* - API mode: Direct call to Kubernetes API using tokenReviewerJwt
* - Gateway mode (gateway reviewer): Gateway uses its own service account
* - Gateway mode (API reviewer): Gateway proxies request with user-provided tokenReviewerJwt
*/
export const validateTokenReviewerPermissions = async ({
kubernetesHost,
tokenReviewerJwt,
caCert,
gatewayExecutor
}: {
kubernetesHost?: string;
tokenReviewerJwt?: string;
caCert?: string;
gatewayExecutor?: GatewayRequestExecutor;
}): Promise<void> => {
const isGatewayMode = Boolean(gatewayExecutor);
const isGatewayWithUserJwt = isGatewayMode && Boolean(tokenReviewerJwt);
const logContext = isGatewayMode ? { context: "gateway" } : { kubernetesHost };
const errorContext = isGatewayMode
? KubernetesAuthErrorContext.GatewayProxy
: KubernetesAuthErrorContext.KubernetesApiServer;
let errorNamePrefix = "TokenReviewer";
if (isGatewayMode && !isGatewayWithUserJwt) {
errorNamePrefix = "GatewayTokenReview";
}
try {
const testToken = "test-token-for-permission-validation";
const tokenReviewBody = {
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: testToken
}
};
let response: AxiosResponse;
if (gatewayExecutor) {
// Gateway mode: optionally pass user JWT if provided (API mode through gateway)
const headers = tokenReviewerJwt ? { Authorization: `Bearer ${tokenReviewerJwt}` } : undefined;
response = await gatewayExecutor("post", "/apis/authentication.k8s.io/v1/tokenreviews", tokenReviewBody, headers);
} else {
// Direct API mode: call Kubernetes API directly
if (!kubernetesHost || !tokenReviewerJwt) {
throw new BadRequestError({
name: `${errorNamePrefix}PermissionError`,
message: "Kubernetes host and token reviewer JWT are required for API mode validation"
});
}
const httpsAgent = new https.Agent({
ca: caCert || undefined,
rejectUnauthorized: Boolean(caCert)
});
await blockLocalAndPrivateIpAddresses(kubernetesHost);
response = await request.post(`${kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`, tokenReviewBody, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
httpsAgent,
timeout: 10000,
signal: AbortSignal.timeout(10000),
validateStatus: () => true
});
}
if (response.status === 401) {
throw new BadRequestError({
name: `${errorNamePrefix}PermissionError`,
message:
isGatewayMode && !isGatewayWithUserJwt
? "Gateway service account is not authorized. Verify the gateway is deployed correctly and has a valid service account."
: "The token reviewer JWT is invalid or expired. Please provide a valid service account token with TokenReview permissions."
});
}
if (response.status === 403) {
const errorMessage =
(response.data as { message?: string })?.message ||
(isGatewayMode && !isGatewayWithUserJwt
? "Gateway service account does not have permission to perform TokenReviews."
: "The token reviewer JWT does not have permission to perform TokenReviews.");
throw new BadRequestError({
name: `${errorNamePrefix}PermissionError`,
message: `${errorMessage}. Ensure the service account has the 'system:auth-delegator' ClusterRole binding.`
});
}
if (response.status >= 200 && response.status < 300) {
const data = response.data as { status?: { authenticated?: boolean; error?: string } };
logger.info(
{ ...logContext, authenticated: data?.status?.authenticated },
"Token reviewer permission validation successful"
);
return;
}
const errorMessage = (response.data as { message?: string })?.message || response.statusText;
throw new BadRequestError({
name: `${errorNamePrefix}PermissionError`,
message: `Unexpected response from Kubernetes API: ${response.status} - ${errorMessage}`
});
} catch (err) {
if (err instanceof BadRequestError) {
throw err;
}
const error = err as Error;
logger.error({ error, ...logContext }, "Failed to validate token reviewer permissions");
if (err instanceof AxiosError) {
throw handleAxiosError(err, { kubernetesHost }, errorContext);
}
throw new BadRequestError({
name: `${errorNamePrefix}PermissionError`,
message: `Failed to validate token reviewer permissions: ${error.message}`
});
}
};