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
+2
View File
@@ -55,3 +55,5 @@ docs/integrations/app-connections/redis.mdx:generic-api-key:80
backend/src/ee/services/app-connections/chef/chef-connection-fns.ts:private-key:42 backend/src/ee/services/app-connections/chef/chef-connection-fns.ts:private-key:42
docs/documentation/platform/pki/enrollment-methods/api.mdx:generic-api-key:93 docs/documentation/platform/pki/enrollment-methods/api.mdx:generic-api-key:93
docs/documentation/platform/pki/enrollment-methods/api.mdx:private-key:139 docs/documentation/platform/pki/enrollment-methods/api.mdx:private-key:139
docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx:private-key:62
docs/documentation/platform/pki/certificate-syncs/chef.mdx:private-key:61
@@ -1,7 +1,12 @@
import { seedData1 } from "@app/db/seed-data"; import { seedData1 } from "@app/db/seed-data";
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types"; 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({ const res = await testServer.inject({
method: "POST", method: "POST",
url: `/api/v1/secret-approvals`, url: `/api/v1/secret-approvals`,
@@ -27,7 +32,7 @@ describe("Secret approval policy router", async () => {
const policy = await createPolicy({ const policy = await createPolicy({
secretPath: "/", secretPath: "/",
approvals: 1, approvals: 1,
approvers: [{id:seedData1.id, type: ApproverType.User}], approvers: [{ id: seedData1.id, type: ApproverType.User }],
name: "test-policy" name: "test-policy"
}); });
+1 -1
View File
@@ -2,7 +2,7 @@
import { execSync } from "child_process"; import { execSync } from "child_process";
import path from "path"; import path from "path";
import promptSync from "prompt-sync"; import promptSync from "prompt-sync";
import slugify from "@sindresorhus/slugify" import slugify from "@sindresorhus/slugify";
const prompt = promptSync({ sigint: true }); const prompt = promptSync({ sigint: true });
@@ -27,6 +27,17 @@ export const getChefServerUrl = async (serverUrl?: string) => {
return chefServerUrl; 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 // Helper to ensure private key is in proper PEM format
const formatPrivateKey = (key: string): string => { const formatPrivateKey = (key: string): string => {
let formattedKey = key.trim(); let formattedKey = key.trim();
@@ -138,7 +149,8 @@ export const validateChefConnectionCredentials = async (config: TChefConnectionC
const headers = getChefAuthHeaders("GET", path, "", inputCredentials.userName, inputCredentials.privateKey); const headers = getChefAuthHeaders("GET", path, "", inputCredentials.userName, inputCredentials.privateKey);
await request.get(`${hostServerUrl}${path}`, { const secureUrl = buildSecureUrl(hostServerUrl, path);
await request.get(secureUrl, {
headers headers
}); });
} catch (error: unknown) { } catch (error: unknown) {
@@ -168,7 +180,8 @@ export const listChefDataBags = async (appConnection: TChefConnection): Promise<
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey); 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 headers
}); });
@@ -203,7 +216,8 @@ export const listChefDataBagItems = async (
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey); 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 headers
}); });
@@ -238,7 +252,8 @@ export const getChefDataBagItem = async ({
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey); 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 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 ({ export const updateChefDataBagItem = async ({
serverUrl, serverUrl,
userName, userName,
@@ -272,7 +319,8 @@ export const updateChefDataBagItem = async ({
const headers = getChefAuthHeaders("PUT", path, body, userName, privateKey); 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 headers
}); });
} catch (error) { } 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 { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { registerAwsCertificateManagerPkiSyncRouter } from "./aws-certificate-manager-pki-sync-router"; 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 { registerAzureKeyVaultPkiSyncRouter } from "./azure-key-vault-pki-sync-router";
import { registerChefPkiSyncRouter } from "./chef-pki-sync-router";
export * from "./pki-sync-router"; export * from "./pki-sync-router";
export const PKI_SYNC_REGISTER_ROUTER_MAP: Record<PkiSync, (server: FastifyZodProvider) => Promise<void>> = { export const PKI_SYNC_REGISTER_ROUTER_MAP: Record<PkiSync, (server: FastifyZodProvider) => Promise<void>> = {
[PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter, [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 { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
import { booleanSchema } from "../sanitizedSchemas";
interface CertificateRequestForService { interface CertificateRequestForService {
commonName?: string; commonName?: string;
keyUsages?: CertKeyUsageType[]; keyUsages?: CertKeyUsageType[];
@@ -87,7 +89,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
) )
.optional(), .optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm), signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm) keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
removeRootsFromChain: booleanSchema.default(false).optional()
}) })
.refine(validateTtlAndDateFields, { .refine(validateTtlAndDateFields, {
message: message:
@@ -131,7 +134,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
profileId: req.body.profileId, profileId: req.body.profileId,
certificateRequest: mappedCertificateRequest certificateRequest: mappedCertificateRequest,
removeRootsFromChain: req.body.removeRootsFromChain
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -171,7 +175,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
.min(1, "TTL cannot be empty") .min(1, "TTL cannot be empty")
.refine((val) => ms(val) > 0, "TTL must be a positive number"), .refine((val) => ms(val) > 0, "TTL must be a positive number"),
notBefore: validateCaDateField.optional(), notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional() notAfter: validateCaDateField.optional(),
removeRootsFromChain: booleanSchema.default(false).optional()
}) })
.refine(validateTtlAndDateFields, { .refine(validateTtlAndDateFields, {
message: message:
@@ -206,7 +211,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
}, },
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined, notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : 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({ await server.services.auditLog.createAuditLog({
@@ -262,7 +268,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
notAfter: validateCaDateField.optional(), notAfter: validateCaDateField.optional(),
commonName: validateTemplateRegexField.optional(), commonName: validateTemplateRegexField.optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm), signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm) keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
removeRootsFromChain: booleanSchema.default(false).optional()
}) })
.refine(validateTtlAndDateFields, { .refine(validateTtlAndDateFields, {
message: message:
@@ -325,7 +332,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined, notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
signatureAlgorithm: req.body.signatureAlgorithm, signatureAlgorithm: req.body.signatureAlgorithm,
keyAlgorithm: req.body.keyAlgorithm keyAlgorithm: req.body.keyAlgorithm
} },
removeRootsFromChain: req.body.removeRootsFromChain
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -357,6 +365,11 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
params: z.object({ params: z.object({
certificateId: z.string().uuid() certificateId: z.string().uuid()
}), }),
body: z
.object({
removeRootsFromChain: booleanSchema.default(false).optional()
})
.optional(),
response: { response: {
200: z.object({ 200: z.object({
certificate: z.string().trim(), certificate: z.string().trim(),
@@ -375,7 +388,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
certificateId: req.params.certificateId certificateId: req.params.certificateId,
removeRootsFromChain: req.body?.removeRootsFromChain
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -170,7 +170,8 @@ const PKI_APP_CONNECTIONS = [
AppConnection.AWS, AppConnection.AWS,
AppConnection.Cloudflare, AppConnection.Cloudflare,
AppConnection.AzureADCS, AppConnection.AzureADCS,
AppConnection.AzureKeyVault AppConnection.AzureKeyVault,
AppConnection.Chef
]; ];
export const listAppConnectionOptions = (projectType?: ProjectType) => { export const listAppConnectionOptions = (projectType?: ProjectType) => {
@@ -196,3 +196,62 @@ export const convertExtendedKeyUsageArrayToLegacy = (
): CertExtendedKeyUsage[] | undefined => { ): CertExtendedKeyUsage[] | undefined => {
return usages?.map(convertToLegacyExtendedKeyUsage); 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, convertKeyUsageArrayFromLegacy,
convertKeyUsageArrayToLegacy, convertKeyUsageArrayToLegacy,
mapEnumsForValidation, mapEnumsForValidation,
normalizeDateForApi normalizeDateForApi,
removeRootCaFromChain
} from "../certificate-common/certificate-utils"; } from "../certificate-common/certificate-utils";
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal"; import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal"; import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
@@ -366,7 +367,8 @@ export const certificateV3ServiceFactory = ({
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
removeRootsFromChain
}: TIssueCertificateFromProfileDTO): Promise<TCertificateFromProfileResponse> => { }: TIssueCertificateFromProfileDTO): Promise<TCertificateFromProfileResponse> => {
const profile = await validateProfileAndPermissions( const profile = await validateProfileAndPermissions(
profileId, profileId,
@@ -480,10 +482,15 @@ export const certificateV3ServiceFactory = ({
renewBeforeDays: finalRenewBeforeDays renewBeforeDays: finalRenewBeforeDays
}); });
let finalCertificateChain = bufferToString(certificateChain);
if (removeRootsFromChain) {
finalCertificateChain = removeRootCaFromChain(finalCertificateChain);
}
return { return {
certificate: bufferToString(certificate), certificate: bufferToString(certificate),
issuingCaCertificate: bufferToString(issuingCaCertificate), issuingCaCertificate: bufferToString(issuingCaCertificate),
certificateChain: bufferToString(certificateChain), certificateChain: finalCertificateChain,
privateKey: bufferToString(privateKey), privateKey: bufferToString(privateKey),
serialNumber, serialNumber,
certificateId: cert.id, certificateId: cert.id,
@@ -503,7 +510,8 @@ export const certificateV3ServiceFactory = ({
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
enrollmentType enrollmentType,
removeRootsFromChain
}: TSignCertificateFromProfileDTO): Promise<Omit<TCertificateFromProfileResponse, "privateKey">> => { }: TSignCertificateFromProfileDTO): Promise<Omit<TCertificateFromProfileResponse, "privateKey">> => {
const profile = await validateProfileAndPermissions( const profile = await validateProfileAndPermissions(
profileId, profileId,
@@ -590,7 +598,10 @@ export const certificateV3ServiceFactory = ({
}); });
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer); 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 { return {
certificate: certificateString, certificate: certificateString,
@@ -610,7 +621,8 @@ export const certificateV3ServiceFactory = ({
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
removeRootsFromChain
}: TOrderCertificateFromProfileDTO): Promise<TCertificateOrderResponse> => { }: TOrderCertificateFromProfileDTO): Promise<TCertificateOrderResponse> => {
const profile = await validateProfileAndPermissions( const profile = await validateProfileAndPermissions(
profileId, profileId,
@@ -665,7 +677,8 @@ export const certificateV3ServiceFactory = ({
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
removeRootsFromChain
}); });
const orderId = randomUUID(); const orderId = randomUUID();
@@ -703,7 +716,8 @@ export const certificateV3ServiceFactory = ({
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
internal = false internal = false,
removeRootsFromChain
}: TRenewCertificateDTO & { internal?: boolean }): Promise<TCertificateFromProfileResponse> => { }: TRenewCertificateDTO & { internal?: boolean }): Promise<TCertificateFromProfileResponse> => {
const renewalResult = await certificateDAL.transaction(async (tx) => { const renewalResult = await certificateDAL.transaction(async (tx) => {
const originalCert = await certificateDAL.findById(certificateId, tx); const originalCert = await certificateDAL.findById(certificateId, tx);
@@ -929,10 +943,14 @@ export const certificateV3ServiceFactory = ({
pkiSyncQueue pkiSyncQueue
}); });
let finalCertificateChain = renewalResult.certificateChain;
if (removeRootsFromChain) {
finalCertificateChain = removeRootCaFromChain(finalCertificateChain);
}
return { return {
certificate: renewalResult.certificate, certificate: renewalResult.certificate,
issuingCaCertificate: renewalResult.issuingCaCertificate, issuingCaCertificate: renewalResult.issuingCaCertificate,
certificateChain: renewalResult.certificateChain, certificateChain: finalCertificateChain,
serialNumber: renewalResult.serialNumber, serialNumber: renewalResult.serialNumber,
certificateId: renewalResult.newCert.id, certificateId: renewalResult.newCert.id,
projectId: renewalResult.profile.projectId, projectId: renewalResult.profile.projectId,
@@ -26,6 +26,7 @@ export type TIssueCertificateFromProfileDTO = {
signatureAlgorithm?: string; signatureAlgorithm?: string;
keyAlgorithm?: string; keyAlgorithm?: string;
}; };
removeRootsFromChain?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TSignCertificateFromProfileDTO = { export type TSignCertificateFromProfileDTO = {
@@ -37,6 +38,7 @@ export type TSignCertificateFromProfileDTO = {
notBefore?: Date; notBefore?: Date;
notAfter?: Date; notAfter?: Date;
enrollmentType: EnrollmentType; enrollmentType: EnrollmentType;
removeRootsFromChain?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TOrderCertificateFromProfileDTO = { export type TOrderCertificateFromProfileDTO = {
@@ -57,6 +59,7 @@ export type TOrderCertificateFromProfileDTO = {
signatureAlgorithm?: string; signatureAlgorithm?: string;
keyAlgorithm?: string; keyAlgorithm?: string;
}; };
removeRootsFromChain?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TCertificateFromProfileResponse = { export type TCertificateFromProfileResponse = {
@@ -101,6 +104,7 @@ export type TCertificateOrderResponse = {
export type TRenewCertificateDTO = { export type TRenewCertificateDTO = {
certificateId: string; certificateId: string;
removeRootsFromChain?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TUpdateRenewalConfigDTO = { export type TUpdateRenewalConfigDTO = {
@@ -14,6 +14,7 @@ export const AwsCertificateManagerPkiSyncConfigSchema = z.object({
const AwsCertificateManagerPkiSyncOptionsSchema = z.object({ const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
canImportCertificates: z.boolean().default(false), canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(true), canRemoveCertificates: z.boolean().default(true),
includeRootCa: z.boolean().default(false),
preserveArn: z.boolean().default(true), preserveArn: z.boolean().default(true),
certificateNameSchema: z certificateNameSchema: z
.string() .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({ const AzureKeyVaultPkiSyncOptionsSchema = z.object({
canImportCertificates: z.boolean().default(false), canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(true), canRemoveCertificates: z.boolean().default(true),
includeRootCa: z.boolean().default(false),
enableVersioning: z.boolean().default(true), enableVersioning: z.boolean().default(true),
certificateNameSchema: z certificateNameSchema: z
.string() .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 { export enum PkiSync {
AzureKeyVault = "azure-key-vault", AzureKeyVault = "azure-key-vault",
AwsCertificateManager = "aws-certificate-manager" AwsCertificateManager = "aws-certificate-manager",
AwsSecretsManager = "aws-secrets-manager",
Chef = "chef"
} }
export enum PkiSyncStatus { 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 { 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 { 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 { 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 { 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 { PkiSync } from "./pki-sync-enums";
import { TCertificateMap, TPkiSyncWithCredentials } from "./pki-sync-types"; import { TCertificateMap, TPkiSyncWithCredentials } from "./pki-sync-types";
@@ -19,7 +23,9 @@ const ENTERPRISE_PKI_SYNCS: PkiSync[] = [];
const PKI_SYNC_LIST_OPTIONS = { const PKI_SYNC_LIST_OPTIONS = {
[PkiSync.AzureKeyVault]: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION, [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 ( export const enterprisePkiSyncCheck = async (
@@ -162,6 +168,8 @@ export const PkiSyncFns = {
dependencies: { dependencies: {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">; appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
certificateDAL: TCertificateDALFactory;
certificateSyncDAL: TCertificateSyncDALFactory;
} }
): Promise<TCertificateMap> => { ): Promise<TCertificateMap> => {
switch (pkiSync.destination) { 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)" "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: default:
throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`); throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`);
} }
@@ -203,7 +219,7 @@ export const PkiSyncFns = {
}> => { }> => {
switch (pkiSync.destination) { switch (pkiSync.destination) {
case PkiSync.AzureKeyVault: { case PkiSync.AzureKeyVault: {
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault); checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault as PkiSync);
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({ const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({
appConnectionDAL: dependencies.appConnectionDAL, appConnectionDAL: dependencies.appConnectionDAL,
kmsService: dependencies.kmsService, kmsService: dependencies.kmsService,
@@ -213,7 +229,7 @@ export const PkiSyncFns = {
return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap); return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap);
} }
case PkiSync.AwsCertificateManager: { case PkiSync.AwsCertificateManager: {
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager); checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager as PkiSync);
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({ const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({
appConnectionDAL: dependencies.appConnectionDAL, appConnectionDAL: dependencies.appConnectionDAL,
kmsService: dependencies.kmsService, kmsService: dependencies.kmsService,
@@ -222,6 +238,22 @@ export const PkiSyncFns = {
}); });
return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap); 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: default:
throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`); throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`);
} }
@@ -240,7 +272,7 @@ export const PkiSyncFns = {
): Promise<void> => { ): Promise<void> => {
switch (pkiSync.destination) { switch (pkiSync.destination) {
case PkiSync.AzureKeyVault: { case PkiSync.AzureKeyVault: {
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault); checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault as PkiSync);
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({ const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({
appConnectionDAL: dependencies.appConnectionDAL, appConnectionDAL: dependencies.appConnectionDAL,
kmsService: dependencies.kmsService, kmsService: dependencies.kmsService,
@@ -254,7 +286,7 @@ export const PkiSyncFns = {
break; break;
} }
case PkiSync.AwsCertificateManager: { case PkiSync.AwsCertificateManager: {
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager); checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager as PkiSync);
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({ const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({
appConnectionDAL: dependencies.appConnectionDAL, appConnectionDAL: dependencies.appConnectionDAL,
kmsService: dependencies.kmsService, kmsService: dependencies.kmsService,
@@ -267,6 +299,27 @@ export const PkiSyncFns = {
}); });
break; 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: default:
throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`); 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> = { export const PKI_SYNC_NAME_MAP: Record<PkiSync, string> = {
[PkiSync.AzureKeyVault]: "Azure Key Vault", [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> = { export const PKI_SYNC_CONNECTION_MAP: Record<PkiSync, AppConnection> = {
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault, [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 { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { getCaCertChain } from "../certificate-authority/certificate-authority-fns"; 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 { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
import { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums"; import { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums";
import { TPkiSyncDALFactory } from "./pki-sync-dal"; import { TPkiSyncDALFactory } from "./pki-sync-dal";
@@ -180,11 +181,16 @@ export const pkiSyncQueueFactory = ({
(cert, index, self) => self.findIndex((c) => c.id === cert.id) === index (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 }; return { certificateMap, certificateMetadata };
} }
certificates = uniqueCertificates; certificates = activeCertificates;
for (const certificate of certificates) { for (const certificate of certificates) {
const cert = certificate as TCertificates; const cert = certificate as TCertificates;
@@ -231,13 +237,15 @@ export const pkiSyncQueueFactory = ({
} }
let certificateChain: string | undefined; let certificateChain: string | undefined;
let caCertificate: string | undefined;
try { try {
if (certBody.encryptedCertificateChain) { if (certBody.encryptedCertificateChain) {
const decryptedCertChain = await kmsDecryptor({ const decryptedCertChain = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificateChain cipherTextBlob: certBody.encryptedCertificateChain
}); });
certificateChain = decryptedCertChain.toString(); certificateChain = decryptedCertChain.toString();
} else if (certificate.caCertId) { }
if (certificate.caCertId) {
const { caCert, caCertChain } = await getCaCertChain({ const { caCert, caCertChain } = await getCaCertChain({
caCertId: certificate.caCertId, caCertId: certificate.caCertId,
certificateAuthorityDAL, certificateAuthorityDAL,
@@ -245,7 +253,10 @@ export const pkiSyncQueueFactory = ({
projectDAL, projectDAL,
kmsService kmsService
}); });
certificateChain = `${caCert}\n${caCertChain}`.trim(); if (!certBody.encryptedCertificateChain) {
certificateChain = `${caCert}\n${caCertChain}`.trim();
}
caCertificate = certificateChain ? extractRootCaFromChain(certificateChain) : caCert;
} }
} catch (chainError) { } catch (chainError) {
logger.warn( logger.warn(
@@ -254,10 +265,16 @@ export const pkiSyncQueueFactory = ({
); );
// Continue without certificate chain // Continue without certificate chain
certificateChain = undefined; certificateChain = undefined;
caCertificate = undefined;
} }
let certificateName: string; 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; const certificateNameSchema = syncOptions?.certificateNameSchema;
if (certificateNameSchema) { if (certificateNameSchema) {
@@ -289,10 +306,16 @@ export const pkiSyncQueueFactory = ({
alternativeNames.push(originalLegacyName); alternativeNames.push(originalLegacyName);
} }
let processedCertificateChain = certificateChain;
if (certificateChain && syncOptions?.includeRootCa === false) {
processedCertificateChain = removeRootCaFromChain(certificateChain);
}
certificateMap[certificateName] = { certificateMap[certificateName] = {
cert: certificatePem, cert: certificatePem,
privateKey: certPrivateKey || "", privateKey: certPrivateKey || "",
certificateChain, certificateChain: processedCertificateChain,
caCertificate,
alternativeNames, alternativeNames,
certificateId: certificate.id certificateId: certificate.id
}; };
@@ -7,6 +7,7 @@ import { PkiSync } from "./pki-sync-enums";
export const PkiSyncOptionsSchema = z.object({ export const PkiSyncOptionsSchema = z.object({
canImportCertificates: z.boolean(), canImportCertificates: z.boolean(),
canRemoveCertificates: z.boolean().optional(), canRemoveCertificates: z.boolean().optional(),
includeRootCa: z.boolean().optional().default(false),
certificateNameSchema: z certificateNameSchema: z
.string() .string()
.optional() .optional()
@@ -73,7 +73,14 @@ export type TPkiSyncListItem = TPkiSync & {
export type TCertificateMap = Record< export type TCertificateMap = Record<
string, 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 = { export type TCreatePkiSyncDTO = {
@@ -0,0 +1,4 @@
---
title: "Create AWS Secrets Manager PKI Sync"
openapi: "POST /api/v1/pki/syncs/aws-secrets-manager"
---
@@ -0,0 +1,4 @@
---
title: "Delete AWS Secrets Manager PKI Sync"
openapi: "DELETE /api/v1/pki/syncs/aws-secrets-manager/{pkiSyncId}"
---
@@ -0,0 +1,4 @@
---
title: "Get AWS Secrets Manager PKI Sync by ID"
openapi: "GET /api/v1/pki/syncs/aws-secrets-manager/{pkiSyncId}"
---
@@ -0,0 +1,4 @@
---
title: "List AWS Secrets Manager PKI Syncs"
openapi: "GET /api/v1/pki/syncs/aws-secrets-manager"
---
@@ -0,0 +1,4 @@
---
title: "Remove Certificates from AWS Secrets Manager"
openapi: "POST /api/v1/pki/syncs/aws-secrets-manager/{pkiSyncId}/remove-certificates"
---
@@ -0,0 +1,4 @@
---
title: "Sync Certificates to AWS Secrets Manager"
openapi: "POST /api/v1/pki/syncs/aws-secrets-manager/{pkiSyncId}/sync-certificates"
---
@@ -0,0 +1,4 @@
---
title: "Update AWS Secrets Manager PKI Sync"
openapi: "PATCH /api/v1/pki/syncs/aws-secrets-manager/{pkiSyncId}"
---
@@ -0,0 +1,4 @@
---
title: "Create Chef PKI Sync"
openapi: "POST /api/v1/pki/syncs/chef"
---
@@ -0,0 +1,4 @@
---
title: "Delete Chef PKI Sync"
openapi: "DELETE /api/v1/pki/syncs/chef/{pkiSyncId}"
---
@@ -0,0 +1,4 @@
---
title: "Get Chef PKI Sync by ID"
openapi: "GET /api/v1/pki/syncs/chef/{pkiSyncId}"
---
@@ -0,0 +1,4 @@
---
title: "List Chef PKI Syncs"
openapi: "GET /api/v1/pki/syncs/chef"
---
@@ -0,0 +1,4 @@
---
title: "Remove Certificates from Chef"
openapi: "POST /api/v1/pki/syncs/chef/{pkiSyncId}/remove-certificates"
---
@@ -0,0 +1,4 @@
---
title: "Sync Certificates to Chef"
openapi: "POST /api/v1/pki/syncs/chef/{pkiSyncId}/sync"
---
@@ -0,0 +1,4 @@
---
title: "Update Chef PKI Sync"
openapi: "PATCH /api/v1/pki/syncs/chef/{pkiSyncId}"
---
+27 -1
View File
@@ -766,7 +766,9 @@
"pages": [ "pages": [
"documentation/platform/pki/certificate-syncs/overview", "documentation/platform/pki/certificate-syncs/overview",
"documentation/platform/pki/certificate-syncs/aws-certificate-manager", "documentation/platform/pki/certificate-syncs/aws-certificate-manager",
"documentation/platform/pki/certificate-syncs/azure-key-vault" "documentation/platform/pki/certificate-syncs/aws-secrets-manager",
"documentation/platform/pki/certificate-syncs/azure-key-vault",
"documentation/platform/pki/certificate-syncs/chef"
] ]
}, },
{ {
@@ -2617,6 +2619,18 @@
"api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates" "api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates"
] ]
}, },
{
"group": "AWS Secrets Manager",
"pages": [
"api-reference/endpoints/pki/syncs/aws-secrets-manager/list",
"api-reference/endpoints/pki/syncs/aws-secrets-manager/get-by-id",
"api-reference/endpoints/pki/syncs/aws-secrets-manager/create",
"api-reference/endpoints/pki/syncs/aws-secrets-manager/update",
"api-reference/endpoints/pki/syncs/aws-secrets-manager/delete",
"api-reference/endpoints/pki/syncs/aws-secrets-manager/sync-certificates",
"api-reference/endpoints/pki/syncs/aws-secrets-manager/remove-certificates"
]
},
{ {
"group": "Azure Key Vault", "group": "Azure Key Vault",
"pages": [ "pages": [
@@ -2628,6 +2642,18 @@
"api-reference/endpoints/pki/syncs/azure-key-vault/sync-certificates", "api-reference/endpoints/pki/syncs/azure-key-vault/sync-certificates",
"api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates" "api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates"
] ]
},
{
"group": "Chef",
"pages": [
"api-reference/endpoints/pki/syncs/chef/list",
"api-reference/endpoints/pki/syncs/chef/get-by-id",
"api-reference/endpoints/pki/syncs/chef/create",
"api-reference/endpoints/pki/syncs/chef/update",
"api-reference/endpoints/pki/syncs/chef/delete",
"api-reference/endpoints/pki/syncs/chef/sync-certificates",
"api-reference/endpoints/pki/syncs/chef/remove-certificates"
]
} }
] ]
} }
@@ -39,6 +39,7 @@ These permissions allow Infisical to list, import, tag, and manage certificates
- **Enable Removal of Expired/Revoked Certificates**: If enabled, Infisical will remove certificates from the destination if they are no longer active in Infisical. - **Enable Removal of Expired/Revoked Certificates**: If enabled, Infisical will remove certificates from the destination if they are no longer active in Infisical.
- **Preserve ARN on Renewal**: If enabled, Infisical will sync renewed certificates to the destination under the same ARN as the original synced certificate instead of creating a new certificate with a new ARN. - **Preserve ARN on Renewal**: If enabled, Infisical will sync renewed certificates to the destination under the same ARN as the original synced certificate instead of creating a new certificate with a new ARN.
- **Include Root CA**: If enabled, the Root CA certificate will be included in the certificate chain when syncing to AWS Certificate Manager. If disabled, only intermediate certificates will be included.
- **Certificate Name Schema** (Optional): Customize how certificate tags are generated in AWS Certificate Manager. Must include `{{certificateId}}` as a placeholder for the certificate ID to ensure proper certificate identification and management. If not specified, defaults to `Infisical-{{certificateId}}`. - **Certificate Name Schema** (Optional): Customize how certificate tags are generated in AWS Certificate Manager. Must include `{{certificateId}}` as a placeholder for the certificate ID to ensure proper certificate identification and management. If not specified, defaults to `Infisical-{{certificateId}}`.
- **Auto-Sync Enabled**: If enabled, certificates will automatically be synced when changes occur. Disable to enforce manual syncing only. - **Auto-Sync Enabled**: If enabled, certificates will automatically be synced when changes occur. Disable to enforce manual syncing only.
@@ -86,6 +87,7 @@ These permissions allow Infisical to list, import, tag, and manage certificates
"syncOptions": { "syncOptions": {
"canRemoveCertificates": true, "canRemoveCertificates": true,
"preserveArnOnRenewal": true, "preserveArnOnRenewal": true,
"includeRootCa": false,
"certificateNameSchema": "myapp-{{certificateId}}" "certificateNameSchema": "myapp-{{certificateId}}"
}, },
"destinationConfig": { "destinationConfig": {
@@ -110,6 +112,7 @@ These permissions allow Infisical to list, import, tag, and manage certificates
"syncOptions": { "syncOptions": {
"canRemoveCertificates": true, "canRemoveCertificates": true,
"preserveArnOnRenewal": true, "preserveArnOnRenewal": true,
"includeRootCa": false,
"certificateNameSchema": "myapp-{{certificateId}}" "certificateNameSchema": "myapp-{{certificateId}}"
}, },
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
@@ -0,0 +1,251 @@
---
title: "AWS Secrets Manager"
description: "Learn how to configure an AWS Secrets Manager Certificate Sync for Infisical PKI."
---
**Prerequisites:**
- Create an [AWS Connection](/integrations/app-connections/aws)
- Ensure your network security policies allow incoming requests from Infisical to this certificate sync provider, if network restrictions apply.
<Note>
The AWS Secrets Manager Certificate Sync requires the following permissions to be set on the AWS IAM user
for Infisical to sync certificates to AWS Secrets Manager: `secretsmanager:CreateSecret`, `secretsmanager:UpdateSecret`,
`secretsmanager:GetSecretValue`, `secretsmanager:DeleteSecret`, `secretsmanager:ListSecrets`.
Any role with these permissions would work such as a custom policy with **SecretsManager** permissions.
</Note>
<Note>
Certificates synced to AWS Secrets Manager will be stored as JSON secrets,
preserving both the certificate and private key components as separate fields within the secret value.
</Note>
<Tabs>
<Tab title="Infisical UI">
1. Navigate to **Project** > **Integrations** > **Certificate Syncs** and press **Add Sync**.
![Certificate Syncs Tab](/images/platform/pki/certificate-syncs/general/create-certificate-sync.png)
2. Select the **AWS Secrets Manager** option.
![Select AWS Secrets Manager](/images/platform/pki/certificate-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png)
3. Configure the **Destination** to where certificates should be deployed, then click **Next**.
![Configure Destination](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-destination.png)
- **AWS Connection**: The AWS Connection to authenticate with.
- **Region**: The AWS region where secrets will be stored.
4. Configure the **Sync Options** to specify how certificates should be synced, then click **Next**.
![Configure Options](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-options.png)
- **Enable Removal of Expired/Revoked Certificates**: If enabled, Infisical will remove certificates from the destination if they are no longer active in Infisical.
- **Preserve Secret on Renewal**: Only applies to certificate renewals. When a certificate is renewed in Infisical, this option controls how the renewed certificate is handled. If enabled, the renewed certificate will update the existing secret, preserving the same secret name. If disabled, the renewed certificate will be created as a new secret with a new name.
- **Include Root CA**: If enabled, the Root CA certificate will be included in the certificate chain when syncing to AWS Secrets Manager. If disabled, only intermediate certificates will be included.
- **Certificate Name Schema** (Optional): Customize how secret names are generated in AWS Secrets Manager. Use `{{certificateId}}` as a placeholder for the certificate ID.
- **Auto-Sync Enabled**: If enabled, certificates will automatically be synced when changes occur. Disable to enforce manual syncing only.
5. Configure the **Field Mappings** to customize how certificate data is stored in AWS Secrets Manager secrets, then click **Next**.
![Configure Field Mappings](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-field-mappings.png)
- **Certificate Field**: The field name where the certificate will be stored in the secret value (default: `certificate`)
- **Private Key Field**: The field name where the private key will be stored in the secret value (default: `private_key`)
- **Certificate Chain Field**: The field name where the full certificate chain excluding the root CA certificate will be stored (default: `certificate_chain`)
- **CA Certificate Field**: The field name where the root CA certificate will be stored (default: `ca_certificate`)
<Tip>
**AWS Secrets Manager Secret Structure**: Certificates are stored in AWS Secrets Manager as JSON secrets with the following structure (field names can be customized via field mappings):
```json
{
"certificate": "-----BEGIN CERTIFICATE-----\n...",
"private_key": "-----BEGIN PRIVATE KEY-----\n...",
"certificate_chain": "-----BEGIN CERTIFICATE-----\n...",
"ca_certificate": "-----BEGIN CERTIFICATE-----\n..."
}
```
**Example with Custom Field Mappings**:
```json
{
"ssl_cert": "-----BEGIN CERTIFICATE-----\n...",
"ssl_key": "-----BEGIN PRIVATE KEY-----\n...",
"ssl_chain": "-----BEGIN CERTIFICATE-----\n...",
"ssl_ca": "-----BEGIN CERTIFICATE-----\n..."
}
```
</Tip>
6. Configure the **Details** of your AWS Secrets Manager Certificate Sync, then click **Next**.
![Configure Details](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-details.png)
- **Name**: The name of your sync. Must be slug-friendly.
- **Description**: An optional description for your sync.
7. Select which certificates should be synced to AWS Secrets Manager.
![Select Certificates](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-certificates.png)
8. Review your AWS Secrets Manager Certificate Sync configuration, then click **Create Sync**.
![Confirm Configuration](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-review.png)
9. If enabled, your AWS Secrets Manager Certificate Sync will begin syncing your certificates to the destination endpoint.
![Sync Certificates](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-synced.png)
</Tab>
<Tab title="API">
To create an **AWS Secrets Manager Certificate Sync**, make an API request to the [Create AWS Secrets Manager Certificate Sync](/api-reference/endpoints/pki/syncs/aws-secrets-manager/create) API endpoint.
### Sample request
<Note>
You can optionally specify `certificateIds` during sync creation to immediately add certificates to the sync.
If not provided, you can add certificates later using the certificate management endpoints.
</Note>
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/pki/syncs/aws-secrets-manager \
--header 'Authorization: Bearer <access-token>' \
--header 'Content-Type: application/json' \
--data '{
"name": "my-aws-secrets-manager-cert-sync",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "an example certificate sync",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"destination": "aws-secrets-manager",
"isAutoSyncEnabled": true,
"certificateIds": [
"550e8400-e29b-41d4-a716-446655440000",
"660f1234-e29b-41d4-a716-446655440001"
],
"syncOptions": {
"canRemoveCertificates": true,
"preserveSecretOnRenewal": true,
"canImportCertificates": false,
"includeRootCa": false,
"certificateNameSchema": "myapp-{{certificateId}}",
"fieldMappings": {
"certificate": "ssl_cert",
"privateKey": "ssl_key",
"certificateChain": "ssl_chain",
"caCertificate": "ssl_ca"
}
},
"destinationConfig": {
"region": "us-east-1",
"keyId": "alias/my-kms-key"
}
}'
```
### Example with Default Field Mappings
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/pki/syncs/aws-secrets-manager \
--header 'Authorization: Bearer <access-token>' \
--header 'Content-Type: application/json' \
--data '{
"name": "my-aws-secrets-manager-cert-sync-default",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "AWS Secrets Manager sync with default field mappings",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"destination": "aws-secrets-manager",
"isAutoSyncEnabled": true,
"syncOptions": {
"canRemoveCertificates": true,
"preserveSecretOnRenewal": true,
"canImportCertificates": false,
"includeRootCa": false,
"certificateNameSchema": "infisical-{{certificateId}}",
"fieldMappings": {
"certificate": "certificate",
"privateKey": "private_key",
"certificateChain": "certificate_chain",
"caCertificate": "ca_certificate"
}
},
"destinationConfig": {
"region": "us-west-2"
}
}'
```
### Sample response
```json Response
{
"pkiSync": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-aws-secrets-manager-cert-sync",
"description": "an example certificate sync",
"destination": "aws-secrets-manager",
"isAutoSyncEnabled": true,
"destinationConfig": {
"region": "us-east-1",
"keyId": "alias/my-kms-key"
},
"syncOptions": {
"canRemoveCertificates": true,
"preserveSecretOnRenewal": true,
"canImportCertificates": false,
"includeRootCa": false,
"certificateNameSchema": "myapp-{{certificateId}}",
"fieldMappings": {
"certificate": "ssl_cert",
"privateKey": "ssl_key",
"certificateChain": "ssl_chain",
"caCertificate": "ssl_ca"
}
},
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-01-01T00:00:00.000Z",
"updatedAt": "2023-01-01T00:00:00.000Z"
}
}
```
</Tab>
</Tabs>
## Certificate Management
Your AWS Secrets Manager Certificate Sync will:
- **Automatic Deployment**: Deploy certificates in Infisical to AWS Secrets Manager as JSON secrets with customizable field names
- **Certificate Updates**: Update certificates in AWS Secrets Manager when renewals occur
- **Expiration Handling**: Optionally remove expired certificates from AWS Secrets Manager (if enabled)
- **Format Preservation**: Maintain certificate format during sync operations
- **Field Customization**: Map certificate data to custom field names that match your application requirements
- **CA Certificate Support**: Include CA certificates in secrets for complete certificate chain management
- **KMS Encryption**: Optionally use custom KMS keys for secret encryption
- **Regional Deployment**: Deploy secrets to specific AWS regions
<Note>
AWS Secrets Manager Certificate Syncs support both automatic and manual
synchronization modes. When auto-sync is enabled, certificates are
automatically deployed as they are issued or renewed.
</Note>
## Manual Certificate Sync
You can manually trigger certificate synchronization to AWS Secrets Manager using the sync certificates functionality. This is useful for:
- Initial setup when you have existing certificates to deploy
- One-time sync of specific certificates
- Testing certificate sync configurations
- Force sync after making changes
To manually sync certificates, use the [Sync Certificates](/api-reference/endpoints/pki/syncs/aws-secrets-manager/sync-certificates) API endpoint or the manual sync option in the Infisical UI.
<Note>
AWS Secrets Manager does not support importing certificates back into Infisical
due to the nature of AWS Secrets Manager where certificates are stored as JSON secrets
rather than managed certificate objects.
</Note>
## Secret Naming Constraints
AWS Secrets Manager has specific naming requirements for secrets:
- **Allowed Characters**: Letters, numbers, hyphens (-), and underscores (_) only
- **Length**: 1-512 characters
@@ -40,6 +40,7 @@ Any role with these permissions would work such as the **Key Vault Certificates
- **Enable Removal of Expired/Revoked Certificates**: If enabled, Infisical will remove certificates from the destination if they are no longer active in Infisical. - **Enable Removal of Expired/Revoked Certificates**: If enabled, Infisical will remove certificates from the destination if they are no longer active in Infisical.
- **Enable Versioning on Renewal**: If enabled, Infisical will sync renewed certificates to the destination under a new version of the original synced certificate instead of creating a new certificate. - **Enable Versioning on Renewal**: If enabled, Infisical will sync renewed certificates to the destination under a new version of the original synced certificate instead of creating a new certificate.
- **Include Root CA**: If enabled, the Root CA certificate will be included in the certificate chain when syncing to Azure Key Vault. If disabled, only intermediate certificates will be included.
- **Certificate Name Schema** (Optional): Customize how certificate names are generated in Azure Key Vault. Use `{{certificateId}}` as a placeholder for the certificate ID. If not specified, defaults to `Infisical-{{certificateId}}`. - **Certificate Name Schema** (Optional): Customize how certificate names are generated in Azure Key Vault. Use `{{certificateId}}` as a placeholder for the certificate ID. If not specified, defaults to `Infisical-{{certificateId}}`.
- **Auto-Sync Enabled**: If enabled, certificates will automatically be synced when changes occur. Disable to enforce manual syncing only. - **Auto-Sync Enabled**: If enabled, certificates will automatically be synced when changes occur. Disable to enforce manual syncing only.
@@ -93,6 +94,7 @@ Any role with these permissions would work such as the **Key Vault Certificates
"syncOptions": { "syncOptions": {
"canRemoveCertificates": true, "canRemoveCertificates": true,
"enableVersioningOnRenewal": true, "enableVersioningOnRenewal": true,
"includeRootCa": false,
"certificateNameSchema": "myapp-{{certificateId}}" "certificateNameSchema": "myapp-{{certificateId}}"
}, },
"destinationConfig": { "destinationConfig": {
@@ -117,6 +119,7 @@ Any role with these permissions would work such as the **Key Vault Certificates
"syncOptions": { "syncOptions": {
"canRemoveCertificates": true, "canRemoveCertificates": true,
"enableVersioningOnRenewal": true, "enableVersioningOnRenewal": true,
"includeRootCa": false,
"certificateNameSchema": "myapp-{{certificateId}}" "certificateNameSchema": "myapp-{{certificateId}}"
}, },
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
@@ -0,0 +1,241 @@
---
title: "Chef"
description: "Learn how to configure a Chef Certificate Sync for Infisical PKI."
---
**Prerequisites:**
- Create a [Chef Connection](/integrations/app-connections/chef)
- Ensure your network security policies allow incoming requests from Infisical to this certificate sync provider, if network restrictions apply.
<Note>
The Chef Certificate Sync requires the following permissions to be set on the Chef user
for Infisical to sync certificates to Chef: `data bag read`, `data bag create`, `data bag update`, `data bag delete`.
Any role with these permissions would work such as a custom role with **Data Bag** permissions.
</Note>
<Note>
Certificates synced to Chef will be stored as data bag items within the specified data bag,
preserving both the certificate and private key components as separate fields.
</Note>
<Tabs>
<Tab title="Infisical UI">
1. Navigate to **Project** > **Integrations** > **Certificate Syncs** and press **Add Sync**.
![Certificate Syncs Tab](/images/platform/pki/certificate-syncs/general/create-certificate-sync.png)
2. Select the **Chef** option.
![Select Chef](/images/platform/pki/certificate-syncs/chef/select-chef-option.png)
3. Configure the **Destination** to where certificates should be deployed, then click **Next**.
![Configure Destination](/images/platform/pki/certificate-syncs/chef/chef-destination.png)
- **Chef Connection**: The Chef Connection to authenticate with.
- **Data Bag Name**: The name of the Chef data bag where certificates will be stored.
4. Configure the **Sync Options** to specify how certificates should be synced, then click **Next**.
![Configure Options](/images/platform/pki/certificate-syncs/chef/chef-options.png)
- **Enable Removal of Expired/Revoked Certificates**: If enabled, Infisical will remove certificates from the destination if they are no longer active in Infisical.
- **Preserve Data Bag Item on Renewal**: Only applies to certificate renewals. When a certificate is renewed in Infisical, this option controls how the renewed certificate is handled. If enabled, the renewed certificate will update the existing data bag item, preserving the same item name. If disabled, the renewed certificate will be created as a new data bag item with a new name.
- **Include Root CA**: If enabled, the Root CA certificate will be included in the certificate chain when syncing to Chef data bags. If disabled, only intermediate certificates will be included.
- **Certificate Name Schema** (Optional): Customize how certificate item names are generated in Chef data bags. Use `{{certificateId}}` as a placeholder for the certificate ID.
- **Auto-Sync Enabled**: If enabled, certificates will automatically be synced when changes occur. Disable to enforce manual syncing only.
5. Configure the **Field Mappings** to customize how certificate data is stored in Chef data bag items, then click **Next**.
![Configure Field Mappings](/images/platform/pki/certificate-syncs/chef/chef-field-mappings.png)
- **Certificate Field**: The field name where the certificate will be stored in the data bag item (default: `certificate`)
- **Private Key Field**: The field name where the private key will be stored in the data bag item (default: `private_key`)
- **Certificate Chain Field**: The field name where the full certificate chain excluding the root CA certificate will be stored (default: `certificate_chain`)
- **CA Certificate Field**: The field name where the root CA certificate will be stored (default: `ca_certificate`)
<Tip>
**Chef Data Bag Item Structure**: Certificates are stored in Chef data bags as items with the following structure (field names can be customized via field mappings):
```json
{
"id": "certificate-item-name",
"certificate": "-----BEGIN CERTIFICATE-----\n...",
"private_key": "-----BEGIN PRIVATE KEY-----\n...",
"certificate_chain": "-----BEGIN CERTIFICATE-----\n...",
"ca_certificate": "-----BEGIN CERTIFICATE-----\n..."
}
```
**Example with Custom Field Mappings**:
```json
{
"id": "certificate-item-name",
"ssl_cert": "-----BEGIN CERTIFICATE-----\n...",
"ssl_key": "-----BEGIN PRIVATE KEY-----\n...",
"ssl_chain": "-----BEGIN CERTIFICATE-----\n...",
"ssl_ca": "-----BEGIN CERTIFICATE-----\n..."
}
```
</Tip>
6. Configure the **Details** of your Chef Certificate Sync, then click **Next**.
![Configure Details](/images/platform/pki/certificate-syncs/chef/chef-details.png)
- **Name**: The name of your sync. Must be slug-friendly.
- **Description**: An optional description for your sync.
7. Select which certificates should be synced to Chef.
![Select Certificates](/images/platform/pki/certificate-syncs/chef/chef-certificates.png)
8. Review your Chef Certificate Sync configuration, then click **Create Sync**.
![Confirm Configuration](/images/platform/pki/certificate-syncs/chef/chef-review.png)
9. If enabled, your Chef Certificate Sync will begin syncing your certificates to the destination endpoint.
![Sync Certificates](/images/platform/pki/certificate-syncs/chef/chef-synced.png)
</Tab>
<Tab title="API">
To create a **Chef Certificate Sync**, make an API request to the [Create Chef Certificate Sync](/api-reference/endpoints/pki/syncs/chef/create) API endpoint.
### Sample request
<Note>
You can optionally specify `certificateIds` during sync creation to immediately add certificates to the sync.
If not provided, you can add certificates later using the certificate management endpoints.
</Note>
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/pki/syncs/chef \
--header 'Authorization: Bearer <access-token>' \
--header 'Content-Type: application/json' \
--data '{
"name": "my-chef-cert-sync",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "an example certificate sync",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"destination": "chef",
"isAutoSyncEnabled": true,
"certificateIds": [
"550e8400-e29b-41d4-a716-446655440000",
"660f1234-e29b-41d4-a716-446655440001"
],
"syncOptions": {
"canRemoveCertificates": true,
"preserveSecretOnRenewal": true,
"canImportCertificates": false,
"includeRootCa": false,
"certificateNameSchema": "myapp-{{certificateId}}",
"fieldMappings": {
"certificate": "ssl_cert",
"privateKey": "ssl_key",
"certificateChain": "ssl_chain",
"caCertificate": "ssl_ca"
}
},
"destinationConfig": {
"dataBagName": "ssl_certificates"
}
}'
```
### Example with Default Field Mappings
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/pki/syncs/chef \
--header 'Authorization: Bearer <access-token>' \
--header 'Content-Type: application/json' \
--data '{
"name": "my-chef-cert-sync-default",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "Chef sync with default field mappings",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"destination": "chef",
"isAutoSyncEnabled": true,
"syncOptions": {
"canRemoveCertificates": true,
"preserveSecretOnRenewal": true,
"canImportCertificates": false,
"includeRootCa": false,
"certificateNameSchema": "{{commonName}}-{{certificateId}}",
"fieldMappings": {
"certificate": "certificate",
"privateKey": "private_key",
"certificateChain": "certificate_chain",
"caCertificate": "ca_certificate"
}
},
"destinationConfig": {
"dataBagName": "certificates"
}
}'
```
### Sample response
```json Response
{
"pkiSync": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-chef-cert-sync",
"description": "an example certificate sync",
"destination": "chef",
"isAutoSyncEnabled": true,
"destinationConfig": {
"dataBagName": "ssl_certificates"
},
"syncOptions": {
"canRemoveCertificates": true,
"preserveSecretOnRenewal": true,
"canImportCertificates": false,
"includeRootCa": false,
"certificateNameSchema": "myapp-{{certificateId}}",
"fieldMappings": {
"certificate": "ssl_cert",
"privateKey": "ssl_key",
"certificateChain": "ssl_chain",
"caCertificate": "ssl_ca"
}
},
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-01-01T00:00:00.000Z",
"updatedAt": "2023-01-01T00:00:00.000Z"
}
}
```
</Tab>
</Tabs>
## Certificate Management
Your Chef Certificate Sync will:
- **Automatic Deployment**: Deploy certificates in Infisical to Chef data bags with customizable field names
- **Certificate Updates**: Update certificates in Chef data bags when renewals occur
- **Expiration Handling**: Optionally remove expired certificates from Chef data bags (if enabled)
- **Format Preservation**: Maintain certificate format during sync operations
- **Field Customization**: Map certificate data to custom field names that match your Chef cookbook requirements
- **CA Certificate Support**: Include CA certificates in data bag items for complete certificate chain management
<Note>
Chef Certificate Syncs support both automatic and manual
synchronization modes. When auto-sync is enabled, certificates are
automatically deployed as they are issued or renewed.
</Note>
## Manual Certificate Sync
You can manually trigger certificate synchronization to Chef using the sync certificates functionality. This is useful for:
- Initial setup when you have existing certificates to deploy
- One-time sync of specific certificates
- Testing certificate sync configurations
- Force sync after making changes
To manually sync certificates, use the [Sync Certificates](/api-reference/endpoints/pki/syncs/chef/sync-certificates) API endpoint or the manual sync option in the Infisical UI.
<Note>
Chef does not support importing certificates back into Infisical
due to the nature of Chef data bags where certificates are stored as data
rather than managed certificate objects.
</Note>
@@ -83,6 +83,7 @@ should be synced. Follow these steps to start syncing:
- <strong>Certificates:</strong> The certificates you wish to push to the destination. - <strong>Certificates:</strong> The certificates you wish to push to the destination.
- <strong>Options:</strong> Customize how certificates should be synced, including: - <strong>Options:</strong> Customize how certificates should be synced, including:
- Whether certificates should be removed from the destination when they expire. - Whether certificates should be removed from the destination when they expire.
- Whether to include the Root CA certificate in the certificate chain.
- Certificate naming schema to control how certificate names are generated in - Certificate naming schema to control how certificate names are generated in
the destination. the destination.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

@@ -64,7 +64,7 @@ export const CreatePkiSyncModal = ({
"Add Sync" "Add Sync"
) )
} }
className="max-w-2xl" className="max-w-3xl"
bodyClassName="overflow-visible" bodyClassName="overflow-visible"
subTitle={ subTitle={
selectedSync ? undefined : "Select a third-party service to sync certificates to." selectedSync ? undefined : "Select a third-party service to sync certificates to."
@@ -15,11 +15,13 @@ type Props = {
export const EditPkiSyncModal = ({ pkiSync, onOpenChange, fields, ...props }: Props) => { export const EditPkiSyncModal = ({ pkiSync, onOpenChange, fields, ...props }: Props) => {
if (!pkiSync) return null; if (!pkiSync) return null;
const modalClassName = fields === PkiSyncEditFields.Mappings ? "max-w-4xl" : "max-w-2xl";
return ( return (
<Modal {...props} onOpenChange={onOpenChange}> <Modal {...props} onOpenChange={onOpenChange}>
<ModalContent <ModalContent
title={<PkiSyncModalHeader isConfigured destination={pkiSync.destination} />} title={<PkiSyncModalHeader isConfigured destination={pkiSync.destination} />}
className="max-w-2xl" className={modalClassName}
bodyClassName="overflow-visible" bodyClassName="overflow-visible"
> >
<EditPkiSyncForm onComplete={() => onOpenChange(false)} fields={fields} pkiSync={pkiSync} /> <EditPkiSyncForm onComplete={() => onOpenChange(false)} fields={fields} pkiSync={pkiSync} />
@@ -0,0 +1,50 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Select, SelectItem } from "@app/components/v2";
import { AWS_REGIONS } from "@app/helpers/appConnections";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
import { PkiSyncConnectionField } from "./PkiSyncConnectionField";
export const AwsSecretsManagerPkiSyncFields = () => {
const { control, setValue } = useFormContext<
TPkiSyncForm & { destination: PkiSync.AwsSecretsManager }
>();
return (
<>
<PkiSyncConnectionField
onChange={() => {
setValue("destinationConfig.region", "");
}}
/>
<Controller
name="destinationConfig.region"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="AWS Region"
tooltipText="Select the AWS region where your secrets will be stored in AWS Secrets Manager."
>
<Select
value={field.value}
onValueChange={field.onChange}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
placeholder="Select an AWS region"
>
{AWS_REGIONS.map(({ name, slug }) => (
<SelectItem value={slug} key={slug}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</>
);
};
@@ -0,0 +1,35 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Input } from "@app/components/v2";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
import { PkiSyncConnectionField } from "./PkiSyncConnectionField";
export const ChefPkiSyncFields = () => {
const { control, setValue } = useFormContext<TPkiSyncForm & { destination: PkiSync.Chef }>();
return (
<>
<PkiSyncConnectionField
onChange={() => {
setValue("destinationConfig.dataBagName", "");
}}
/>
<Controller
name="destinationConfig.dataBagName"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Data Bag Name"
tooltipText="Enter your Chef data bag name where certificates will be stored. This data bag will be used to store SSL/TLS certificates, private keys, and certificate chains. Data bag names must contain only alphanumeric characters, underscores, and hyphens."
>
<Input {...field} placeholder="ssl_certificates" maxLength={255} />
</FormControl>
)}
/>
</>
);
};
@@ -4,7 +4,6 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Switch } from "@app/components/v2"; import { Button, FormControl, Switch } from "@app/components/v2";
@@ -16,6 +15,7 @@ import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas/pki-sync-schema";
import { PkiSyncCertificatesFields } from "./PkiSyncCertificatesFields"; import { PkiSyncCertificatesFields } from "./PkiSyncCertificatesFields";
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields"; import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields"; import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
import { PkiSyncFieldMappingsFields } from "./PkiSyncFieldMappingsFields";
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields"; import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
import { PkiSyncReviewFields } from "./PkiSyncReviewFields"; import { PkiSyncReviewFields } from "./PkiSyncReviewFields";
@@ -26,13 +26,38 @@ type Props = {
initialData?: any; initialData?: any;
}; };
const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] = [ const getFormTabs = (
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] }, destination: PkiSync
{ name: "Sync Options", key: "options", fields: ["syncOptions"] }, ): { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] => {
{ name: "Details", key: "details", fields: ["name", "description"] }, const baseTabs = [
{ name: "Certificates", key: "certificates", fields: ["certificateIds"] }, {
{ name: "Review", key: "review", fields: [] } name: "Destination",
]; key: "destination",
fields: ["connection", "destinationConfig"] as (keyof TPkiSyncForm)[]
},
{ name: "Sync Options", key: "options", fields: ["syncOptions"] as (keyof TPkiSyncForm)[] }
];
if (destination === PkiSync.Chef || destination === PkiSync.AwsSecretsManager) {
baseTabs.push({
name: "Mappings",
key: "mappings",
fields: ["syncOptions"] as (keyof TPkiSyncForm)[]
});
}
baseTabs.push(
{ name: "Details", key: "details", fields: ["name", "description"] as (keyof TPkiSyncForm)[] },
{
name: "Certificates",
key: "certificates",
fields: ["certificateIds"] as (keyof TPkiSyncForm)[]
},
{ name: "Review", key: "review", fields: [] as (keyof TPkiSyncForm)[] }
);
return baseTabs;
};
export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialData }: Props) => { export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialData }: Props) => {
const createPkiSync = useCreatePkiSync(); const createPkiSync = useCreatePkiSync();
@@ -42,6 +67,7 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa
const [showConfirmation, setShowConfirmation] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false);
const [selectedTabIndex, setSelectedTabIndex] = useState(0); const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const FORM_TABS = getFormTabs(destination);
const { syncOption } = usePkiSyncOption(destination); const { syncOption } = usePkiSyncOption(destination);
@@ -55,7 +81,19 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa
canImportCertificates: false, canImportCertificates: false,
canRemoveCertificates: false, canRemoveCertificates: false,
preserveArn: true, preserveArn: true,
certificateNameSchema: syncOption?.defaultCertificateNameSchema certificateNameSchema: syncOption?.defaultCertificateNameSchema,
...((destination === PkiSync.Chef || destination === PkiSync.AwsSecretsManager) && {
fieldMappings: {
certificate: "certificate",
privateKey: "private_key",
certificateChain: "certificate_chain",
caCertificate: "ca_certificate"
}
}),
...(destination === PkiSync.AwsSecretsManager && {
preserveSecretOnRenewal: true,
updateExistingCertificates: true
})
}, },
...initialData ...initialData
} as Partial<TPkiSyncForm>, } as Partial<TPkiSyncForm>,
@@ -167,10 +205,10 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa
); );
return ( return (
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}> <form className="flex max-h-[70vh] flex-col overflow-hidden">
<FormProvider {...formMethods}> <FormProvider {...formMethods}>
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}> <Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600"> <Tab.List className="-pb-1 mb-6 w-full flex-shrink-0 border-b-2 border-mineshaft-600">
{FORM_TABS.map((tab, index) => ( {FORM_TABS.map((tab, index) => (
<Tab <Tab
onClick={async (e) => { onClick={async (e) => {
@@ -191,11 +229,11 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa
</Tab> </Tab>
))} ))}
</Tab.List> </Tab.List>
<Tab.Panels> <Tab.Panels className="flex-1 overflow-y-auto">
<Tab.Panel> <Tab.Panel className="max-h-full overflow-y-auto">
<PkiSyncDestinationFields /> <PkiSyncDestinationFields />
</Tab.Panel> </Tab.Panel>
<Tab.Panel> <Tab.Panel className="max-h-full overflow-y-auto">
<PkiSyncOptionsFields destination={destination} /> <PkiSyncOptionsFields destination={destination} />
<Controller <Controller
control={control} control={control}
@@ -225,20 +263,25 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa
}} }}
/> />
</Tab.Panel> </Tab.Panel>
<Tab.Panel> {(destination === PkiSync.Chef || destination === PkiSync.AwsSecretsManager) && (
<Tab.Panel className="max-h-full overflow-y-auto">
<PkiSyncFieldMappingsFields destination={destination} />
</Tab.Panel>
)}
<Tab.Panel className="max-h-full overflow-y-auto">
<PkiSyncDetailsFields /> <PkiSyncDetailsFields />
</Tab.Panel> </Tab.Panel>
<Tab.Panel> <Tab.Panel className="max-h-full overflow-y-auto">
<PkiSyncCertificatesFields /> <PkiSyncCertificatesFields />
</Tab.Panel> </Tab.Panel>
<Tab.Panel> <Tab.Panel className="max-h-full overflow-y-auto">
<PkiSyncReviewFields /> <PkiSyncReviewFields />
</Tab.Panel> </Tab.Panel>
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>
</FormProvider> </FormProvider>
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4"> <div className="mt-4 flex w-full flex-shrink-0 flex-row-reverse justify-between gap-4 border-t border-mineshaft-600 pt-4">
<Button onClick={handleNext} colorSchema="secondary"> <Button onClick={handleNext} colorSchema="secondary">
{isFinalStep ? "Create Sync" : "Next"} {isFinalStep ? "Create Sync" : "Next"}
</Button> </Button>
@@ -11,6 +11,7 @@ import { TPkiSync, useUpdatePkiSync } from "@app/hooks/api/pkiSyncs";
import { TUpdatePkiSyncForm, UpdatePkiSyncFormSchema } from "./schemas/pki-sync-schema"; import { TUpdatePkiSyncForm, UpdatePkiSyncFormSchema } from "./schemas/pki-sync-schema";
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields"; import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields"; import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
import { PkiSyncFieldMappingsFields } from "./PkiSyncFieldMappingsFields";
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields"; import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
import { PkiSyncSourceFields } from "./PkiSyncSourceFields"; import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
@@ -66,6 +67,9 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => {
case PkiSyncEditFields.Options: case PkiSyncEditFields.Options:
Component = <PkiSyncOptionsFields destination={pkiSync.destination} />; Component = <PkiSyncOptionsFields destination={pkiSync.destination} />;
break; break;
case PkiSyncEditFields.Mappings:
Component = <PkiSyncFieldMappingsFields destination={pkiSync.destination} />;
break;
case PkiSyncEditFields.Source: case PkiSyncEditFields.Source:
Component = <PkiSyncSourceFields />; Component = <PkiSyncSourceFields />;
break; break;
@@ -4,7 +4,9 @@ import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { TPkiSyncForm } from "./schemas/pki-sync-schema"; import { TPkiSyncForm } from "./schemas/pki-sync-schema";
import { AwsCertificateManagerPkiSyncFields } from "./AwsCertificateManagerPkiSyncFields"; import { AwsCertificateManagerPkiSyncFields } from "./AwsCertificateManagerPkiSyncFields";
import { AwsSecretsManagerPkiSyncFields } from "./AwsSecretsManagerPkiSyncFields";
import { AzureKeyVaultPkiSyncFields } from "./AzureKeyVaultPkiSyncFields"; import { AzureKeyVaultPkiSyncFields } from "./AzureKeyVaultPkiSyncFields";
import { ChefPkiSyncFields } from "./ChefPkiSyncFields";
export const PkiSyncDestinationFields = () => { export const PkiSyncDestinationFields = () => {
const { watch } = useFormContext<TPkiSyncForm>(); const { watch } = useFormContext<TPkiSyncForm>();
@@ -16,6 +18,10 @@ export const PkiSyncDestinationFields = () => {
return <AzureKeyVaultPkiSyncFields />; return <AzureKeyVaultPkiSyncFields />;
case PkiSync.AwsCertificateManager: case PkiSync.AwsCertificateManager:
return <AwsCertificateManagerPkiSyncFields />; return <AwsCertificateManagerPkiSyncFields />;
case PkiSync.AwsSecretsManager:
return <AwsSecretsManagerPkiSyncFields />;
case PkiSync.Chef:
return <ChefPkiSyncFields />;
default: default:
return ( return (
<div className="flex items-center justify-center rounded-md border border-red-500 bg-red-100 p-4 text-red-700"> <div className="flex items-center justify-center rounded-md border border-red-500 bg-red-100 p-4 text-red-700">
@@ -0,0 +1,103 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Input } from "@app/components/v2";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
type Props = {
destination?: PkiSync;
};
export const PkiSyncFieldMappingsFields = ({ destination }: Props) => {
const { control, watch } = useFormContext<TPkiSyncForm>();
const currentDestination = destination || watch("destination");
if (currentDestination !== PkiSync.Chef && currentDestination !== PkiSync.AwsSecretsManager) {
return null;
}
return (
<>
<p className="mb-4 text-sm text-bunker-300">
Configure how certificate fields are mapped to your{" "}
{currentDestination === PkiSync.Chef ? "Chef data bag items" : "AWS secrets"}.
</p>
<div className="grid grid-cols-2 gap-4">
<Controller
control={control}
name="syncOptions.fieldMappings.certificate"
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Certificate Field"
tooltipText={`The field name used to store the certificate content in the ${currentDestination === PkiSync.Chef ? "Chef data bag item" : "AWS secret"}.`}
>
<Input {...field} placeholder="certificate" />
</FormControl>
)}
/>
<Controller
control={control}
name="syncOptions.fieldMappings.privateKey"
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Private Key Field"
tooltipText={`The field name used to store the private key content in the ${currentDestination === PkiSync.Chef ? "Chef data bag item" : "AWS secret"}.`}
>
<Input {...field} placeholder="private_key" />
</FormControl>
)}
/>
<Controller
control={control}
name="syncOptions.fieldMappings.certificateChain"
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Certificate Chain Field"
tooltipText={`The field name used to store the certificate chain content in the ${currentDestination === PkiSync.Chef ? "Chef data bag item" : "AWS secret"}.`}
>
<Input {...field} placeholder="certificate_chain" />
</FormControl>
)}
/>
<Controller
control={control}
name="syncOptions.fieldMappings.caCertificate"
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="CA Certificate Field"
tooltipText={`The field name used to store the CA certificate content in the ${currentDestination === PkiSync.Chef ? "Chef data bag item" : "AWS secret"}.`}
>
<Input {...field} placeholder="ca_certificate" />
</FormControl>
)}
/>
</div>
<div className="mt-6 rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
<h4 className="mb-2 text-sm font-medium text-mineshaft-100">Preview JSON Structure</h4>
<pre className="text-xs text-bunker-300">
{`{
"id": "certificate-item-name",
"${watch("syncOptions.fieldMappings.certificate") || "certificate"}": "<certificate-content>",
"${watch("syncOptions.fieldMappings.privateKey") || "private_key"}": "<private-key-content>",
"${watch("syncOptions.fieldMappings.certificateChain") || "certificate_chain"}": "<certificate-chain-content>",
"${watch("syncOptions.fieldMappings.caCertificate") || "ca_certificate"}": "<ca-certificate-content>"
}`}
</pre>
</div>
</>
);
};
@@ -95,6 +95,48 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
)} )}
/> />
<Controller
control={control}
name="syncOptions.includeRootCa"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="include-root-ca"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p>
Include Root CA in Certificate Chain{" "}
<Tooltip
className="max-w-md"
content={
<>
<p>
When enabled, the full certificate chain including the root CA will be
synced to the destination.
</p>
<p className="mt-4">
When disabled, the root CA will be excluded from the certificate chain
during sync operations, reducing the size of the synced certificate chain.
</p>
<p className="mt-4">
Most applications and services work correctly with intermediate certificates
only, as they can validate the trust chain up to a root CA they already
trust.
</p>
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
{currentDestination === PkiSync.AwsCertificateManager && ( {currentDestination === PkiSync.AwsCertificateManager && (
<Controller <Controller
control={control} control={control}
@@ -183,6 +225,97 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
/> />
)} )}
{currentDestination === PkiSync.AwsSecretsManager && (
<Controller
control={control}
name="syncOptions.preserveSecretOnRenewal"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="preserve-secret-on-renewal"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p>
Preserve Secret on Renewal{" "}
<Tooltip
className="max-w-md"
content={
<>
<p>
<strong>Only applies to certificate renewals:</strong> When a certificate
is renewed in Infisical, this option controls how the renewed certificate
is handled in AWS Secrets Manager.
</p>
<p className="mt-4">
When enabled, the renewed certificate will update the existing secret,
preserving the same secret name and ARN. This allows consuming services to
continue using the same secret reference without requiring updates.
</p>
<p className="mt-4">
When disabled, the renewed certificate will be created as a new secret
with a new name, and the old secret will be removed.
</p>
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
)}
{currentDestination === PkiSync.Chef && (
<Controller
control={control}
name="syncOptions.preserveItemOnRenewal"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="preserve-item-on-renewal"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p>
Preserve Data Bag Item on Renewal{" "}
<Tooltip
className="max-w-md"
content={
<>
<p>
<strong>Only applies to certificate renewals:</strong> When a certificate
is renewed in Infisical, this option controls how the renewed certificate
is handled in Chef.
</p>
<p className="mt-4">
When enabled, the renewed certificate will update the existing data bag
item, preserving the same item name. This allows consuming services to
continue using the same data bag item without requiring updates to Chef
cookbooks or recipes.
</p>
<p className="mt-4">
When disabled, the renewed certificate will be created as a new data bag
item with a new name, and the old item will be removed.
</p>
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
)}
<Controller <Controller
control={control} control={control}
name="syncOptions.certificateNameSchema" name="syncOptions.certificateNameSchema"
@@ -7,6 +7,7 @@ import { BasePkiSyncSchema } from "./base-pki-sync-schema";
const AwsCertificateManagerSyncOptionsSchema = z.object({ const AwsCertificateManagerSyncOptionsSchema = z.object({
canImportCertificates: z.boolean().default(false), canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(false), canRemoveCertificates: z.boolean().default(false),
includeRootCa: z.boolean().default(false),
preserveArn: z.boolean().default(true), preserveArn: z.boolean().default(true),
certificateNameSchema: z certificateNameSchema: z
.string() .string()
@@ -0,0 +1,98 @@
import { z } from "zod";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
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 AwsSecretsManagerSyncOptionsSchema = 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(
(val) => {
if (!val) return true;
const allowedOptionalPlaceholders = [
"{{environment}}",
"{{profileId}}",
"{{commonName}}",
"{{friendlyName}}"
];
const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders]
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
.join("|");
const allowedContentRegex = new RegExp(
`^([a-zA-Z0-9_\\-]|${allowedPlaceholdersRegexPart})*$`
);
const contentIsValid = allowedContentRegex.test(val);
if (val.trim()) {
const certificateIdRegex = /\{\{certificateId\}\}/;
const certificateIdIsPresent = certificateIdRegex.test(val);
return contentIsValid && certificateIdIsPresent;
}
return contentIsValid;
},
{
message:
"Certificate name schema must include exactly one {{certificateId}} placeholder. It can also include {{environment}}, {{profileId}}, {{commonName}}, or {{friendlyName}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), hyphens (-), and underscores (_) are allowed besides the placeholders."
}
),
fieldMappings: AwsSecretsManagerFieldMappingsSchema.optional().default({
certificate: "certificate",
privateKey: "private_key",
certificateChain: "certificate_chain",
caCertificate: "ca_certificate"
})
});
export const AwsSecretsManagerPkiSyncDestinationSchema = BasePkiSyncSchema(
AwsSecretsManagerSyncOptionsSchema
).merge(
z.object({
destination: z.literal(PkiSync.AwsSecretsManager),
destinationConfig: z.object({
region: z.string().min(1, "AWS region is required")
})
})
);
export const UpdateAwsSecretsManagerPkiSyncDestinationSchema =
AwsSecretsManagerPkiSyncDestinationSchema.partial().merge(
z.object({
name: z
.string()
.trim()
.min(1, "Name is required")
.max(255, "Name must be less than 255 characters"),
destination: z.literal(PkiSync.AwsSecretsManager),
connection: z.object({
id: z.string().uuid("Invalid connection ID format"),
name: z
.string()
.min(1, "Connection name is required")
.max(255, "Connection name must be less than 255 characters")
})
})
);
@@ -7,6 +7,7 @@ import { BasePkiSyncSchema } from "./base-pki-sync-schema";
const AzureKeyVaultSyncOptionsSchema = z.object({ const AzureKeyVaultSyncOptionsSchema = z.object({
canImportCertificates: z.boolean().default(false), canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(true), canRemoveCertificates: z.boolean().default(true),
includeRootCa: z.boolean().default(false),
enableVersioning: z.boolean().default(true), enableVersioning: z.boolean().default(true),
certificateNameSchema: z certificateNameSchema: z
.string() .string()
@@ -6,6 +6,7 @@ export const BasePkiSyncSchema = <T extends AnyZodObject | undefined = undefined
const baseSyncOptionsSchema = z.object({ const baseSyncOptionsSchema = z.object({
canImportCertificates: z.boolean().default(false), canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(false), canRemoveCertificates: z.boolean().default(false),
includeRootCa: z.boolean().default(false),
certificateNameSchema: z certificateNameSchema: z
.string() .string()
.optional() .optional()
@@ -0,0 +1,102 @@
import { z } from "zod";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
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 ChefSyncOptionsSchema = 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(
(val) => {
if (!val) return true;
const allowedOptionalPlaceholders = [
"{{environment}}",
"{{profileId}}",
"{{commonName}}",
"{{friendlyName}}"
];
const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders]
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
.join("|");
const allowedContentRegex = new RegExp(
`^([a-zA-Z0-9_\\-]|${allowedPlaceholdersRegexPart})*$`
);
const contentIsValid = allowedContentRegex.test(val);
if (val.trim()) {
const certificateIdRegex = /\{\{certificateId\}\}/;
const certificateIdIsPresent = certificateIdRegex.test(val);
return contentIsValid && certificateIdIsPresent;
}
return contentIsValid;
},
{
message:
"Certificate item name schema must include exactly one {{certificateId}} placeholder. It can also include {{environment}}, {{profileId}}, {{commonName}}, or {{friendlyName}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), hyphens (-), and underscores (_) are allowed besides the placeholders."
}
),
fieldMappings: ChefFieldMappingsSchema.optional().default({
certificate: "certificate",
privateKey: "private_key",
certificateChain: "certificate_chain",
caCertificate: "ca_certificate"
})
});
export const ChefPkiSyncDestinationSchema = BasePkiSyncSchema(ChefSyncOptionsSchema).merge(
z.object({
destination: z.literal(PkiSync.Chef),
destinationConfig: z.object({
dataBagName: z
.string()
.min(1, "Data bag name is required")
.max(255, "Data bag name must be less than 255 characters")
.regex(
/^[a-zA-Z0-9_-]+$/,
"Data bag name can only contain alphanumeric characters, underscores, and hyphens"
)
})
})
);
export const UpdateChefPkiSyncDestinationSchema = ChefPkiSyncDestinationSchema.partial().merge(
z.object({
name: z
.string()
.trim()
.min(1, "Name is required")
.max(255, "Name must be less than 255 characters"),
destination: z.literal(PkiSync.Chef),
connection: z.object({
id: z.string().uuid("Invalid connection ID format"),
name: z
.string()
.min(1, "Connection name is required")
.max(255, "Connection name must be less than 255 characters")
})
})
);
@@ -4,19 +4,31 @@ import {
AwsCertificateManagerPkiSyncDestinationSchema, AwsCertificateManagerPkiSyncDestinationSchema,
UpdateAwsCertificateManagerPkiSyncDestinationSchema UpdateAwsCertificateManagerPkiSyncDestinationSchema
} from "./aws-certificate-manager-pki-sync-destination-schema"; } from "./aws-certificate-manager-pki-sync-destination-schema";
import {
AwsSecretsManagerPkiSyncDestinationSchema,
UpdateAwsSecretsManagerPkiSyncDestinationSchema
} from "./aws-secrets-manager-pki-sync-destination-schema";
import { import {
AzureKeyVaultPkiSyncDestinationSchema, AzureKeyVaultPkiSyncDestinationSchema,
UpdateAzureKeyVaultPkiSyncDestinationSchema UpdateAzureKeyVaultPkiSyncDestinationSchema
} from "./azure-key-vault-pki-sync-destination-schema"; } from "./azure-key-vault-pki-sync-destination-schema";
import {
ChefPkiSyncDestinationSchema,
UpdateChefPkiSyncDestinationSchema
} from "./chef-pki-sync-destination-schema";
const PkiSyncUnionSchema = z.discriminatedUnion("destination", [ const PkiSyncUnionSchema = z.discriminatedUnion("destination", [
AzureKeyVaultPkiSyncDestinationSchema, AzureKeyVaultPkiSyncDestinationSchema,
AwsCertificateManagerPkiSyncDestinationSchema AwsCertificateManagerPkiSyncDestinationSchema,
AwsSecretsManagerPkiSyncDestinationSchema,
ChefPkiSyncDestinationSchema
]); ]);
const UpdatePkiSyncUnionSchema = z.discriminatedUnion("destination", [ const UpdatePkiSyncUnionSchema = z.discriminatedUnion("destination", [
UpdateAzureKeyVaultPkiSyncDestinationSchema, UpdateAzureKeyVaultPkiSyncDestinationSchema,
UpdateAwsCertificateManagerPkiSyncDestinationSchema UpdateAwsCertificateManagerPkiSyncDestinationSchema,
UpdateAwsSecretsManagerPkiSyncDestinationSchema,
UpdateChefPkiSyncDestinationSchema
]); ]);
export const PkiSyncFormSchema = PkiSyncUnionSchema; export const PkiSyncFormSchema = PkiSyncUnionSchema;
@@ -1,6 +1,7 @@
export enum PkiSyncEditFields { export enum PkiSyncEditFields {
Details = "details", Details = "details",
Options = "options", Options = "options",
Mappings = "mappings",
Source = "source", Source = "source",
Destination = "destination" Destination = "destination"
} }
+11 -1
View File
@@ -15,10 +15,20 @@ export const PKI_SYNC_MAP: Record<
[PkiSync.AwsCertificateManager]: { [PkiSync.AwsCertificateManager]: {
name: "AWS Certificate Manager", name: "AWS Certificate Manager",
image: "Amazon Web Services.png" image: "Amazon Web Services.png"
},
[PkiSync.AwsSecretsManager]: {
name: "AWS Secrets Manager",
image: "Amazon Web Services.png"
},
[PkiSync.Chef]: {
name: "Chef",
image: "Chef.png"
} }
}; };
export const PKI_SYNC_CONNECTION_MAP: Record<PkiSync, AppConnection> = { export const PKI_SYNC_CONNECTION_MAP: Record<PkiSync, AppConnection> = {
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault, [PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault,
[PkiSync.AwsCertificateManager]: AppConnection.AWS [PkiSync.AwsCertificateManager]: AppConnection.AWS,
[PkiSync.AwsSecretsManager]: AppConnection.AWS,
[PkiSync.Chef]: AppConnection.Chef
}; };
+4 -2
View File
@@ -1,6 +1,8 @@
export enum PkiSync { export enum PkiSync {
AzureKeyVault = "azure-key-vault", AzureKeyVault = "azure-key-vault",
AwsCertificateManager = "aws-certificate-manager" AwsCertificateManager = "aws-certificate-manager",
AwsSecretsManager = "aws-secrets-manager",
Chef = "chef"
} }
export enum PkiSyncStatus { export enum PkiSyncStatus {
@@ -12,7 +14,7 @@ export enum PkiSyncStatus {
export enum CertificateSyncStatus { export enum CertificateSyncStatus {
Pending = "pending", Pending = "pending",
Syncing = "syncing", Running = "running",
Succeeded = "succeeded", Succeeded = "succeeded",
Failed = "failed" Failed = "failed"
} }
@@ -0,0 +1,29 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { PkiSync } from "../enums";
import { TRootPkiSync } from "./common";
export type TAwsSecretsManagerFieldMappings = {
certificate: string;
privateKey: string;
certificateChain: string;
caCertificate: string;
};
export type TAwsSecretsManagerPkiSync = TRootPkiSync & {
destination: PkiSync.AwsSecretsManager;
destinationConfig: {
region: string;
keyId?: string;
};
connection: {
app: AppConnection.AWS;
name: string;
id: string;
};
syncOptions: TRootPkiSync["syncOptions"] & {
fieldMappings?: TAwsSecretsManagerFieldMappings;
preserveSecretOnRenewal?: boolean;
updateExistingCertificates?: boolean;
};
};
@@ -0,0 +1,16 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { PkiSync } from "../enums";
import { TRootPkiSync } from "./common";
export type TChefPkiSync = TRootPkiSync & {
destination: PkiSync.Chef;
destinationConfig: {
dataBagName: string;
};
connection: {
app: AppConnection.Chef;
name: string;
id: string;
};
};
@@ -2,11 +2,23 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { CertificateSyncStatus, PkiSyncStatus } from "../enums"; import { CertificateSyncStatus, PkiSyncStatus } from "../enums";
export type TChefFieldMappings = {
certificate: string;
privateKey: string;
certificateChain: string;
caCertificate: string;
};
export type RootPkiSyncOptions = { export type RootPkiSyncOptions = {
canImportCertificates: boolean; canImportCertificates: boolean;
canRemoveCertificates: boolean; canRemoveCertificates: boolean;
certificateNamePrefix?: string; certificateNamePrefix?: string;
certificateNameSchema?: string; certificateNameSchema?: string;
preserveArn?: boolean;
enableVersioning?: boolean;
preserveItemOnRenewal?: boolean;
updateExistingCertificates?: boolean;
fieldMappings?: TChefFieldMappings;
}; };
export type TRootPkiSync = { export type TRootPkiSync = {
+21 -1
View File
@@ -1,7 +1,9 @@
import { PkiSync } from "@app/hooks/api/pkiSyncs"; import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { TAwsCertificateManagerPkiSync } from "./aws-certificate-manager-sync"; import { TAwsCertificateManagerPkiSync } from "./aws-certificate-manager-sync";
import { TAwsSecretsManagerPkiSync } from "./aws-secrets-manager-sync";
import { TAzureKeyVaultPkiSync } from "./azure-key-vault-sync"; import { TAzureKeyVaultPkiSync } from "./azure-key-vault-sync";
import { TChefPkiSync } from "./chef-sync";
export type TPkiSyncOption = { export type TPkiSyncOption = {
name: string; name: string;
@@ -16,7 +18,11 @@ export type TPkiSyncOption = {
minCertificateNameLength?: number; minCertificateNameLength?: number;
}; };
export type TPkiSync = TAzureKeyVaultPkiSync | TAwsCertificateManagerPkiSync; export type TPkiSync =
| TAzureKeyVaultPkiSync
| TAwsCertificateManagerPkiSync
| TAwsSecretsManagerPkiSync
| TChefPkiSync;
export type TListPkiSyncs = { pkiSyncs: TPkiSync[] }; export type TListPkiSyncs = { pkiSyncs: TPkiSync[] };
@@ -31,6 +37,17 @@ type TCreatePkiSyncDTOBase = {
canRemoveCertificates: boolean; canRemoveCertificates: boolean;
certificateNamePrefix?: string; certificateNamePrefix?: string;
certificateNameSchema?: string; certificateNameSchema?: string;
preserveArn?: boolean;
enableVersioning?: boolean;
preserveItemOnRenewal?: boolean;
updateExistingCertificates?: boolean;
preserveSecretOnRenewal?: boolean;
fieldMappings?: {
certificate: string;
privateKey: string;
certificateChain: string;
caCertificate: string;
};
}; };
isAutoSyncEnabled: boolean; isAutoSyncEnabled: boolean;
subscriberId?: string | null; subscriberId?: string | null;
@@ -43,6 +60,7 @@ export type TCreatePkiSyncDTO = TCreatePkiSyncDTOBase & {
destinationConfig: { destinationConfig: {
vaultBaseUrl?: string; vaultBaseUrl?: string;
region?: string; region?: string;
dataBagName?: string;
}; };
}; };
@@ -76,5 +94,7 @@ export type TTriggerPkiSyncRemoveCertificatesDTO = {
}; };
export * from "./aws-certificate-manager-sync"; export * from "./aws-certificate-manager-sync";
export * from "./aws-secrets-manager-sync";
export * from "./azure-key-vault-sync"; export * from "./azure-key-vault-sync";
export * from "./chef-sync";
export * from "./common"; export * from "./common";
@@ -354,6 +354,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
<div> <div>
<h4 className="text-sm font-medium text-mineshaft-300">Certificate Details</h4> <h4 className="text-sm font-medium text-mineshaft-300">Certificate Details</h4>
<p className="text-sm text-mineshaft-400">Serial Number: {cert.serialNumber}</p> <p className="text-sm text-mineshaft-400">Serial Number: {cert.serialNumber}</p>
<p className="text-sm text-mineshaft-400">Certificate Id: {cert.id}</p>
<p className="text-sm text-mineshaft-400">Common Name: {cert.commonName}</p> <p className="text-sm text-mineshaft-400">Common Name: {cert.commonName}</p>
<p className="text-sm text-mineshaft-400">Status: {cert.status}</p> <p className="text-sm text-mineshaft-400">Status: {cert.status}</p>
</div> </div>
@@ -21,6 +21,7 @@ import {
PkiSyncCertificatesSection, PkiSyncCertificatesSection,
PkiSyncDestinationSection, PkiSyncDestinationSection,
PkiSyncDetailsSection, PkiSyncDetailsSection,
PkiSyncFieldMappingsSection,
PkiSyncOptionsSection PkiSyncOptionsSection
} from "./components"; } from "./components";
@@ -63,6 +64,7 @@ const PageContent = () => {
const handleEditDetails = () => handlePopUpOpen("editSync", PkiSyncEditFields.Details); const handleEditDetails = () => handlePopUpOpen("editSync", PkiSyncEditFields.Details);
const handleEditOptions = () => handlePopUpOpen("editSync", PkiSyncEditFields.Options); const handleEditOptions = () => handlePopUpOpen("editSync", PkiSyncEditFields.Options);
const handleEditMappings = () => handlePopUpOpen("editSync", PkiSyncEditFields.Mappings);
const handleEditDestination = () => handlePopUpOpen("editSync", PkiSyncEditFields.Destination); const handleEditDestination = () => handlePopUpOpen("editSync", PkiSyncEditFields.Destination);
return ( return (
@@ -103,6 +105,7 @@ const PageContent = () => {
<div className="mr-4 flex w-72 flex-col gap-4"> <div className="mr-4 flex w-72 flex-col gap-4">
<PkiSyncDetailsSection pkiSync={pkiSync} onEditDetails={handleEditDetails} /> <PkiSyncDetailsSection pkiSync={pkiSync} onEditDetails={handleEditDetails} />
<PkiSyncOptionsSection pkiSync={pkiSync} onEditOptions={handleEditOptions} /> <PkiSyncOptionsSection pkiSync={pkiSync} onEditOptions={handleEditOptions} />
<PkiSyncFieldMappingsSection pkiSync={pkiSync} onEditMappings={handleEditMappings} />
</div> </div>
<div className="flex flex-1 flex-col gap-4"> <div className="flex flex-1 flex-col gap-4">
<PkiSyncDestinationSection <PkiSyncDestinationSection
@@ -42,14 +42,14 @@ type Props = {
const getSyncStatusVariant = (status?: CertificateSyncStatus | null) => { const getSyncStatusVariant = (status?: CertificateSyncStatus | null) => {
if (status === CertificateSyncStatus.Succeeded) return "success"; if (status === CertificateSyncStatus.Succeeded) return "success";
if (status === CertificateSyncStatus.Failed) return "danger"; if (status === CertificateSyncStatus.Failed) return "danger";
if (status === CertificateSyncStatus.Syncing) return "neutral"; if (status === CertificateSyncStatus.Running) return "neutral";
return "project"; return "project";
}; };
const getSyncStatusText = (status?: CertificateSyncStatus | null) => { const getSyncStatusText = (status?: CertificateSyncStatus | null) => {
if (status === CertificateSyncStatus.Succeeded) return "Synced"; if (status === CertificateSyncStatus.Succeeded) return "Synced";
if (status === CertificateSyncStatus.Failed) return "Failed"; if (status === CertificateSyncStatus.Failed) return "Failed";
if (status === CertificateSyncStatus.Syncing) return "Syncing"; if (status === CertificateSyncStatus.Running) return "Syncing";
if (status === CertificateSyncStatus.Pending) return "Pending"; if (status === CertificateSyncStatus.Pending) return "Pending";
return "Unknown"; return "Unknown";
}; };
@@ -13,7 +13,9 @@ import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
import { import {
AwsCertificateManagerPkiSyncDestinationSection, AwsCertificateManagerPkiSyncDestinationSection,
AzureKeyVaultPkiSyncDestinationSection AwsSecretsManagerPkiSyncDestinationSection,
AzureKeyVaultPkiSyncDestinationSection,
ChefPkiSyncDestinationSection
} from "./PkiSyncDestinationSection/index"; } from "./PkiSyncDestinationSection/index";
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => ( const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
@@ -38,9 +40,15 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
case PkiSync.AwsCertificateManager: case PkiSync.AwsCertificateManager:
DestinationComponents = <AwsCertificateManagerPkiSyncDestinationSection pkiSync={pkiSync} />; DestinationComponents = <AwsCertificateManagerPkiSyncDestinationSection pkiSync={pkiSync} />;
break; break;
case PkiSync.AwsSecretsManager:
DestinationComponents = <AwsSecretsManagerPkiSyncDestinationSection pkiSync={pkiSync} />;
break;
case PkiSync.AzureKeyVault: case PkiSync.AzureKeyVault:
DestinationComponents = <AzureKeyVaultPkiSyncDestinationSection pkiSync={pkiSync} />; DestinationComponents = <AzureKeyVaultPkiSyncDestinationSection pkiSync={pkiSync} />;
break; break;
case PkiSync.Chef:
DestinationComponents = <ChefPkiSyncDestinationSection pkiSync={pkiSync} />;
break;
default: default:
// For future destinations, return null (no additional fields to show) // For future destinations, return null (no additional fields to show)
DestinationComponents = null; DestinationComponents = null;
@@ -0,0 +1,28 @@
import { TAwsSecretsManagerPkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
<div className="text-sm text-mineshaft-300">{children}</div>
</div>
);
type Props = {
pkiSync: TPkiSync;
};
export const AwsSecretsManagerPkiSyncDestinationSection = ({ pkiSync }: Props) => {
const awsSecretsManagerPkiSync = pkiSync as TAwsSecretsManagerPkiSync;
const { destinationConfig } = awsSecretsManagerPkiSync;
return (
<>
<GenericFieldLabel label="AWS Region">
{destinationConfig.region || "us-east-1"}
</GenericFieldLabel>
{destinationConfig.keyId && (
<GenericFieldLabel label="KMS Key">{destinationConfig.keyId}</GenericFieldLabel>
)}
</>
);
};
@@ -0,0 +1,25 @@
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
<div className="text-sm text-mineshaft-300">{children}</div>
</div>
);
type Props = {
pkiSync: TPkiSync;
};
export const ChefPkiSyncDestinationSection = ({ pkiSync }: Props) => {
const dataBagName =
pkiSync.destinationConfig && "dataBagName" in pkiSync.destinationConfig
? pkiSync.destinationConfig.dataBagName
: undefined;
return (
<GenericFieldLabel label="Chef Data Bag Name">
{dataBagName || "Not specified"}
</GenericFieldLabel>
);
};
@@ -1,2 +1,4 @@
export { AwsCertificateManagerPkiSyncDestinationSection } from "./AwsCertificateManagerPkiSyncDestinationSection"; export { AwsCertificateManagerPkiSyncDestinationSection } from "./AwsCertificateManagerPkiSyncDestinationSection";
export { AwsSecretsManagerPkiSyncDestinationSection } from "./AwsSecretsManagerPkiSyncDestinationSection";
export { AzureKeyVaultPkiSyncDestinationSection } from "./AzureKeyVaultPkiSyncDestinationSection"; export { AzureKeyVaultPkiSyncDestinationSection } from "./AzureKeyVaultPkiSyncDestinationSection";
export { ChefPkiSyncDestinationSection } from "./ChefPkiSyncDestinationSection";
@@ -0,0 +1,92 @@
import { subject } from "@casl/ability";
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { IconButton } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({
label,
children,
labelClassName
}: {
label: string;
children: React.ReactNode;
labelClassName?: string;
}) => (
<div className="mb-3">
<p className={`mb-1 text-sm font-medium text-mineshaft-300 ${labelClassName || ""}`}>{label}</p>
<div className="text-sm text-mineshaft-400">{children}</div>
</div>
);
type Props = {
pkiSync: TPkiSync;
onEditMappings: VoidFunction;
};
export const PkiSyncFieldMappingsSection = ({ pkiSync, onEditMappings }: Props) => {
if (pkiSync.destination !== PkiSync.Chef && pkiSync.destination !== PkiSync.AwsSecretsManager) {
return null;
}
const fieldMappings = pkiSync.syncOptions?.fieldMappings;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: pkiSync.subscriberId || ""
});
return (
<div>
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="text-lg font-medium text-mineshaft-100">Field Mappings</h3>
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
isDisabled={!isAllowed}
ariaLabel="Edit field mappings"
onClick={onEditMappings}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
<div className="pt-1">
<div className="space-y-3">
<GenericFieldLabel label="Certificate Field">
<Badge variant="neutral" className="max-w-full truncate">
{fieldMappings?.certificate || "certificate"}
</Badge>
</GenericFieldLabel>
<GenericFieldLabel label="Private Key Field">
<Badge variant="neutral" className="max-w-full truncate">
{fieldMappings?.privateKey || "private_key"}
</Badge>
</GenericFieldLabel>
<GenericFieldLabel label="Certificate Chain Field">
<Badge variant="neutral" className="max-w-full truncate">
{fieldMappings?.certificateChain || "certificate_chain"}
</Badge>
</GenericFieldLabel>
<GenericFieldLabel label="CA Certificate Field">
<Badge variant="neutral" className="max-w-full truncate">
{fieldMappings?.caCertificate || "ca_certificate"}
</Badge>
</GenericFieldLabel>
</div>
</div>
</div>
</div>
);
};
@@ -3,5 +3,6 @@ export { PkiSyncAuditLogsSection } from "./PkiSyncAuditLogsSection";
export { PkiSyncCertificatesSection } from "./PkiSyncCertificatesSection"; export { PkiSyncCertificatesSection } from "./PkiSyncCertificatesSection";
export { PkiSyncDestinationSection } from "./PkiSyncDestinationSection"; export { PkiSyncDestinationSection } from "./PkiSyncDestinationSection";
export { PkiSyncDetailsSection } from "./PkiSyncDetailsSection"; export { PkiSyncDetailsSection } from "./PkiSyncDetailsSection";
export { PkiSyncFieldMappingsSection } from "./PkiSyncFieldMappingsSection";
export { PkiSyncOptionsSection } from "./PkiSyncOptionsSection"; export { PkiSyncOptionsSection } from "./PkiSyncOptionsSection";
export { PkiSyncSourceSection } from "./PkiSyncSourceSection"; export { PkiSyncSourceSection } from "./PkiSyncSourceSection";