mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 07:29:35 +00:00
feat: add dynamic secret kube auth vault migration
This commit is contained in:
@@ -393,6 +393,56 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/vault/kubernetes-roles",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
namespace: z.string(),
|
||||
mountPath: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
roles: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
mountPath: z.string(),
|
||||
allowed_kubernetes_namespaces: z.array(z.string()).nullish(),
|
||||
allowed_kubernetes_namespace_selector: z.string().nullish(),
|
||||
token_max_ttl: z.number().nullish(),
|
||||
token_default_ttl: z.number().nullish(),
|
||||
token_default_audiences: z.array(z.string()).nullish(),
|
||||
service_account_name: z.string().nullish(),
|
||||
kubernetes_role_name: z.string().nullish(),
|
||||
kubernetes_role_type: z.string().nullish(),
|
||||
generated_role_rules: z.string().nullish(),
|
||||
name_template: z.string().nullish(),
|
||||
extra_annotations: z.record(z.string()).nullish(),
|
||||
extra_labels: z.record(z.string()).nullish(),
|
||||
config: z.object({
|
||||
kubernetes_host: z.string(),
|
||||
kubernetes_ca_cert: z.string().nullish()
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const roles = await server.services.migration.getVaultKubernetesRoles({
|
||||
actor: req.permission,
|
||||
namespace: req.query.namespace,
|
||||
mountPath: req.query.mountPath
|
||||
});
|
||||
|
||||
return { roles };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/vault/secret-paths",
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
THCVaultKubernetesAuthConfig,
|
||||
THCVaultKubernetesAuthRole,
|
||||
THCVaultKubernetesAuthRoleWithConfig,
|
||||
THCVaultKubernetesRole,
|
||||
THCVaultKubernetesSecretsConfig,
|
||||
THCVaultMount,
|
||||
THCVaultMountResponse
|
||||
} from "./hc-vault-connection-types";
|
||||
@@ -799,3 +801,122 @@ export const getHCVaultKubernetesAuthRoles = async (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getHCVaultKubernetesRoles = async (
|
||||
namespace: string,
|
||||
mountPath: string,
|
||||
connection: THCVaultConnection,
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||
): Promise<THCVaultKubernetesRole[]> => {
|
||||
// Remove trailing slash from mount path
|
||||
const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath;
|
||||
|
||||
try {
|
||||
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
||||
// 1. Get the Kubernetes secrets engine configuration for this mount
|
||||
const { data: configResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesSecretsConfig }>(
|
||||
connection,
|
||||
gatewayService,
|
||||
{
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/config`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const kubernetesConfig = configResponse.data;
|
||||
|
||||
// 2. List all roles in this mount
|
||||
let roleNames: string[] = [];
|
||||
try {
|
||||
const { data: roleListResponse } = await requestWithHCVaultGateway<{ data: { keys: string[] } }>(
|
||||
connection,
|
||||
gatewayService,
|
||||
{
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/roles?list=true`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
}
|
||||
);
|
||||
roleNames = roleListResponse.data.keys || [];
|
||||
} catch (error) {
|
||||
// Vault returns 404 when no roles are configured yet
|
||||
if (error && typeof error === "object" && "response" in error) {
|
||||
const axiosError = error as { response?: { status?: number } };
|
||||
if (axiosError.response?.status === 404) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!roleNames || roleNames.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 3. Fetch details for each role with concurrency control
|
||||
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
|
||||
|
||||
const roleDetailsPromises = roleNames.map((roleName) =>
|
||||
limiter(async () => {
|
||||
const { data: roleResponse } = await requestWithHCVaultGateway<{
|
||||
data: {
|
||||
allowed_kubernetes_namespaces?: string[];
|
||||
allowed_kubernetes_namespace_selector?: string;
|
||||
token_max_ttl?: number;
|
||||
token_default_ttl?: number;
|
||||
token_default_audiences?: string[];
|
||||
service_account_name?: string;
|
||||
kubernetes_role_name?: string;
|
||||
kubernetes_role_type?: string;
|
||||
generated_role_rules?: string;
|
||||
name_template?: string;
|
||||
extra_annotations?: Record<string, string>;
|
||||
extra_labels?: Record<string, string>;
|
||||
};
|
||||
}>(connection, gatewayService, {
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/roles/${roleName}`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Merge the role with the config
|
||||
return {
|
||||
...roleResponse.data,
|
||||
name: roleName,
|
||||
config: kubernetesConfig,
|
||||
mountPath: cleanMountPath
|
||||
} as THCVaultKubernetesRole;
|
||||
})
|
||||
);
|
||||
|
||||
const roles = await Promise.all(roleDetailsPromises);
|
||||
|
||||
return roles;
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Unable to list HC Vault Kubernetes secrets engine roles");
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
const errorMessage =
|
||||
(error.response?.data as { errors?: string[] })?.errors?.[0] || error.message || "Unknown error";
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list Kubernetes secrets engine roles: ${errorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list Kubernetes secrets engine roles from HashiCorp Vault"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,3 +95,26 @@ export type THCVaultKubernetesAuthRoleWithConfig = THCVaultKubernetesAuthRole &
|
||||
config: THCVaultKubernetesAuthConfig;
|
||||
mountPath: string;
|
||||
};
|
||||
|
||||
export type THCVaultKubernetesSecretsConfig = {
|
||||
kubernetes_host: string;
|
||||
kubernetes_ca_cert?: string;
|
||||
};
|
||||
|
||||
export type THCVaultKubernetesRole = {
|
||||
name: string;
|
||||
allowed_kubernetes_namespaces?: string[];
|
||||
allowed_kubernetes_namespace_selector?: string;
|
||||
token_max_ttl?: number;
|
||||
token_default_ttl?: number;
|
||||
token_default_audiences?: string[];
|
||||
service_account_name?: string;
|
||||
kubernetes_role_name?: string;
|
||||
kubernetes_role_type?: string;
|
||||
generated_role_rules?: string;
|
||||
name_template?: string;
|
||||
extra_annotations?: Record<string, string>;
|
||||
extra_labels?: Record<string, string>;
|
||||
config: THCVaultKubernetesSecretsConfig;
|
||||
mountPath: string;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import { TAppConnectionServiceFactory } from "../app-connection/app-connection-s
|
||||
import {
|
||||
getHCVaultAuthMounts,
|
||||
getHCVaultKubernetesAuthRoles,
|
||||
getHCVaultKubernetesRoles,
|
||||
getHCVaultSecretsForPath,
|
||||
HCVaultAuthType,
|
||||
listHCVaultMounts,
|
||||
@@ -747,6 +748,55 @@ export const externalMigrationServiceFactory = ({
|
||||
return roles;
|
||||
};
|
||||
|
||||
const getVaultKubernetesRoles = async ({
|
||||
actor,
|
||||
namespace,
|
||||
mountPath
|
||||
}: {
|
||||
actor: OrgServiceActor;
|
||||
namespace: string;
|
||||
mountPath: string;
|
||||
}) => {
|
||||
const { hasRole } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
if (!hasRole(OrgMembershipRole.Admin)) {
|
||||
throw new ForbiddenRequestError({ message: "Only admins can get Kubernetes roles" });
|
||||
}
|
||||
|
||||
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
|
||||
orgId: actor.orgId,
|
||||
namespace
|
||||
});
|
||||
|
||||
if (!vaultConfig) {
|
||||
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
|
||||
}
|
||||
|
||||
if (!vaultConfig.connection) {
|
||||
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
|
||||
}
|
||||
|
||||
const credentials = await decryptAppConnectionCredentials({
|
||||
orgId: vaultConfig.orgId,
|
||||
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
|
||||
kmsService,
|
||||
projectId: null
|
||||
});
|
||||
|
||||
const connection = {
|
||||
...vaultConfig.connection,
|
||||
credentials
|
||||
} as THCVaultConnection;
|
||||
|
||||
return getHCVaultKubernetesRoles(namespace, mountPath, connection, gatewayService);
|
||||
};
|
||||
|
||||
return {
|
||||
importEnvKeyData,
|
||||
importVaultData,
|
||||
@@ -761,6 +811,7 @@ export const externalMigrationServiceFactory = ({
|
||||
getVaultAuthMounts,
|
||||
getVaultSecretPaths,
|
||||
importVaultSecrets,
|
||||
getVaultKubernetesAuthRoles
|
||||
getVaultKubernetesAuthRoles,
|
||||
getVaultKubernetesRoles
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user