mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 10:23:57 +00:00
PKI Syncs revamp
This commit is contained in:
+125
-41
@@ -4,6 +4,7 @@ import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
@@ -101,7 +102,8 @@ const findInfisicalCertificateTag = (tags: AWS.ACM.TagList | undefined): AWS.ACM
|
||||
|
||||
const validateCertificateIdentification = (
|
||||
certName: string,
|
||||
existingCert: { arn?: string; Tags?: AWS.ACM.TagList; cert?: string; privateKey?: string; certificateChain?: string }
|
||||
existingCert: { arn?: string; Tags?: AWS.ACM.TagList; cert?: string; privateKey?: string; certificateChain?: string },
|
||||
alternativeCertNames?: string[]
|
||||
): boolean => {
|
||||
if (!existingCert?.arn || !existingCert?.Tags) {
|
||||
return false;
|
||||
@@ -113,12 +115,15 @@ const validateCertificateIdentification = (
|
||||
return false;
|
||||
}
|
||||
|
||||
return certNameTag.Value === certName;
|
||||
};
|
||||
if (certNameTag.Value === certName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
type TAwsCertificateManagerPkiSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
if (alternativeCertNames && alternativeCertNames.includes(certNameTag.Value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const validateCertificateNameSchema = (schema: string): void => {
|
||||
@@ -174,6 +179,11 @@ const generateCertificateName = (certificateName: string, pkiSync: TPkiSyncWithC
|
||||
return sanitizedCertificateName;
|
||||
};
|
||||
|
||||
type TAwsCertificateManagerPkiSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
const getAwsAcmClient = async (
|
||||
connectionId: string,
|
||||
region: AWSRegion,
|
||||
@@ -392,48 +402,59 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
kmsService
|
||||
);
|
||||
|
||||
const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id);
|
||||
const {
|
||||
acmCertificates
|
||||
}: {
|
||||
acmCertificates: Record<
|
||||
string,
|
||||
{ cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList }
|
||||
>;
|
||||
} = await $getAwsAcmCertificates(acm, pkiSync.id);
|
||||
|
||||
const setCertificates: CertificateImportRequest[] = [];
|
||||
const validationErrors: Array<{ name: string; error: string }> = [];
|
||||
|
||||
const activeCertificateNames = Object.keys(certificateMap);
|
||||
const syncOptions = pkiSync.syncOptions as { preserveArn?: boolean } | undefined;
|
||||
const preserveArn = syncOptions?.preserveArn ?? true;
|
||||
|
||||
Object.entries(certificateMap).forEach(([certName, certData]) => {
|
||||
const { cert, privateKey, certificateChain } = certData;
|
||||
const certificateName = generateCertificateName(certName, pkiSync);
|
||||
|
||||
const existingCert = Object.values(acmCertificates).find((acmCert) =>
|
||||
validateCertificateIdentification(certName, acmCert)
|
||||
);
|
||||
|
||||
const shouldUpdateCert = !existingCert || existingCert.cert !== cert;
|
||||
const { cert, privateKey, certificateChain, alternativeNames } = certData;
|
||||
|
||||
try {
|
||||
validateCertificateContent(cert, privateKey);
|
||||
} catch (validationError) {
|
||||
throw new PkiSyncError({
|
||||
message: `Certificate validation failed for ${certName}: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
|
||||
shouldRetry: false,
|
||||
context: {
|
||||
certificateName,
|
||||
certName
|
||||
}
|
||||
const errorMessage = validationError instanceof Error ? validationError.message : String(validationError);
|
||||
validationErrors.push({
|
||||
name: certName,
|
||||
error: `Certificate validation failed: ${errorMessage}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldUpdateCert) {
|
||||
setCertificates.push({
|
||||
key: certName,
|
||||
name: certificateName,
|
||||
cert,
|
||||
privateKey,
|
||||
certificateChain,
|
||||
existingArn: existingCert?.arn
|
||||
});
|
||||
const certificateName = generateCertificateName(certName, pkiSync);
|
||||
|
||||
let existingArn: string | undefined;
|
||||
|
||||
const existingCert = Object.values(acmCertificates).find((acmCert) => {
|
||||
return validateCertificateIdentification(certName, acmCert, alternativeNames);
|
||||
});
|
||||
|
||||
if (existingCert?.arn && preserveArn) {
|
||||
// When preserveArn is true, reuse the existing ARN
|
||||
existingArn = existingCert.arn;
|
||||
}
|
||||
|
||||
setCertificates.push({
|
||||
key: certName,
|
||||
name: certificateName,
|
||||
cert,
|
||||
privateKey,
|
||||
certificateChain,
|
||||
existingArn
|
||||
});
|
||||
});
|
||||
|
||||
// Identify expired/removed certificates that need to be cleaned up from ACM
|
||||
const certificatesToRemove = Object.values(acmCertificates)
|
||||
.filter((acmCert) => {
|
||||
if (!acmCert.arn || !acmCert.Tags) {
|
||||
@@ -445,8 +466,22 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
return false;
|
||||
}
|
||||
|
||||
const isActive = activeCertificateNames.includes(certNameTag.Value);
|
||||
return !isActive;
|
||||
const isActive = activeCertificateNames.some((activeCertName) => {
|
||||
const certData = certificateMap[activeCertName];
|
||||
if (!certData) return false;
|
||||
|
||||
return validateCertificateIdentification(activeCertName, acmCert, certData.alternativeNames);
|
||||
});
|
||||
|
||||
if (!isActive) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!preserveArn && isActive) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.map((acmCert) => acmCert.arn!)
|
||||
.filter((arn) => arn);
|
||||
@@ -457,14 +492,17 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
try {
|
||||
const importParams: AWS.ACM.ImportCertificateRequest = {
|
||||
Certificate: cert,
|
||||
PrivateKey: privateKey,
|
||||
Tags: [
|
||||
PrivateKey: privateKey
|
||||
};
|
||||
|
||||
if (!existingArn) {
|
||||
importParams.Tags = [
|
||||
{
|
||||
Key: INFISICAL_CERTIFICATE_TAG,
|
||||
Value: key
|
||||
}
|
||||
]
|
||||
};
|
||||
];
|
||||
}
|
||||
|
||||
if (certificateChain && certificateChain.trim().length > 0) {
|
||||
importParams.CertificateChain = certificateChain;
|
||||
@@ -478,6 +516,39 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
syncId: pkiSync.id
|
||||
});
|
||||
|
||||
if (existingArn && response.CertificateArn) {
|
||||
try {
|
||||
// Small delay to ensure AWS ACM has processed the certificate import
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(() => resolve(), 100);
|
||||
});
|
||||
|
||||
await withRateLimitRetry(
|
||||
() =>
|
||||
acm
|
||||
.addTagsToCertificate({
|
||||
CertificateArn: response.CertificateArn!,
|
||||
Tags: [
|
||||
{
|
||||
Key: INFISICAL_CERTIFICATE_TAG,
|
||||
Value: key
|
||||
}
|
||||
]
|
||||
})
|
||||
.promise(),
|
||||
{
|
||||
operation: "add-tags-to-certificate",
|
||||
syncId: pkiSync.id
|
||||
}
|
||||
);
|
||||
} catch (tagError) {
|
||||
const errorMessage = tagError instanceof Error ? tagError.message : "Unknown tagging error";
|
||||
logger.warn(
|
||||
`Failed to add tags to certificate ${key} (ARN: ${response.CertificateArn}): ${errorMessage}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { key, name, success: true, response };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
@@ -520,15 +591,21 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
const details: {
|
||||
failedUploads?: Array<{ name: string; error: string }>;
|
||||
failedRemovals?: Array<{ name: string; error: string }>;
|
||||
validationErrors?: Array<{ name: string; error: string }>;
|
||||
} = {};
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
details.validationErrors = validationErrors;
|
||||
}
|
||||
|
||||
if (failedUploads.length > 0) {
|
||||
details.failedUploads = failedUploads.map((failure, index) => {
|
||||
const certificateName = setCertificates[index]?.name || "unknown";
|
||||
const certificateRequest = setCertificates[index];
|
||||
const certificateName = certificateRequest?.name || certificateRequest?.key || "unknown";
|
||||
let errorMessage = "Unknown error";
|
||||
|
||||
if (failure.status === "rejected") {
|
||||
errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error";
|
||||
errorMessage = failure.reason instanceof Error ? failure.reason.message : String(failure.reason);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -577,7 +654,14 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
kmsService
|
||||
);
|
||||
|
||||
const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id);
|
||||
const {
|
||||
acmCertificates
|
||||
}: {
|
||||
acmCertificates: Record<
|
||||
string,
|
||||
{ cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList }
|
||||
>;
|
||||
} = await $getAwsAcmCertificates(acm, pkiSync.id);
|
||||
|
||||
const certificateArnsToRemove: string[] = [];
|
||||
|
||||
|
||||
+9
-4
@@ -14,6 +14,7 @@ export const AwsCertificateManagerPkiSyncConfigSchema = z.object({
|
||||
const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(true),
|
||||
preserveArn: z.boolean().default(true),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -28,6 +29,9 @@ const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
||||
|
||||
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_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some(
|
||||
@@ -43,7 +47,7 @@ const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Certificate name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, spaces, hyphens, and underscores and be 1-256 characters long when compiled for AWS Certificate Manager"
|
||||
"Certificate name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, spaces, hyphens, and underscores and be 1-256 characters long when compiled for AWS Certificate Manager. Available placeholders: {{certificateId}}, {{profileId}}, {{commonName}}, {{friendlyName}}, {{environment}}"
|
||||
}
|
||||
)
|
||||
});
|
||||
@@ -60,9 +64,10 @@ export const CreateAwsCertificateManagerPkiSyncSchema = z.object({
|
||||
isAutoSyncEnabled: z.boolean().default(true),
|
||||
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema,
|
||||
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional().default({}),
|
||||
subscriberId: z.string().optional(),
|
||||
subscriberId: z.string().nullish(),
|
||||
connectionId: z.string(),
|
||||
projectId: z.string().trim().min(1)
|
||||
projectId: z.string().trim().min(1),
|
||||
certificateIds: z.array(z.string().uuid()).optional()
|
||||
});
|
||||
|
||||
export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({
|
||||
@@ -71,7 +76,7 @@ export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({
|
||||
isAutoSyncEnabled: z.boolean().optional(),
|
||||
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema.optional(),
|
||||
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional(),
|
||||
subscriberId: z.string().optional(),
|
||||
subscriberId: z.string().nullish(),
|
||||
connectionId: z.string().optional()
|
||||
});
|
||||
|
||||
|
||||
+1
@@ -39,6 +39,7 @@ export interface SyncCertificatesResult {
|
||||
details?: {
|
||||
failedUploads?: Array<{ name: string; error: string }>;
|
||||
failedRemovals?: Array<{ name: string; error: string }>;
|
||||
validationErrors?: Array<{ name: string; error: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user