Merge pull request #4897 from Infisical/feat/PKI-29

Add Chef PKI sync
This commit is contained in:
carlosmonastyrski
2025-11-20 17:28:40 -03:00
committed by GitHub
100 changed files with 3409 additions and 72 deletions
@@ -1,7 +1,12 @@
import { seedData1 } from "@app/db/seed-data";
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
const createPolicy = async (dto: { name: string; secretPath: string; approvers: {type: ApproverType.User, id: string}[]; approvals: number }) => {
const createPolicy = async (dto: {
name: string;
secretPath: string;
approvers: { type: ApproverType.User; id: string }[];
approvals: number;
}) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/secret-approvals`,
@@ -27,7 +32,7 @@ describe("Secret approval policy router", async () => {
const policy = await createPolicy({
secretPath: "/",
approvals: 1,
approvers: [{id:seedData1.id, type: ApproverType.User}],
approvers: [{ id: seedData1.id, type: ApproverType.User }],
name: "test-policy"
});
+1 -1
View File
@@ -2,7 +2,7 @@
import { execSync } from "child_process";
import path from "path";
import promptSync from "prompt-sync";
import slugify from "@sindresorhus/slugify"
import slugify from "@sindresorhus/slugify";
const prompt = promptSync({ sigint: true });
@@ -27,6 +27,17 @@ export const getChefServerUrl = async (serverUrl?: string) => {
return chefServerUrl;
};
const buildSecureUrl = (baseUrl: string, path: string): string => {
try {
const url = new URL(path, baseUrl);
return url.toString();
} catch (error) {
throw new BadRequestError({
message: "Invalid URL construction parameters"
});
}
};
// Helper to ensure private key is in proper PEM format
const formatPrivateKey = (key: string): string => {
let formattedKey = key.trim();
@@ -138,7 +149,8 @@ export const validateChefConnectionCredentials = async (config: TChefConnectionC
const headers = getChefAuthHeaders("GET", path, "", inputCredentials.userName, inputCredentials.privateKey);
await request.get(`${hostServerUrl}${path}`, {
const secureUrl = buildSecureUrl(hostServerUrl, path);
await request.get(secureUrl, {
headers
});
} catch (error: unknown) {
@@ -168,7 +180,8 @@ export const listChefDataBags = async (appConnection: TChefConnection): Promise<
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
const res = await request.get<Record<string, string>>(`${hostServerUrl}${path}`, {
const secureUrl = buildSecureUrl(hostServerUrl, path);
const res = await request.get<Record<string, string>>(secureUrl, {
headers
});
@@ -203,7 +216,8 @@ export const listChefDataBagItems = async (
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
const res = await request.get<Record<string, string>>(`${hostServerUrl}${path}`, {
const secureUrl = buildSecureUrl(hostServerUrl, path);
const res = await request.get<Record<string, string>>(secureUrl, {
headers
});
@@ -238,7 +252,8 @@ export const getChefDataBagItem = async ({
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
const res = await request.get<TChefDataBagItemContent>(`${hostServerUrl}${path}`, {
const secureUrl = buildSecureUrl(hostServerUrl, path);
const res = await request.get<TChefDataBagItemContent>(secureUrl, {
headers
});
@@ -255,6 +270,38 @@ export const getChefDataBagItem = async ({
}
};
export const createChefDataBagItem = async ({
serverUrl,
userName,
privateKey,
orgName,
dataBagName,
data
}: Omit<TUpdateChefDataBagItem, "dataBagItemName">): Promise<void> => {
try {
const path = `/organizations/${orgName}/data/${dataBagName}`;
const body = JSON.stringify(data);
const hostServerUrl = await getChefServerUrl(serverUrl);
const headers = getChefAuthHeaders("POST", path, body, userName, privateKey);
const secureUrl = buildSecureUrl(hostServerUrl, path);
await request.post(secureUrl, data, {
headers
});
} catch (error) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to create Chef data bag item: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to create Chef data bag item"
});
}
};
export const updateChefDataBagItem = async ({
serverUrl,
userName,
@@ -272,7 +319,8 @@ export const updateChefDataBagItem = async ({
const headers = getChefAuthHeaders("PUT", path, body, userName, privateKey);
await request.put(`${hostServerUrl}${path}`, data, {
const secureUrl = buildSecureUrl(hostServerUrl, path);
await request.put(secureUrl, data, {
headers
});
} catch (error) {
@@ -286,3 +334,35 @@ export const updateChefDataBagItem = async ({
});
}
};
export const removeChefDataBagItem = async ({
serverUrl,
userName,
privateKey,
orgName,
dataBagName,
dataBagItemName
}: Omit<TUpdateChefDataBagItem, "data">): Promise<void> => {
try {
const path = `/organizations/${orgName}/data/${dataBagName}/${dataBagItemName}`;
const body = "";
const hostServerUrl = await getChefServerUrl(serverUrl);
const headers = getChefAuthHeaders("DELETE", path, body, userName, privateKey);
const secureUrl = buildSecureUrl(hostServerUrl, path);
await request.delete(secureUrl, {
headers
});
} catch (error) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to remove Chef data bag item: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to remove Chef data bag item"
});
}
};
@@ -0,0 +1,22 @@
import {
AWS_SECRETS_MANAGER_PKI_SYNC_LIST_OPTION,
AwsSecretsManagerPkiSyncSchema,
CreateAwsSecretsManagerPkiSyncSchema,
UpdateAwsSecretsManagerPkiSyncSchema
} from "@app/services/pki-sync/aws-secrets-manager";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { registerSyncPkiEndpoints } from "./pki-sync-endpoints";
export const registerAwsSecretsManagerPkiSyncRouter = async (server: FastifyZodProvider) =>
registerSyncPkiEndpoints({
destination: PkiSync.AwsSecretsManager,
server,
responseSchema: AwsSecretsManagerPkiSyncSchema,
createSchema: CreateAwsSecretsManagerPkiSyncSchema,
updateSchema: UpdateAwsSecretsManagerPkiSyncSchema,
syncOptions: {
canImportCertificates: AWS_SECRETS_MANAGER_PKI_SYNC_LIST_OPTION.canImportCertificates,
canRemoveCertificates: AWS_SECRETS_MANAGER_PKI_SYNC_LIST_OPTION.canRemoveCertificates
}
});
@@ -0,0 +1,17 @@
import { ChefPkiSyncSchema, CreateChefPkiSyncSchema, UpdateChefPkiSyncSchema } from "@app/services/pki-sync/chef";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { registerSyncPkiEndpoints } from "./pki-sync-endpoints";
export const registerChefPkiSyncRouter = async (server: FastifyZodProvider) =>
registerSyncPkiEndpoints({
destination: PkiSync.Chef,
server,
responseSchema: ChefPkiSyncSchema,
createSchema: CreateChefPkiSyncSchema,
updateSchema: UpdateChefPkiSyncSchema,
syncOptions: {
canImportCertificates: false,
canRemoveCertificates: true
}
});
@@ -1,11 +1,15 @@
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { registerAwsCertificateManagerPkiSyncRouter } from "./aws-certificate-manager-pki-sync-router";
import { registerAwsSecretsManagerPkiSyncRouter } from "./aws-secrets-manager-pki-sync-router";
import { registerAzureKeyVaultPkiSyncRouter } from "./azure-key-vault-pki-sync-router";
import { registerChefPkiSyncRouter } from "./chef-pki-sync-router";
export * from "./pki-sync-router";
export const PKI_SYNC_REGISTER_ROUTER_MAP: Record<PkiSync, (server: FastifyZodProvider) => Promise<void>> = {
[PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter,
[PkiSync.AwsCertificateManager]: registerAwsCertificateManagerPkiSyncRouter
[PkiSync.AwsCertificateManager]: registerAwsCertificateManagerPkiSyncRouter,
[PkiSync.AwsSecretsManager]: registerAwsSecretsManagerPkiSyncRouter,
[PkiSync.Chef]: registerChefPkiSyncRouter
};
@@ -23,6 +23,8 @@ import { mapEnumsForValidation } from "@app/services/certificate-common/certific
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
import { booleanSchema } from "../sanitizedSchemas";
interface CertificateRequestForService {
commonName?: string;
keyUsages?: CertKeyUsageType[];
@@ -87,7 +89,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
)
.optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
removeRootsFromChain: booleanSchema.default(false).optional()
})
.refine(validateTtlAndDateFields, {
message:
@@ -131,7 +134,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.body.profileId,
certificateRequest: mappedCertificateRequest
certificateRequest: mappedCertificateRequest,
removeRootsFromChain: req.body.removeRootsFromChain
});
await server.services.auditLog.createAuditLog({
@@ -171,7 +175,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
.min(1, "TTL cannot be empty")
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional()
notAfter: validateCaDateField.optional(),
removeRootsFromChain: booleanSchema.default(false).optional()
})
.refine(validateTtlAndDateFields, {
message:
@@ -206,7 +211,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
},
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
enrollmentType: EnrollmentType.API
enrollmentType: EnrollmentType.API,
removeRootsFromChain: req.body.removeRootsFromChain
});
await server.services.auditLog.createAuditLog({
@@ -262,7 +268,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
notAfter: validateCaDateField.optional(),
commonName: validateTemplateRegexField.optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
removeRootsFromChain: booleanSchema.default(false).optional()
})
.refine(validateTtlAndDateFields, {
message:
@@ -325,7 +332,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
signatureAlgorithm: req.body.signatureAlgorithm,
keyAlgorithm: req.body.keyAlgorithm
}
},
removeRootsFromChain: req.body.removeRootsFromChain
});
await server.services.auditLog.createAuditLog({
@@ -357,6 +365,11 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
params: z.object({
certificateId: z.string().uuid()
}),
body: z
.object({
removeRootsFromChain: booleanSchema.default(false).optional()
})
.optional(),
response: {
200: z.object({
certificate: z.string().trim(),
@@ -375,7 +388,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
certificateId: req.params.certificateId
certificateId: req.params.certificateId,
removeRootsFromChain: req.body?.removeRootsFromChain
});
await server.services.auditLog.createAuditLog({
@@ -170,7 +170,8 @@ const PKI_APP_CONNECTIONS = [
AppConnection.AWS,
AppConnection.Cloudflare,
AppConnection.AzureADCS,
AppConnection.AzureKeyVault
AppConnection.AzureKeyVault,
AppConnection.Chef
];
export const listAppConnectionOptions = (projectType?: ProjectType) => {
@@ -196,3 +196,62 @@ export const convertExtendedKeyUsageArrayToLegacy = (
): CertExtendedKeyUsage[] | undefined => {
return usages?.map(convertToLegacyExtendedKeyUsage);
};
/**
* Parses a PEM-formatted certificate chain and returns individual certificates
* @param certificateChain - PEM-formatted certificate chain
* @returns Array of individual PEM certificates
*/
const parseCertificateChain = (certificateChain: string): string[] => {
if (!certificateChain || typeof certificateChain !== "string") {
return [];
}
const certRegex = new RE2(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
const certificates = certificateChain.match(certRegex);
return certificates ? certificates.map((cert) => cert.trim()) : [];
};
/**
* Removes the root CA certificate from a certificate chain, leaving only intermediate certificates.
* If the chain contains only the root CA certificate, returns an empty string.
*
* @param certificateChain - PEM-formatted certificate chain containing leaf + intermediates + root CA
* @returns PEM-formatted certificate chain with only intermediate certificates (no root CA)
*/
export const removeRootCaFromChain = (certificateChain?: string): string => {
if (!certificateChain || typeof certificateChain !== "string") {
return "";
}
const certificates = parseCertificateChain(certificateChain);
if (certificates.length === 0) {
return "";
}
const intermediateCerts = certificates.slice(0, -1);
return intermediateCerts.join("\n");
};
/**
* Extracts the root CA certificate from a certificate chain.
*
* @param certificateChain - PEM-formatted certificate chain containing leaf + intermediates + root CA
* @returns PEM-formatted root CA certificate, or empty string if not found
*/
export const extractRootCaFromChain = (certificateChain?: string): string => {
if (!certificateChain || typeof certificateChain !== "string") {
return "";
}
const certificates = parseCertificateChain(certificateChain);
if (certificates.length === 0) {
return "";
}
return certificates[certificates.length - 1];
};
@@ -47,7 +47,8 @@ import {
convertKeyUsageArrayFromLegacy,
convertKeyUsageArrayToLegacy,
mapEnumsForValidation,
normalizeDateForApi
normalizeDateForApi,
removeRootCaFromChain
} from "../certificate-common/certificate-utils";
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
@@ -366,7 +367,8 @@ export const certificateV3ServiceFactory = ({
actor,
actorId,
actorAuthMethod,
actorOrgId
actorOrgId,
removeRootsFromChain
}: TIssueCertificateFromProfileDTO): Promise<TCertificateFromProfileResponse> => {
const profile = await validateProfileAndPermissions(
profileId,
@@ -480,10 +482,15 @@ export const certificateV3ServiceFactory = ({
renewBeforeDays: finalRenewBeforeDays
});
let finalCertificateChain = bufferToString(certificateChain);
if (removeRootsFromChain) {
finalCertificateChain = removeRootCaFromChain(finalCertificateChain);
}
return {
certificate: bufferToString(certificate),
issuingCaCertificate: bufferToString(issuingCaCertificate),
certificateChain: bufferToString(certificateChain),
certificateChain: finalCertificateChain,
privateKey: bufferToString(privateKey),
serialNumber,
certificateId: cert.id,
@@ -503,7 +510,8 @@ export const certificateV3ServiceFactory = ({
actorId,
actorAuthMethod,
actorOrgId,
enrollmentType
enrollmentType,
removeRootsFromChain
}: TSignCertificateFromProfileDTO): Promise<Omit<TCertificateFromProfileResponse, "privateKey">> => {
const profile = await validateProfileAndPermissions(
profileId,
@@ -590,7 +598,10 @@ export const certificateV3ServiceFactory = ({
});
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
const certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
if (removeRootsFromChain) {
certificateChainString = removeRootCaFromChain(certificateChainString);
}
return {
certificate: certificateString,
@@ -610,7 +621,8 @@ export const certificateV3ServiceFactory = ({
actor,
actorId,
actorAuthMethod,
actorOrgId
actorOrgId,
removeRootsFromChain
}: TOrderCertificateFromProfileDTO): Promise<TCertificateOrderResponse> => {
const profile = await validateProfileAndPermissions(
profileId,
@@ -665,7 +677,8 @@ export const certificateV3ServiceFactory = ({
actor,
actorId,
actorAuthMethod,
actorOrgId
actorOrgId,
removeRootsFromChain
});
const orderId = randomUUID();
@@ -703,7 +716,8 @@ export const certificateV3ServiceFactory = ({
actorId,
actorAuthMethod,
actorOrgId,
internal = false
internal = false,
removeRootsFromChain
}: TRenewCertificateDTO & { internal?: boolean }): Promise<TCertificateFromProfileResponse> => {
const renewalResult = await certificateDAL.transaction(async (tx) => {
const originalCert = await certificateDAL.findById(certificateId, tx);
@@ -929,10 +943,14 @@ export const certificateV3ServiceFactory = ({
pkiSyncQueue
});
let finalCertificateChain = renewalResult.certificateChain;
if (removeRootsFromChain) {
finalCertificateChain = removeRootCaFromChain(finalCertificateChain);
}
return {
certificate: renewalResult.certificate,
issuingCaCertificate: renewalResult.issuingCaCertificate,
certificateChain: renewalResult.certificateChain,
certificateChain: finalCertificateChain,
serialNumber: renewalResult.serialNumber,
certificateId: renewalResult.newCert.id,
projectId: renewalResult.profile.projectId,
@@ -26,6 +26,7 @@ export type TIssueCertificateFromProfileDTO = {
signatureAlgorithm?: string;
keyAlgorithm?: string;
};
removeRootsFromChain?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TSignCertificateFromProfileDTO = {
@@ -37,6 +38,7 @@ export type TSignCertificateFromProfileDTO = {
notBefore?: Date;
notAfter?: Date;
enrollmentType: EnrollmentType;
removeRootsFromChain?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TOrderCertificateFromProfileDTO = {
@@ -57,6 +59,7 @@ export type TOrderCertificateFromProfileDTO = {
signatureAlgorithm?: string;
keyAlgorithm?: string;
};
removeRootsFromChain?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TCertificateFromProfileResponse = {
@@ -101,6 +104,7 @@ export type TCertificateOrderResponse = {
export type TRenewCertificateDTO = {
certificateId: string;
removeRootsFromChain?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateRenewalConfigDTO = {
@@ -14,6 +14,7 @@ export const AwsCertificateManagerPkiSyncConfigSchema = z.object({
const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(true),
includeRootCa: z.boolean().default(false),
preserveArn: z.boolean().default(true),
certificateNameSchema: z
.string()
@@ -0,0 +1,71 @@
import RE2 from "re2";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
/**
* AWS Secrets Manager naming constraints for secrets
*/
export const AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING = {
/**
* Regular expression pattern for valid AWS Secrets Manager secret names
* Must contain only alphanumeric characters, hyphens, and underscores
* Must be 1-512 characters long
*/
NAME_PATTERN: new RE2("^[\\w-]+$"),
/**
* String of characters that are forbidden in AWS Secrets Manager secret names
*/
FORBIDDEN_CHARACTERS: " @#$%^&*()+=[]{}|;':\"<>?,./",
/**
* Minimum length for secret names in AWS Secrets Manager
*/
MIN_LENGTH: 1,
/**
* Maximum length for secret names in AWS Secrets Manager
*/
MAX_LENGTH: 512,
/**
* String representation of the allowed character pattern (for UI display)
*/
ALLOWED_CHARACTER_PATTERN: "^[\\w-]+$"
} as const;
export const AWS_SECRETS_MANAGER_PKI_SYNC_DEFAULTS = {
INFISICAL_PREFIX: "infisical-",
DEFAULT_ENVIRONMENT: "production",
DEFAULT_CERTIFICATE_NAME_SCHEMA: "infisical-{{certificateId}}",
DEFAULT_FIELD_MAPPINGS: {
certificate: "certificate",
privateKey: "private_key",
certificateChain: "certificate_chain",
caCertificate: "ca_certificate"
}
};
export const AWS_SECRETS_MANAGER_PKI_SYNC_OPTIONS = {
DEFAULT_CAN_REMOVE_CERTIFICATES: true,
DEFAULT_PRESERVE_SECRET_ON_RENEWAL: true,
DEFAULT_UPDATE_EXISTING_CERTIFICATES: true,
DEFAULT_CAN_IMPORT_CERTIFICATES: false
};
/**
* AWS Secrets Manager PKI Sync list option configuration
*/
export const AWS_SECRETS_MANAGER_PKI_SYNC_LIST_OPTION = {
name: "AWS Secrets Manager" as const,
connection: AppConnection.AWS,
destination: PkiSync.AwsSecretsManager,
canImportCertificates: false,
canRemoveCertificates: true,
defaultCertificateNameSchema: "infisical-{{certificateId}}",
forbiddenCharacters: AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS,
allowedCharacterPattern: AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.ALLOWED_CHARACTER_PATTERN,
maxCertificateNameLength: AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.MAX_LENGTH,
minCertificateNameLength: AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.MIN_LENGTH
} as const;
@@ -0,0 +1,555 @@
/* eslint-disable no-continue */
/* eslint-disable no-await-in-loop */
import {
CreateSecretCommand,
DeleteSecretCommand,
ListSecretsCommand,
SecretsManagerClient,
UpdateSecretCommand
} from "@aws-sdk/client-secrets-manager";
import RE2 from "re2";
import { TCertificateSyncs } from "@app/db/schemas";
import { CustomAWSHasher } from "@app/lib/aws/hashing";
import { crypto } from "@app/lib/crypto";
import { logger } from "@app/lib/logger";
import { AWSRegion } from "@app/services/app-connection/app-connection-enums";
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
import { matchesCertificateNameSchema } from "@app/services/pki-sync/pki-sync-fns";
import { TCertificateMap, TPkiSyncWithCredentials } from "@app/services/pki-sync/pki-sync-types";
import { AWS_SECRETS_MANAGER_PKI_SYNC_DEFAULTS } from "./aws-secrets-manager-pki-sync-constants";
import {
AwsSecretsManagerCertificateSecret,
SyncCertificatesResult,
TAwsSecretsManagerPkiSyncWithCredentials
} from "./aws-secrets-manager-pki-sync-types";
const AWS_SECRETS_MANAGER_RATE_LIMIT_CONFIG: RateLimitConfig = {
MAX_CONCURRENT_REQUESTS: 10,
BASE_DELAY: 1000,
MAX_DELAY: 30000,
MAX_RETRIES: 3,
RATE_LIMIT_STATUS_CODES: [429, 503]
};
const awsSecretsManagerConnectionQueue = createConnectionQueue(AWS_SECRETS_MANAGER_RATE_LIMIT_CONFIG);
const { withRateLimitRetry } = awsSecretsManagerConnectionQueue;
const MAX_RETRIES = 10;
const sleep = async () =>
new Promise((resolve) => {
setTimeout(resolve, 1000);
});
const isInfisicalManagedCertificate = (secretName: string, pkiSync: TPkiSyncWithCredentials): boolean => {
const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined;
const certificateNameSchema = syncOptions?.certificateNameSchema;
if (certificateNameSchema) {
const environment = AWS_SECRETS_MANAGER_PKI_SYNC_DEFAULTS.DEFAULT_ENVIRONMENT;
return matchesCertificateNameSchema(secretName, environment, certificateNameSchema);
}
return secretName.startsWith(AWS_SECRETS_MANAGER_PKI_SYNC_DEFAULTS.INFISICAL_PREFIX);
};
const parseErrorMessage = (error: unknown): string => {
if (error instanceof Error) {
return error.message;
}
if (typeof error === "string") {
return error;
}
if (error && typeof error === "object" && "message" in error) {
const { message } = error as { message: unknown };
if (typeof message === "string") {
return message;
}
}
return "Unknown error occurred";
};
const getSecretsManagerClient = async (pkiSync: TAwsSecretsManagerPkiSyncWithCredentials) => {
const { destinationConfig, connection } = pkiSync;
const config = await getAwsConnectionConfig(
connection as TAwsConnectionConfig,
destinationConfig.region as AWSRegion
);
if (!config.credentials) {
throw new Error("AWS credentials not found in connection configuration");
}
const secretsManagerClient = new SecretsManagerClient({
region: config.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials: config.credentials
});
return secretsManagerClient;
};
type TAwsSecretsManagerPkiSyncFactoryDeps = {
certificateDAL: Pick<TCertificateDALFactory, "findById">;
certificateSyncDAL: Pick<
TCertificateSyncDALFactory,
| "removeCertificates"
| "addCertificates"
| "findByPkiSyncAndCertificate"
| "updateById"
| "findByPkiSyncId"
| "updateSyncStatus"
>;
};
export const awsSecretsManagerPkiSyncFactory = ({
certificateDAL,
certificateSyncDAL
}: TAwsSecretsManagerPkiSyncFactoryDeps) => {
const $getSecretsManagerSecrets = async (
pkiSync: TAwsSecretsManagerPkiSyncWithCredentials,
syncId = "unknown"
): Promise<Record<string, string>> => {
const client = await getSecretsManagerClient(pkiSync);
const secrets: Record<string, string> = {};
let hasNext = true;
let nextToken: string | undefined;
let attempt = 0;
while (hasNext) {
try {
const currentToken = nextToken;
const output = await withRateLimitRetry(
() => client.send(new ListSecretsCommand({ NextToken: currentToken })),
{
operation: "list-secrets-manager-secrets",
syncId
}
);
attempt = 0;
if (output.SecretList) {
output.SecretList.forEach((secretEntry) => {
if (
secretEntry.Name &&
isInfisicalManagedCertificate(secretEntry.Name, pkiSync as unknown as TPkiSyncWithCredentials)
) {
secrets[secretEntry.Name] = secretEntry.ARN || secretEntry.Name;
}
});
}
hasNext = Boolean(output.NextToken);
nextToken = output.NextToken;
} catch (e) {
if (
e &&
typeof e === "object" &&
"name" in e &&
(e as { name: string }).name === "ThrottlingException" &&
attempt < MAX_RETRIES
) {
attempt += 1;
await sleep();
continue;
}
throw e;
}
}
return secrets;
};
const syncCertificates = async (
pkiSync: TPkiSyncWithCredentials,
certificateMap: TCertificateMap
): Promise<SyncCertificatesResult> => {
const awsPkiSync = pkiSync as unknown as TAwsSecretsManagerPkiSyncWithCredentials;
const client = await getSecretsManagerClient(awsPkiSync);
const existingSecrets = await $getSecretsManagerSecrets(awsPkiSync, pkiSync.id);
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
const syncRecordsByCertId = new Map<string, TCertificateSyncs>();
const syncRecordsByExternalId = new Map<string, TCertificateSyncs>();
existingSyncRecords.forEach((record: TCertificateSyncs) => {
if (record.certificateId) {
syncRecordsByCertId.set(record.certificateId, record);
}
if (record.externalIdentifier) {
syncRecordsByExternalId.set(record.externalIdentifier, record);
}
});
type CertificateUploadData = {
secretName: string;
certificateData: AwsSecretsManagerCertificateSecret;
certificateId: string;
isUpdate: boolean;
targetSecretName: string;
oldCertificateIdToRemove?: string;
};
const setCertificates: CertificateUploadData[] = [];
const validationErrors: Array<{ name: string; error: string }> = [];
const syncOptions = pkiSync.syncOptions as
| {
canRemoveCertificates?: boolean;
preserveSecretOnRenewal?: boolean;
fieldMappings?: {
certificate?: string;
privateKey?: string;
certificateChain?: string;
caCertificate?: string;
};
certificateNameSchema?: string;
}
| undefined;
const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true;
const preserveSecretOnRenewal = syncOptions?.preserveSecretOnRenewal ?? true;
const fieldMappings = {
certificate: syncOptions?.fieldMappings?.certificate ?? "certificate",
privateKey: syncOptions?.fieldMappings?.privateKey ?? "private_key",
certificateChain: syncOptions?.fieldMappings?.certificateChain ?? "certificate_chain",
caCertificate: syncOptions?.fieldMappings?.caCertificate ?? "ca_certificate"
};
const activeExternalIdentifiers = new Set<string>();
for (const [certName, certData] of Object.entries(certificateMap)) {
const { cert, privateKey: certPrivateKey, certificateChain, caCertificate, certificateId } = certData;
if (!cert || cert.trim().length === 0) {
validationErrors.push({
name: certName,
error: "Certificate content is empty or missing"
});
continue;
}
if (!certPrivateKey || certPrivateKey.trim().length === 0) {
validationErrors.push({
name: certName,
error: "Private key content is empty or missing"
});
continue;
}
if (!certificateId || typeof certificateId !== "string") {
continue;
}
const certificateData: AwsSecretsManagerCertificateSecret = {
[fieldMappings.certificate]: cert,
[fieldMappings.privateKey]: certPrivateKey
};
if (certificateChain && certificateChain.trim().length > 0) {
certificateData[fieldMappings.certificateChain] = certificateChain;
}
if (caCertificate && typeof caCertificate === "string" && caCertificate.trim().length > 0) {
certificateData[fieldMappings.caCertificate] = caCertificate;
}
let targetSecretName = certName;
if (syncOptions?.certificateNameSchema) {
const extendedCertData = certData as Record<string, unknown>;
const safeCommonName = typeof extendedCertData.commonName === "string" ? extendedCertData.commonName : "";
targetSecretName = syncOptions.certificateNameSchema
.replace(new RE2("\\{\\{certificateId\\}\\}", "g"), certificateId)
.replace(new RE2("\\{\\{commonName\\}\\}", "g"), safeCommonName);
} else {
targetSecretName = `${AWS_SECRETS_MANAGER_PKI_SYNC_DEFAULTS.INFISICAL_PREFIX}${certificateId}`;
}
const certificate = await certificateDAL.findById(certificateId);
if (certificate?.renewedByCertificateId) {
continue;
}
const syncRecordLookupId = certificate?.renewedFromCertificateId || certificateId;
const existingRecord = syncRecordsByCertId.get(syncRecordLookupId);
let shouldProcess = true;
let isUpdate = false;
if (existingRecord?.externalIdentifier) {
const existingSecret = existingSecrets[existingRecord.externalIdentifier];
if (existingSecret) {
if (certificate?.renewedFromCertificateId && preserveSecretOnRenewal) {
targetSecretName = existingRecord.externalIdentifier;
isUpdate = true;
} else if (certificate?.renewedFromCertificateId && !preserveSecretOnRenewal) {
activeExternalIdentifiers.add(existingRecord.externalIdentifier);
} else if (!certificate?.renewedFromCertificateId) {
activeExternalIdentifiers.add(existingRecord.externalIdentifier);
shouldProcess = false;
}
}
}
if (!shouldProcess) {
continue;
}
if (existingSecrets[targetSecretName]) {
isUpdate = true;
}
activeExternalIdentifiers.add(targetSecretName);
setCertificates.push({
secretName: certName,
certificateData,
certificateId,
isUpdate,
targetSecretName,
oldCertificateIdToRemove:
certificate?.renewedFromCertificateId && preserveSecretOnRenewal
? certificate.renewedFromCertificateId
: undefined
});
}
const result: SyncCertificatesResult = {
uploaded: 0,
updated: 0,
removed: 0,
failedRemovals: 0,
skipped: 0,
details: {
failedUploads: [],
failedRemovals: [],
validationErrors
}
};
for (const certData of setCertificates) {
const { secretName, certificateData, certificateId, isUpdate, targetSecretName, oldCertificateIdToRemove } =
certData;
try {
const secretValue = JSON.stringify(certificateData);
const configKeyId: unknown = awsPkiSync.destinationConfig.keyId;
const keyId: string = typeof configKeyId === "string" ? configKeyId : "alias/aws/secretsmanager";
if (isUpdate) {
await withRateLimitRetry(
() =>
client.send(
new UpdateSecretCommand({
SecretId: targetSecretName,
SecretString: secretValue,
KmsKeyId: keyId
})
),
{
operation: "update-secret",
syncId: pkiSync.id
}
);
result.updated += 1;
} else {
await withRateLimitRetry(
() =>
client.send(
new CreateSecretCommand({
Name: targetSecretName,
SecretString: secretValue,
KmsKeyId: keyId,
Description: `Certificate managed by Infisical`
})
),
{
operation: "create-secret",
syncId: pkiSync.id
}
);
result.uploaded += 1;
}
const existingRecord = syncRecordsByCertId.get(certificateId);
if (existingRecord?.id) {
await certificateSyncDAL.updateById(existingRecord.id, {
externalIdentifier: targetSecretName,
syncStatus: CertificateSyncStatus.Succeeded,
lastSyncedAt: new Date(),
lastSyncMessage: "Certificate successfully synced to AWS Secrets Manager"
});
if (oldCertificateIdToRemove && oldCertificateIdToRemove !== certificateId) {
await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateIdToRemove]);
}
} else {
await certificateSyncDAL.addCertificates(pkiSync.id, [
{
certificateId,
externalIdentifier: targetSecretName
}
]);
const newCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId);
if (newCertSync?.id) {
await certificateSyncDAL.updateById(newCertSync.id, {
syncStatus: CertificateSyncStatus.Succeeded,
lastSyncedAt: new Date(),
lastSyncMessage: "Certificate successfully synced to AWS Secrets Manager"
});
}
}
} catch (error) {
result.details?.failedUploads?.push({
name: secretName,
error: parseErrorMessage(error)
});
logger.error(
{
secretName,
certificateId,
error: parseErrorMessage(error),
pkiSyncId: pkiSync.id
},
"Failed to sync certificate"
);
const existingRecord = syncRecordsByCertId.get(certificateId);
if (existingRecord?.id) {
await certificateSyncDAL.updateById(existingRecord.id, {
syncStatus: CertificateSyncStatus.Failed,
lastSyncMessage: parseErrorMessage(error)
});
}
}
}
if (canRemoveCertificates) {
for (const [secretName] of Object.entries(existingSecrets)) {
if (!activeExternalIdentifiers.has(secretName)) {
try {
await withRateLimitRetry(
() =>
client.send(
new DeleteSecretCommand({
SecretId: secretName,
ForceDeleteWithoutRecovery: true
})
),
{
operation: "delete-secret",
syncId: pkiSync.id
}
);
result.removed += 1;
} catch (error) {
result.failedRemovals += 1;
result.details?.failedRemovals?.push({
name: secretName,
error: parseErrorMessage(error)
});
logger.error(
{
secretName,
error: parseErrorMessage(error),
pkiSyncId: pkiSync.id
},
"Failed to remove certificate secret"
);
}
}
}
}
return result;
};
const removeCertificates = async (
pkiSync: TPkiSyncWithCredentials,
certificateMap: TCertificateMap
): Promise<{ removed: number; failed: number }> => {
const awsPkiSync = pkiSync as unknown as TAwsSecretsManagerPkiSyncWithCredentials;
const client = await getSecretsManagerClient(awsPkiSync);
const existingSecrets = await $getSecretsManagerSecrets(awsPkiSync, pkiSync.id);
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
let removed = 0;
let failed = 0;
for (const [, certData] of Object.entries(certificateMap)) {
if (!certData.certificateId) continue;
const syncRecord = existingSyncRecords.find((record) => record.certificateId === certData.certificateId);
if (!syncRecord?.externalIdentifier) continue;
const secretName = syncRecord.externalIdentifier;
if (existingSecrets[secretName]) {
try {
await withRateLimitRetry(
() =>
client.send(
new DeleteSecretCommand({
SecretId: secretName,
ForceDeleteWithoutRecovery: true
})
),
{
operation: "delete-secret",
syncId: pkiSync.id
}
);
if (syncRecord.id) {
await certificateSyncDAL.updateById(syncRecord.id, {
syncStatus: CertificateSyncStatus.Failed
});
}
removed += 1;
} catch (error) {
failed += 1;
logger.error(
{
secretName,
certificateId: certData.certificateId,
error: parseErrorMessage(error),
pkiSyncId: pkiSync.id
},
"Failed to remove certificate secret"
);
}
}
}
return { removed, failed };
};
return {
syncCertificates,
removeCertificates
};
};
export type TAwsSecretsManagerPkiSyncFactory = ReturnType<typeof awsSecretsManagerPkiSyncFactory>;
@@ -0,0 +1,104 @@
import RE2 from "re2";
import { z } from "zod";
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { PkiSyncSchema } from "@app/services/pki-sync/pki-sync-schemas";
import { AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING } from "./aws-secrets-manager-pki-sync-constants";
export const AwsSecretsManagerPkiSyncConfigSchema = z.object({
region: z.nativeEnum(AWSRegion),
keyId: z.string().trim().optional()
});
export const AwsSecretsManagerFieldMappingsSchema = z.object({
certificate: z.string().min(1, "Certificate field name is required").default("certificate"),
privateKey: z.string().min(1, "Private key field name is required").default("private_key"),
certificateChain: z.string().min(1, "Certificate chain field name is required").default("certificate_chain"),
caCertificate: z.string().min(1, "CA certificate field name is required").default("ca_certificate")
});
const AwsSecretsManagerPkiSyncOptionsSchema = z.object({
canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(true),
includeRootCa: z.boolean().default(false),
preserveSecretOnRenewal: z.boolean().default(true),
updateExistingCertificates: z.boolean().default(true),
certificateNameSchema: z
.string()
.optional()
.refine(
(schema) => {
if (!schema) return true;
if (!schema.includes("{{certificateId}}")) {
return false;
}
const testName = schema
.replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "test-cert-id")
.replace(new RE2("\\{\\{profileId\\}\\}", "g"), "test-profile-id")
.replace(new RE2("\\{\\{commonName\\}\\}", "g"), "test-common-name")
.replace(new RE2("\\{\\{friendlyName\\}\\}", "g"), "test-friendly-name")
.replace(new RE2("\\{\\{environment\\}\\}", "g"), "test-env");
const hasForbiddenChars = AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some(
(char) => testName.includes(char)
);
return (
AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.NAME_PATTERN.test(testName) &&
!hasForbiddenChars &&
testName.length >= AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.MIN_LENGTH &&
testName.length <= AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.MAX_LENGTH
);
},
{
message:
"Certificate name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, underscores, and hyphens and be 1-512 characters long for AWS Secrets Manager."
}
),
fieldMappings: AwsSecretsManagerFieldMappingsSchema.optional().default({
certificate: "certificate",
privateKey: "private_key",
certificateChain: "certificate_chain",
caCertificate: "ca_certificate"
})
});
export const AwsSecretsManagerPkiSyncSchema = PkiSyncSchema.extend({
destination: z.literal(PkiSync.AwsSecretsManager),
destinationConfig: AwsSecretsManagerPkiSyncConfigSchema,
syncOptions: AwsSecretsManagerPkiSyncOptionsSchema
});
export const CreateAwsSecretsManagerPkiSyncSchema = z.object({
name: z.string().trim().min(1).max(64),
description: z.string().optional(),
isAutoSyncEnabled: z.boolean().default(true),
destinationConfig: AwsSecretsManagerPkiSyncConfigSchema,
syncOptions: AwsSecretsManagerPkiSyncOptionsSchema.optional().default({}),
subscriberId: z.string().nullish(),
connectionId: z.string(),
projectId: z.string().trim().min(1),
certificateIds: z.array(z.string().uuid()).optional()
});
export const UpdateAwsSecretsManagerPkiSyncSchema = z.object({
name: z.string().trim().min(1).max(64).optional(),
description: z.string().optional(),
isAutoSyncEnabled: z.boolean().optional(),
destinationConfig: AwsSecretsManagerPkiSyncConfigSchema.optional(),
syncOptions: AwsSecretsManagerPkiSyncOptionsSchema.optional(),
subscriberId: z.string().nullish(),
connectionId: z.string().optional()
});
export const AwsSecretsManagerPkiSyncListItemSchema = z.object({
name: z.literal("AWS Secrets Manager"),
connection: z.literal(AppConnection.AWS),
destination: z.literal(PkiSync.AwsSecretsManager),
canImportCertificates: z.literal(false),
canRemoveCertificates: z.literal(true)
});
@@ -0,0 +1,59 @@
import { z } from "zod";
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
import {
AwsSecretsManagerFieldMappingsSchema,
AwsSecretsManagerPkiSyncConfigSchema,
AwsSecretsManagerPkiSyncSchema,
CreateAwsSecretsManagerPkiSyncSchema,
UpdateAwsSecretsManagerPkiSyncSchema
} from "./aws-secrets-manager-pki-sync-schemas";
export type TAwsSecretsManagerPkiSyncConfig = z.infer<typeof AwsSecretsManagerPkiSyncConfigSchema>;
export type TAwsSecretsManagerFieldMappings = z.infer<typeof AwsSecretsManagerFieldMappingsSchema>;
export type TAwsSecretsManagerPkiSync = z.infer<typeof AwsSecretsManagerPkiSyncSchema>;
export type TAwsSecretsManagerPkiSyncInput = z.infer<typeof CreateAwsSecretsManagerPkiSyncSchema>;
export type TAwsSecretsManagerPkiSyncUpdate = z.infer<typeof UpdateAwsSecretsManagerPkiSyncSchema>;
export type TAwsSecretsManagerPkiSyncWithCredentials = TAwsSecretsManagerPkiSync & {
connection: TAwsConnection;
appConnectionName: string;
appConnectionApp: string;
};
export interface AwsSecretsManagerCertificateSecret {
[key: string]: string;
}
export interface SyncCertificatesResult {
uploaded: number;
updated: number;
removed: number;
failedRemovals: number;
skipped: number;
details?: {
failedUploads?: Array<{ name: string; error: string }>;
failedRemovals?: Array<{ name: string; error: string }>;
validationErrors?: Array<{ name: string; error: string }>;
};
}
export interface RemoveCertificatesResult {
removed: number;
failed: number;
skipped: number;
}
export interface CertificateImportRequest {
name: string;
certificate: string;
privateKey: string;
certificateChain?: string;
caCertificate?: string;
certificateId?: string;
}
@@ -0,0 +1,4 @@
export * from "./aws-secrets-manager-pki-sync-constants";
export * from "./aws-secrets-manager-pki-sync-fns";
export * from "./aws-secrets-manager-pki-sync-schemas";
export * from "./aws-secrets-manager-pki-sync-types";
@@ -14,6 +14,7 @@ export const AzureKeyVaultPkiSyncConfigSchema = z.object({
const AzureKeyVaultPkiSyncOptionsSchema = z.object({
canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(true),
includeRootCa: z.boolean().default(false),
enableVersioning: z.boolean().default(true),
certificateNameSchema: z
.string()
@@ -0,0 +1,23 @@
import RE2 from "re2";
export const CHEF_PKI_SYNC_CERTIFICATE_NAMING = {
NAME_PATTERN: new RE2("^[a-zA-Z0-9_-]+$"),
FORBIDDEN_CHARACTERS: "[]{}()<>|\\:;\"'=+*&^%$#@!~`?/",
MIN_LENGTH: 1,
MAX_LENGTH: 255,
DEFAULT_SCHEMA: "{{certificateId}}"
};
export const CHEF_PKI_SYNC_DATA_BAG_NAMING = {
NAME_PATTERN: new RE2("^[a-zA-Z0-9_-]+$"),
FORBIDDEN_CHARACTERS: "[]{}()<>|\\:;\"'=+*&^%$#@!~`?/.",
MIN_LENGTH: 1,
MAX_LENGTH: 255
};
export const CHEF_PKI_SYNC_DEFAULTS = {
CERTIFICATE_DATA_BAG: "ssl_certificates",
ITEM_NAME_TEMPLATE: "{{certificateId}}",
INFISICAL_PREFIX: "Infisical-",
DEFAULT_ENVIRONMENT: "global"
} as const;
@@ -0,0 +1,595 @@
/* eslint-disable no-continue */
/* eslint-disable no-await-in-loop */
import { TCertificateSyncs } from "@app/db/schemas";
import {
createChefDataBagItem,
listChefDataBagItems,
removeChefDataBagItem,
updateChefDataBagItem
} from "@app/ee/services/app-connections/chef";
import { TChefDataBagItemContent } from "@app/ee/services/secret-sync/chef";
import { logger } from "@app/lib/logger";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
import { matchesCertificateNameSchema } from "@app/services/pki-sync/pki-sync-fns";
import { TCertificateMap, TPkiSyncWithCredentials } from "@app/services/pki-sync/pki-sync-types";
import { CHEF_PKI_SYNC_DEFAULTS } from "./chef-pki-sync-constants";
import { ChefCertificateDataBagItem, SyncCertificatesResult, TChefPkiSyncWithCredentials } from "./chef-pki-sync-types";
const CHEF_RATE_LIMIT_CONFIG: RateLimitConfig = {
MAX_CONCURRENT_REQUESTS: 5, // Chef servers generally have lower rate limits
BASE_DELAY: 1500,
MAX_DELAY: 30000,
MAX_RETRIES: 3,
RATE_LIMIT_STATUS_CODES: [429, 503]
};
const chefConnectionQueue = createConnectionQueue(CHEF_RATE_LIMIT_CONFIG);
const { withRateLimitRetry } = chefConnectionQueue;
const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyncWithCredentials): boolean => {
const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined;
const certificateNameSchema = syncOptions?.certificateNameSchema;
if (certificateNameSchema) {
const environment = CHEF_PKI_SYNC_DEFAULTS.DEFAULT_ENVIRONMENT;
return matchesCertificateNameSchema(certificateName, environment, certificateNameSchema);
}
return certificateName.startsWith(CHEF_PKI_SYNC_DEFAULTS.INFISICAL_PREFIX);
};
const parseErrorMessage = (error: unknown): string => {
if (error instanceof Error) {
return error.message;
}
if (typeof error === "string") {
return error;
}
if (error && typeof error === "object" && "message" in error) {
const { message } = error as { message: unknown };
if (typeof message === "string") {
return message;
}
}
return "Unknown error occurred";
};
type TChefPkiSyncFactoryDeps = {
certificateDAL: Pick<TCertificateDALFactory, "findById">;
certificateSyncDAL: Pick<
TCertificateSyncDALFactory,
| "removeCertificates"
| "addCertificates"
| "findByPkiSyncAndCertificate"
| "updateById"
| "findByPkiSyncId"
| "updateSyncStatus"
>;
};
export const chefPkiSyncFactory = ({ certificateDAL, certificateSyncDAL }: TChefPkiSyncFactoryDeps) => {
const $getChefDataBagItems = async (
pkiSync: TChefPkiSyncWithCredentials,
syncId = "unknown"
): Promise<Record<string, boolean>> => {
const {
connection,
destinationConfig: { dataBagName }
} = pkiSync;
const { serverUrl, userName, privateKey, orgName } = connection.credentials;
const dataBagItems = await withRateLimitRetry(
() =>
listChefDataBagItems(
{
credentials: { serverUrl, userName, privateKey, orgName }
} as Parameters<typeof listChefDataBagItems>[0],
dataBagName
),
{
operation: "list-chef-data-bag-items",
syncId
}
);
const chefDataBagItems: Record<string, boolean> = {};
dataBagItems.forEach((item) => {
chefDataBagItems[item.name] = true;
});
return chefDataBagItems;
};
const syncCertificates = async (
pkiSync: TPkiSyncWithCredentials,
certificateMap: TCertificateMap
): Promise<SyncCertificatesResult> => {
const chefPkiSync = pkiSync as unknown as TChefPkiSyncWithCredentials;
const {
connection,
destinationConfig: { dataBagName }
} = chefPkiSync;
const { serverUrl, userName, privateKey, orgName } = connection.credentials;
const chefDataBagItems = await $getChefDataBagItems(chefPkiSync, pkiSync.id);
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
const syncRecordsByCertId = new Map<string, TCertificateSyncs>();
const syncRecordsByExternalId = new Map<string, TCertificateSyncs>();
existingSyncRecords.forEach((record: TCertificateSyncs) => {
if (record.certificateId) {
syncRecordsByCertId.set(record.certificateId, record);
}
if (record.externalIdentifier) {
syncRecordsByExternalId.set(record.externalIdentifier, record);
}
});
type CertificateUploadData = {
key: string;
name: string;
cert: string;
privateKey: string;
certificateChain?: string;
caCertificate?: string;
certificateId: string;
isUpdate: boolean;
targetItemName: string;
oldCertificateIdToRemove?: string;
};
const setCertificates: CertificateUploadData[] = [];
const validationErrors: Array<{ name: string; error: string }> = [];
const syncOptions = pkiSync.syncOptions as
| {
canRemoveCertificates?: boolean;
preserveItemOnRenewal?: boolean;
fieldMappings?: {
certificate?: string;
privateKey?: string;
certificateChain?: string;
caCertificate?: string;
metadata?: string;
};
}
| undefined;
const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true;
const preserveItemOnRenewal = syncOptions?.preserveItemOnRenewal ?? true;
const fieldMappings = {
certificate: syncOptions?.fieldMappings?.certificate ?? "certificate",
privateKey: syncOptions?.fieldMappings?.privateKey ?? "private_key",
certificateChain: syncOptions?.fieldMappings?.certificateChain ?? "certificate_chain",
caCertificate: syncOptions?.fieldMappings?.caCertificate ?? "ca_certificate"
};
const activeExternalIdentifiers = new Set<string>();
for (const [certName, certData] of Object.entries(certificateMap)) {
const { cert, privateKey: certPrivateKey, certificateChain, caCertificate, certificateId } = certData;
if (!cert || cert.trim().length === 0) {
validationErrors.push({
name: certName,
error: "Certificate content is empty or missing"
});
continue;
}
if (!certPrivateKey || certPrivateKey.trim().length === 0) {
validationErrors.push({
name: certName,
error: "Private key content is empty or missing"
});
continue;
}
if (!certificateId || typeof certificateId !== "string") {
continue;
}
const targetCertificateName = certName;
const certificate = await certificateDAL.findById(certificateId);
if (certificate?.renewedByCertificateId) {
continue;
}
const syncRecordLookupId = certificate?.renewedFromCertificateId || certificateId;
const existingSyncRecord = syncRecordsByCertId.get(syncRecordLookupId);
let shouldProcess = true;
let isUpdate = false;
let targetItemName = targetCertificateName;
if (existingSyncRecord?.externalIdentifier) {
const existingChefItem = chefDataBagItems[existingSyncRecord.externalIdentifier];
if (existingChefItem) {
if (certificate?.renewedFromCertificateId && preserveItemOnRenewal) {
targetItemName = existingSyncRecord.externalIdentifier;
isUpdate = true;
} else if (!certificate?.renewedFromCertificateId) {
shouldProcess = false;
}
}
}
if (!shouldProcess) {
continue;
}
setCertificates.push({
key: certName,
name: certName,
cert,
privateKey: certPrivateKey,
certificateChain,
caCertificate,
certificateId,
isUpdate,
targetItemName,
oldCertificateIdToRemove:
certificate?.renewedFromCertificateId && preserveItemOnRenewal
? certificate.renewedFromCertificateId
: undefined
});
activeExternalIdentifiers.add(targetItemName);
}
type UploadResult =
| { status: "fulfilled"; certificate: CertificateUploadData }
| { status: "rejected"; certificate: CertificateUploadData; error: unknown };
const uploadPromises = setCertificates.map(async (certificateData): Promise<UploadResult> => {
const {
targetItemName,
cert,
privateKey: certPrivateKey,
certificateChain,
caCertificate,
certificateId
} = certificateData;
try {
const chefDataBagItem: ChefCertificateDataBagItem = {
id: targetItemName,
[fieldMappings.certificate]: cert,
[fieldMappings.privateKey]: certPrivateKey,
...(certificateChain && { [fieldMappings.certificateChain]: certificateChain }),
...(caCertificate && { [fieldMappings.caCertificate]: caCertificate })
};
const itemExists = chefDataBagItems[targetItemName] === true;
if (itemExists) {
await withRateLimitRetry(
() =>
updateChefDataBagItem({
serverUrl,
userName,
privateKey,
orgName,
dataBagName,
dataBagItemName: targetItemName,
data: chefDataBagItem as unknown as TChefDataBagItemContent
}),
{
operation: "update-chef-data-bag-item",
syncId: pkiSync.id
}
);
} else {
await withRateLimitRetry(
() =>
createChefDataBagItem({
serverUrl,
userName,
privateKey,
orgName,
dataBagName,
data: chefDataBagItem as unknown as TChefDataBagItemContent
}),
{
operation: "create-chef-data-bag-item",
syncId: pkiSync.id
}
);
}
return { status: "fulfilled" as const, certificate: certificateData };
} catch (error) {
logger.error(
{
syncId: pkiSync.id,
certificateId,
targetItemName,
error: error instanceof Error ? error.message : String(error)
},
"Failed to sync certificate to Chef"
);
return { status: "rejected" as const, certificate: certificateData, error };
}
});
const uploadResults = await Promise.allSettled(uploadPromises);
const successfulUploads = uploadResults.filter(
(result): result is PromiseFulfilledResult<UploadResult> =>
result.status === "fulfilled" && result.value.status === "fulfilled"
);
const failedUploads = uploadResults.filter(
(
result
): result is
| PromiseRejectedResult
| PromiseFulfilledResult<{ status: "rejected"; certificate: CertificateUploadData; error: unknown }> =>
result.status === "rejected" || (result.status === "fulfilled" && result.value.status === "rejected")
);
let removedCount = 0;
let failedRemovals: Array<{ name: string; error: string }> = [];
if (canRemoveCertificates) {
const itemsToRemove: string[] = [];
Object.keys(chefDataBagItems).forEach((itemName) => {
if (!activeExternalIdentifiers.has(itemName) && isInfisicalManagedCertificate(itemName, pkiSync)) {
itemsToRemove.push(itemName);
}
});
if (itemsToRemove.length > 0) {
const removalPromises = itemsToRemove.map(async (itemName) => {
try {
await withRateLimitRetry(
() =>
removeChefDataBagItem({
serverUrl,
userName,
privateKey,
orgName,
dataBagName,
dataBagItemName: itemName
}),
{
operation: "remove-chef-data-bag-item",
syncId: pkiSync.id
}
);
const syncRecord = syncRecordsByExternalId.get(itemName);
if (syncRecord?.certificateId) {
await certificateSyncDAL.removeCertificates(pkiSync.id, [syncRecord.certificateId]);
}
return { status: "fulfilled" as const, itemName };
} catch (error) {
logger.error(
{
syncId: pkiSync.id,
itemName,
error: error instanceof Error ? error.message : String(error)
},
"Failed to remove Chef data bag item"
);
return { status: "rejected" as const, itemName, error };
}
});
const removalResults = await Promise.allSettled(removalPromises);
const successfulRemovals = removalResults.filter(
(result): result is PromiseFulfilledResult<{ status: "fulfilled"; itemName: string }> =>
result.status === "fulfilled" && result.value.status === "fulfilled"
);
removedCount = successfulRemovals.length;
const failedRemovalPromises = removalResults.filter(
(
result
): result is
| PromiseRejectedResult
| PromiseFulfilledResult<{ status: "rejected"; itemName: string; error: unknown }> =>
result.status === "rejected" || (result.status === "fulfilled" && result.value.status === "rejected")
);
failedRemovals = failedRemovalPromises.map((result) => {
if (result.status === "rejected") {
return {
name: "unknown",
error: parseErrorMessage(result.reason)
};
}
const { itemName, error } = result.value;
return {
name: String(itemName),
error: parseErrorMessage(error)
};
});
}
}
for (const result of successfulUploads) {
const { certificateId, targetItemName, oldCertificateIdToRemove } = result.value.certificate;
if (certificateId && typeof certificateId === "string") {
const existingCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId);
if (existingCertSync) {
await certificateSyncDAL.updateById(existingCertSync.id, {
externalIdentifier: targetItemName,
syncStatus: CertificateSyncStatus.Succeeded,
lastSyncedAt: new Date(),
lastSyncMessage: "Certificate successfully synced to destination"
});
} else {
await certificateSyncDAL.addCertificates(pkiSync.id, [
{
certificateId,
externalIdentifier: targetItemName
}
]);
const newCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId);
if (newCertSync) {
await certificateSyncDAL.updateById(newCertSync.id, {
syncStatus: CertificateSyncStatus.Succeeded,
lastSyncedAt: new Date(),
lastSyncMessage: "Certificate successfully synced to destination"
});
}
}
if (oldCertificateIdToRemove) {
await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateIdToRemove]);
}
}
}
await Promise.all(
failedUploads.map(async (result) => {
let certificateId: string;
let errorMessage: string;
if (result.status === "rejected") {
certificateId = "unknown";
errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
return;
}
const { certificate, error } = result.value;
certificateId = certificate.certificateId;
errorMessage = error instanceof Error ? error.message : String(error);
const existingSyncRecord = syncRecordsByCertId.get(certificateId);
if (existingSyncRecord) {
await certificateSyncDAL.updateSyncStatus(
pkiSync.id,
certificateId,
CertificateSyncStatus.Failed,
errorMessage
);
}
})
);
return {
uploaded: successfulUploads.filter((result) => !result.value.certificate.isUpdate).length,
updated: successfulUploads.filter((result) => result.value.certificate.isUpdate).length,
removed: removedCount,
failedRemovals: failedRemovals.length,
skipped: validationErrors.length,
details: {
failedUploads: failedUploads.map((result) => {
if (result.status === "rejected") {
return {
name: "unknown",
error: result.reason instanceof Error ? result.reason.message : String(result.reason)
};
}
const { certificate, error } = result.value;
return {
name: certificate.name,
error: error instanceof Error ? error.message : String(error)
};
}),
failedRemovals,
validationErrors
}
};
};
const importCertificates = async (): Promise<SyncCertificatesResult> => {
throw new Error("Chef PKI Sync does not support importing certificates from Chef data bags");
};
const removeCertificates = async (
sync: TPkiSyncWithCredentials,
certificateNames: string[],
deps?: { certificateSyncDAL?: TCertificateSyncDALFactory; certificateMap?: TCertificateMap }
): Promise<void> => {
const chefPkiSync = sync as unknown as TChefPkiSyncWithCredentials;
const {
connection,
destinationConfig: { dataBagName }
} = chefPkiSync;
const { serverUrl, userName, privateKey, orgName } = connection.credentials;
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(sync.id);
const certificateIdsToRemove: string[] = [];
const itemsToRemove: string[] = [];
for (const certName of certificateNames) {
const certificateData = deps?.certificateMap?.[certName];
if (certificateData?.certificateId && typeof certificateData.certificateId === "string") {
const syncRecord = existingSyncRecords.find((record) => record.certificateId === certificateData.certificateId);
if (syncRecord) {
certificateIdsToRemove.push(certificateData.certificateId);
if (syncRecord.externalIdentifier) {
itemsToRemove.push(syncRecord.externalIdentifier);
}
}
} else {
const targetName = certName;
const syncRecord = existingSyncRecords.find((record) => record.externalIdentifier === targetName);
if (syncRecord && syncRecord.certificateId) {
certificateIdsToRemove.push(syncRecord.certificateId);
itemsToRemove.push(targetName);
}
}
}
const removalPromises = itemsToRemove.map(async (itemName) => {
try {
await withRateLimitRetry(
() =>
removeChefDataBagItem({
serverUrl,
userName,
privateKey,
orgName,
dataBagName,
dataBagItemName: itemName
}),
{
operation: "remove-chef-data-bag-item",
syncId: sync.id
}
);
} catch (error) {
logger.error(
{
syncId: sync.id,
itemName,
error: error instanceof Error ? error.message : String(error)
},
"Failed to remove Chef data bag item during certificate removal"
);
}
});
await Promise.allSettled(removalPromises);
if (certificateIdsToRemove.length > 0) {
await certificateSyncDAL.removeCertificates(sync.id, certificateIdsToRemove);
}
};
return {
syncCertificates,
importCertificates,
removeCertificates
};
};
@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
export const CHEF_PKI_SYNC_LIST_OPTION = {
name: "Chef" as const,
connection: AppConnection.Chef,
destination: PkiSync.Chef,
canImportCertificates: false,
canRemoveCertificates: true
} as const;
@@ -0,0 +1,113 @@
import RE2 from "re2";
import { z } from "zod";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { PkiSyncSchema } from "@app/services/pki-sync/pki-sync-schemas";
import { CHEF_PKI_SYNC_CERTIFICATE_NAMING, CHEF_PKI_SYNC_DATA_BAG_NAMING } from "./chef-pki-sync-constants";
export const ChefPkiSyncConfigSchema = z.object({
dataBagName: z
.string()
.trim()
.min(1, "Data bag name required")
.max(255, "Data bag name cannot exceed 255 characters")
.refine(
(name) => CHEF_PKI_SYNC_DATA_BAG_NAMING.NAME_PATTERN.test(name),
"Data bag name can only contain alphanumeric characters, underscores, and hyphens"
)
});
const ChefFieldMappingsSchema = z.object({
certificate: z.string().min(1, "Certificate field name is required").default("certificate"),
privateKey: z.string().min(1, "Private key field name is required").default("private_key"),
certificateChain: z.string().min(1, "Certificate chain field name is required").default("certificate_chain"),
caCertificate: z.string().min(1, "CA certificate field name is required").default("ca_certificate")
});
const ChefPkiSyncOptionsSchema = z.object({
canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(true),
includeRootCa: z.boolean().default(false),
preserveItemOnRenewal: z.boolean().default(true),
updateExistingCertificates: z.boolean().default(true),
certificateNameSchema: z
.string()
.optional()
.refine(
(schema) => {
if (!schema) return true;
if (!schema.includes("{{certificateId}}")) {
return false;
}
const testName = schema
.replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "test-cert-id")
.replace(new RE2("\\{\\{profileId\\}\\}", "g"), "test-profile-id")
.replace(new RE2("\\{\\{commonName\\}\\}", "g"), "test-common-name")
.replace(new RE2("\\{\\{friendlyName\\}\\}", "g"), "test-friendly-name")
.replace(new RE2("\\{\\{environment\\}\\}", "g"), "test-env");
const hasForbiddenChars = CHEF_PKI_SYNC_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some((char) =>
testName.includes(char)
);
return (
CHEF_PKI_SYNC_CERTIFICATE_NAMING.NAME_PATTERN.test(testName) &&
!hasForbiddenChars &&
testName.length >= CHEF_PKI_SYNC_CERTIFICATE_NAMING.MIN_LENGTH &&
testName.length <= CHEF_PKI_SYNC_CERTIFICATE_NAMING.MAX_LENGTH
);
},
{
message:
"Certificate item name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, underscores, and hyphens and be 1-255 characters long for Chef data bag items."
}
),
fieldMappings: ChefFieldMappingsSchema.optional().default({
certificate: "certificate",
privateKey: "private_key",
certificateChain: "certificate_chain",
caCertificate: "ca_certificate"
})
});
export const ChefPkiSyncSchema = PkiSyncSchema.extend({
destination: z.literal(PkiSync.Chef),
destinationConfig: ChefPkiSyncConfigSchema,
syncOptions: ChefPkiSyncOptionsSchema
});
export const CreateChefPkiSyncSchema = z.object({
name: z.string().trim().min(1).max(64),
description: z.string().optional(),
isAutoSyncEnabled: z.boolean().default(true),
destinationConfig: ChefPkiSyncConfigSchema,
syncOptions: ChefPkiSyncOptionsSchema.optional().default({}),
subscriberId: z.string().nullish(),
connectionId: z.string(),
projectId: z.string().trim().min(1),
certificateIds: z.array(z.string().uuid()).optional()
});
export const UpdateChefPkiSyncSchema = z.object({
name: z.string().trim().min(1).max(64).optional(),
description: z.string().optional(),
isAutoSyncEnabled: z.boolean().optional(),
destinationConfig: ChefPkiSyncConfigSchema.optional(),
syncOptions: ChefPkiSyncOptionsSchema.optional(),
subscriberId: z.string().nullish(),
connectionId: z.string().optional()
});
export const ChefPkiSyncListItemSchema = z.object({
name: z.literal("Chef"),
connection: z.literal(AppConnection.Chef),
destination: z.literal(PkiSync.Chef),
canImportCertificates: z.literal(false),
canRemoveCertificates: z.literal(true)
});
export { ChefFieldMappingsSchema };
@@ -0,0 +1,59 @@
import { z } from "zod";
import { TChefConnection } from "@app/ee/services/app-connections/chef/chef-connection-types";
import {
ChefFieldMappingsSchema,
ChefPkiSyncConfigSchema,
ChefPkiSyncSchema,
CreateChefPkiSyncSchema,
UpdateChefPkiSyncSchema
} from "./chef-pki-sync-schemas";
export type TChefPkiSyncConfig = z.infer<typeof ChefPkiSyncConfigSchema>;
export type TChefFieldMappings = z.infer<typeof ChefFieldMappingsSchema>;
export type TChefPkiSync = z.infer<typeof ChefPkiSyncSchema>;
export type TChefPkiSyncInput = z.infer<typeof CreateChefPkiSyncSchema>;
export type TChefPkiSyncUpdate = z.infer<typeof UpdateChefPkiSyncSchema>;
export type TChefPkiSyncWithCredentials = TChefPkiSync & {
connection: TChefConnection;
};
export interface ChefCertificateDataBagItem {
id: string;
[key: string]: string;
}
export interface SyncCertificatesResult {
uploaded: number;
updated: number;
removed: number;
failedRemovals: number;
skipped: number;
details?: {
failedUploads?: Array<{ name: string; error: string }>;
failedRemovals?: Array<{ name: string; error: string }>;
validationErrors?: Array<{ name: string; error: string }>;
};
}
export interface RemoveCertificatesResult {
removed: number;
failed: number;
skipped: number;
}
export interface CertificateImportRequest {
id: string;
name: string;
certificate: string;
privateKey: string;
certificateChain?: string;
alternativeNames?: string[];
certificateId?: string;
}
@@ -0,0 +1,4 @@
export * from "./chef-pki-sync-constants";
export * from "./chef-pki-sync-fns";
export * from "./chef-pki-sync-schemas";
export * from "./chef-pki-sync-types";
@@ -1,6 +1,8 @@
export enum PkiSync {
AzureKeyVault = "azure-key-vault",
AwsCertificateManager = "aws-certificate-manager"
AwsCertificateManager = "aws-certificate-manager",
AwsSecretsManager = "aws-secrets-manager",
Chef = "chef"
}
export enum PkiSyncStatus {
+58 -5
View File
@@ -10,8 +10,12 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION } from "./aws-certificate-manager/aws-certificate-manager-pki-sync-constants";
import { awsCertificateManagerPkiSyncFactory } from "./aws-certificate-manager/aws-certificate-manager-pki-sync-fns";
import { AWS_SECRETS_MANAGER_PKI_SYNC_LIST_OPTION } from "./aws-secrets-manager/aws-secrets-manager-pki-sync-constants";
import { awsSecretsManagerPkiSyncFactory } from "./aws-secrets-manager/aws-secrets-manager-pki-sync-fns";
import { AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION } from "./azure-key-vault/azure-key-vault-pki-sync-constants";
import { azureKeyVaultPkiSyncFactory } from "./azure-key-vault/azure-key-vault-pki-sync-fns";
import { chefPkiSyncFactory } from "./chef/chef-pki-sync-fns";
import { CHEF_PKI_SYNC_LIST_OPTION } from "./chef/chef-pki-sync-list-constants";
import { PkiSync } from "./pki-sync-enums";
import { TCertificateMap, TPkiSyncWithCredentials } from "./pki-sync-types";
@@ -19,7 +23,9 @@ const ENTERPRISE_PKI_SYNCS: PkiSync[] = [];
const PKI_SYNC_LIST_OPTIONS = {
[PkiSync.AzureKeyVault]: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION,
[PkiSync.AwsCertificateManager]: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION
[PkiSync.AwsCertificateManager]: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION,
[PkiSync.AwsSecretsManager]: AWS_SECRETS_MANAGER_PKI_SYNC_LIST_OPTION,
[PkiSync.Chef]: CHEF_PKI_SYNC_LIST_OPTION
};
export const enterprisePkiSyncCheck = async (
@@ -162,6 +168,8 @@ export const PkiSyncFns = {
dependencies: {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
certificateDAL: TCertificateDALFactory;
certificateSyncDAL: TCertificateSyncDALFactory;
}
): Promise<TCertificateMap> => {
switch (pkiSync.destination) {
@@ -175,6 +183,14 @@ export const PkiSyncFns = {
"AWS Certificate Manager does not support importing certificates into Infisical (private keys cannot be extracted)"
);
}
case PkiSync.AwsSecretsManager: {
throw new Error("AWS Secrets Manager does not support importing certificates into Infisical");
}
case PkiSync.Chef: {
throw new Error(
"Chef does not support importing certificates into Infisical (private keys cannot be extracted securely)"
);
}
default:
throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`);
}
@@ -203,7 +219,7 @@ export const PkiSyncFns = {
}> => {
switch (pkiSync.destination) {
case PkiSync.AzureKeyVault: {
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault as PkiSync);
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({
appConnectionDAL: dependencies.appConnectionDAL,
kmsService: dependencies.kmsService,
@@ -213,7 +229,7 @@ export const PkiSyncFns = {
return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap);
}
case PkiSync.AwsCertificateManager: {
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager as PkiSync);
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({
appConnectionDAL: dependencies.appConnectionDAL,
kmsService: dependencies.kmsService,
@@ -222,6 +238,22 @@ export const PkiSyncFns = {
});
return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap);
}
case PkiSync.AwsSecretsManager: {
checkPkiSyncDestination(pkiSync, PkiSync.AwsSecretsManager as PkiSync);
const awsSecretsManagerPkiSync = awsSecretsManagerPkiSyncFactory({
certificateDAL: dependencies.certificateDAL,
certificateSyncDAL: dependencies.certificateSyncDAL
});
return awsSecretsManagerPkiSync.syncCertificates(pkiSync, certificateMap);
}
case PkiSync.Chef: {
checkPkiSyncDestination(pkiSync, PkiSync.Chef as PkiSync);
const chefPkiSync = chefPkiSyncFactory({
certificateDAL: dependencies.certificateDAL,
certificateSyncDAL: dependencies.certificateSyncDAL
});
return chefPkiSync.syncCertificates(pkiSync, certificateMap);
}
default:
throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`);
}
@@ -240,7 +272,7 @@ export const PkiSyncFns = {
): Promise<void> => {
switch (pkiSync.destination) {
case PkiSync.AzureKeyVault: {
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault as PkiSync);
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({
appConnectionDAL: dependencies.appConnectionDAL,
kmsService: dependencies.kmsService,
@@ -254,7 +286,7 @@ export const PkiSyncFns = {
break;
}
case PkiSync.AwsCertificateManager: {
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager as PkiSync);
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({
appConnectionDAL: dependencies.appConnectionDAL,
kmsService: dependencies.kmsService,
@@ -267,6 +299,27 @@ export const PkiSyncFns = {
});
break;
}
case PkiSync.AwsSecretsManager: {
checkPkiSyncDestination(pkiSync, PkiSync.AwsSecretsManager as PkiSync);
const awsSecretsManagerPkiSync = awsSecretsManagerPkiSyncFactory({
certificateDAL: dependencies.certificateDAL,
certificateSyncDAL: dependencies.certificateSyncDAL
});
await awsSecretsManagerPkiSync.removeCertificates(pkiSync, dependencies.certificateMap);
break;
}
case PkiSync.Chef: {
checkPkiSyncDestination(pkiSync, PkiSync.Chef as PkiSync);
const chefPkiSync = chefPkiSyncFactory({
certificateDAL: dependencies.certificateDAL,
certificateSyncDAL: dependencies.certificateSyncDAL
});
await chefPkiSync.removeCertificates(pkiSync, certificateNames, {
certificateSyncDAL: dependencies.certificateSyncDAL,
certificateMap: dependencies.certificateMap
});
break;
}
default:
throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`);
}
@@ -4,10 +4,14 @@ import { PkiSync } from "./pki-sync-enums";
export const PKI_SYNC_NAME_MAP: Record<PkiSync, string> = {
[PkiSync.AzureKeyVault]: "Azure Key Vault",
[PkiSync.AwsCertificateManager]: "AWS Certificate Manager"
[PkiSync.AwsCertificateManager]: "AWS Certificate Manager",
[PkiSync.AwsSecretsManager]: "AWS Secrets Manager",
[PkiSync.Chef]: "Chef"
};
export const PKI_SYNC_CONNECTION_MAP: Record<PkiSync, AppConnection> = {
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault,
[PkiSync.AwsCertificateManager]: AppConnection.AWS
[PkiSync.AwsCertificateManager]: AppConnection.AWS,
[PkiSync.AwsSecretsManager]: AppConnection.AWS,
[PkiSync.Chef]: AppConnection.Chef
};
@@ -26,6 +26,7 @@ import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { getCaCertChain } from "../certificate-authority/certificate-authority-fns";
import { extractRootCaFromChain, removeRootCaFromChain } from "../certificate-common/certificate-utils";
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
import { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums";
import { TPkiSyncDALFactory } from "./pki-sync-dal";
@@ -180,11 +181,16 @@ export const pkiSyncQueueFactory = ({
(cert, index, self) => self.findIndex((c) => c.id === cert.id) === index
);
if (uniqueCertificates.length === 0) {
const activeCertificates = uniqueCertificates.filter((cert) => {
const typedCert = cert as TCertificates;
return !typedCert.renewedByCertificateId;
});
if (activeCertificates.length === 0) {
return { certificateMap, certificateMetadata };
}
certificates = uniqueCertificates;
certificates = activeCertificates;
for (const certificate of certificates) {
const cert = certificate as TCertificates;
@@ -231,13 +237,15 @@ export const pkiSyncQueueFactory = ({
}
let certificateChain: string | undefined;
let caCertificate: string | undefined;
try {
if (certBody.encryptedCertificateChain) {
const decryptedCertChain = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificateChain
});
certificateChain = decryptedCertChain.toString();
} else if (certificate.caCertId) {
}
if (certificate.caCertId) {
const { caCert, caCertChain } = await getCaCertChain({
caCertId: certificate.caCertId,
certificateAuthorityDAL,
@@ -245,7 +253,10 @@ export const pkiSyncQueueFactory = ({
projectDAL,
kmsService
});
certificateChain = `${caCert}\n${caCertChain}`.trim();
if (!certBody.encryptedCertificateChain) {
certificateChain = `${caCert}\n${caCertChain}`.trim();
}
caCertificate = certificateChain ? extractRootCaFromChain(certificateChain) : caCert;
}
} catch (chainError) {
logger.warn(
@@ -254,10 +265,16 @@ export const pkiSyncQueueFactory = ({
);
// Continue without certificate chain
certificateChain = undefined;
caCertificate = undefined;
}
let certificateName: string;
const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined;
const syncOptions = pkiSync.syncOptions as
| {
certificateNameSchema?: string;
includeRootCa?: boolean;
}
| undefined;
const certificateNameSchema = syncOptions?.certificateNameSchema;
if (certificateNameSchema) {
@@ -289,10 +306,16 @@ export const pkiSyncQueueFactory = ({
alternativeNames.push(originalLegacyName);
}
let processedCertificateChain = certificateChain;
if (certificateChain && syncOptions?.includeRootCa === false) {
processedCertificateChain = removeRootCaFromChain(certificateChain);
}
certificateMap[certificateName] = {
cert: certificatePem,
privateKey: certPrivateKey || "",
certificateChain,
certificateChain: processedCertificateChain,
caCertificate,
alternativeNames,
certificateId: certificate.id
};
@@ -7,6 +7,7 @@ import { PkiSync } from "./pki-sync-enums";
export const PkiSyncOptionsSchema = z.object({
canImportCertificates: z.boolean(),
canRemoveCertificates: z.boolean().optional(),
includeRootCa: z.boolean().optional().default(false),
certificateNameSchema: z
.string()
.optional()
@@ -73,7 +73,14 @@ export type TPkiSyncListItem = TPkiSync & {
export type TCertificateMap = Record<
string,
{ cert: string; privateKey: string; certificateChain?: string; alternativeNames?: string[]; certificateId?: string }
{
cert: string;
privateKey: string;
certificateChain?: string;
caCertificate?: string;
alternativeNames?: string[];
certificateId?: string;
}
>;
export type TCreatePkiSyncDTO = {