PKI Syncs ui and removal improvements

This commit is contained in:
Carlos Monastyrski
2025-10-30 14:54:07 -03:00
parent 696680bd8b
commit caee0b34e8
17 changed files with 290 additions and 92 deletions

View File

@@ -93,6 +93,8 @@ const PkiSyncCertificateSchema = z.object({
certificateStatus: z.string().optional(), certificateStatus: z.string().optional(),
certificateNotBefore: z.date().optional(), certificateNotBefore: z.date().optional(),
certificateNotAfter: z.date().optional(), certificateNotAfter: z.date().optional(),
certificateRenewBeforeDays: z.number().optional(),
certificateRenewalError: z.string().optional(),
pkiSyncName: z.string().optional(), pkiSyncName: z.string().optional(),
pkiSyncDestination: z.string().optional() pkiSyncDestination: z.string().optional()
}); });

View File

@@ -184,6 +184,9 @@ export const certificateSyncDALFactory = (db: TDbClient) => {
certificateStatus?: string; certificateStatus?: string;
certificateNotBefore?: Date; certificateNotBefore?: Date;
certificateNotAfter?: Date; certificateNotAfter?: Date;
certificateRenewBeforeDays?: number;
certificateRenewedByCertificateId?: string;
certificateRenewalError?: string;
pkiSyncName?: string; pkiSyncName?: string;
pkiSyncDestination?: string; pkiSyncDestination?: string;
})[]; })[];
@@ -216,6 +219,9 @@ export const certificateSyncDALFactory = (db: TDbClient) => {
db.ref("status").withSchema(TableName.Certificate).as("certificateStatus"), db.ref("status").withSchema(TableName.Certificate).as("certificateStatus"),
db.ref("notBefore").withSchema(TableName.Certificate).as("certificateNotBefore"), db.ref("notBefore").withSchema(TableName.Certificate).as("certificateNotBefore"),
db.ref("notAfter").withSchema(TableName.Certificate).as("certificateNotAfter"), db.ref("notAfter").withSchema(TableName.Certificate).as("certificateNotAfter"),
db.ref("renewBeforeDays").withSchema(TableName.Certificate).as("certificateRenewBeforeDays"),
db.ref("renewedByCertificateId").withSchema(TableName.Certificate).as("certificateRenewedByCertificateId"),
db.ref("renewalError").withSchema(TableName.Certificate).as("certificateRenewalError"),
db.ref("name").withSchema(TableName.PkiSync).as("pkiSyncName"), db.ref("name").withSchema(TableName.PkiSync).as("pkiSyncName"),
db.ref("destination").withSchema(TableName.PkiSync).as("pkiSyncDestination") db.ref("destination").withSchema(TableName.PkiSync).as("pkiSyncDestination")
) )
@@ -235,6 +241,9 @@ export const certificateSyncDALFactory = (db: TDbClient) => {
certificateStatus?: string; certificateStatus?: string;
certificateNotBefore?: Date; certificateNotBefore?: Date;
certificateNotAfter?: Date; certificateNotAfter?: Date;
certificateRenewBeforeDays?: number;
certificateRenewedByCertificateId?: string;
certificateRenewalError?: string;
pkiSyncName?: string; pkiSyncName?: string;
pkiSyncDestination?: string; pkiSyncDestination?: string;
})[]; })[];

View File

@@ -2,5 +2,6 @@ export enum CertificateSyncStatus {
Pending = "pending", Pending = "pending",
Syncing = "syncing", Syncing = "syncing",
Succeeded = "succeeded", Succeeded = "succeeded",
Failed = "failed" Failed = "failed",
Running = "running"
} }

View File

@@ -51,7 +51,7 @@ import {
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";
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue"; import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
import { replaceCertificateInSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils"; import { addRenewedCertificateToSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils";
import { import {
TCertificateFromProfileResponse, TCertificateFromProfileResponse,
TCertificateOrderResponse, TCertificateOrderResponse,
@@ -885,7 +885,7 @@ export const certificateV3ServiceFactory = ({
tx tx
); );
await replaceCertificateInSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx); await addRenewedCertificateToSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx);
return { return {
certificate, certificate,

View File

@@ -15,6 +15,9 @@ import {
AwsConnectionAssumeRoleCredentialsSchema AwsConnectionAssumeRoleCredentialsSchema
} from "@app/services/app-connection/aws/aws-connection-schemas"; } from "@app/services/app-connection/aws/aws-connection-schemas";
import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types"; 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 { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types"; import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types";
@@ -182,6 +185,8 @@ const generateCertificateName = (certificateName: string, pkiSync: TPkiSyncWithC
type TAwsCertificateManagerPkiSyncFactoryDeps = { type TAwsCertificateManagerPkiSyncFactoryDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">; appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
certificateSyncDAL: Pick<TCertificateSyncDALFactory, "removeCertificates">;
certificateDAL: Pick<TCertificateDALFactory, "findById">;
}; };
const getAwsAcmClient = async ( const getAwsAcmClient = async (
@@ -240,7 +245,9 @@ const getAwsAcmClient = async (
export const awsCertificateManagerPkiSyncFactory = ({ export const awsCertificateManagerPkiSyncFactory = ({
kmsService, kmsService,
appConnectionDAL appConnectionDAL,
certificateSyncDAL,
certificateDAL
}: TAwsCertificateManagerPkiSyncFactoryDeps) => { }: TAwsCertificateManagerPkiSyncFactoryDeps) => {
const deleteCertificateFromAcm = async ( const deleteCertificateFromAcm = async (
acm: AWS.ACM, acm: AWS.ACM,
@@ -415,11 +422,12 @@ export const awsCertificateManagerPkiSyncFactory = ({
const validationErrors: Array<{ name: string; error: string }> = []; const validationErrors: Array<{ name: string; error: string }> = [];
const activeCertificateNames = Object.keys(certificateMap); const activeCertificateNames = Object.keys(certificateMap);
const syncOptions = pkiSync.syncOptions as { preserveArn?: boolean } | undefined; const syncOptions = pkiSync.syncOptions as { preserveArn?: boolean; canRemoveCertificates?: boolean } | undefined;
const preserveArn = syncOptions?.preserveArn ?? true; const preserveArn = syncOptions?.preserveArn ?? true;
const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true;
Object.entries(certificateMap).forEach(([certName, certData]) => { for (const [certName, certData] of Object.entries(certificateMap)) {
const { cert, privateKey, certificateChain, alternativeNames } = certData; const { cert, privateKey, certificateChain, alternativeNames, certificateId } = certData;
try { try {
validateCertificateContent(cert, privateKey); validateCertificateContent(cert, privateKey);
@@ -429,7 +437,16 @@ export const awsCertificateManagerPkiSyncFactory = ({
name: certName, name: certName,
error: `Certificate validation failed: ${errorMessage}` error: `Certificate validation failed: ${errorMessage}`
}); });
return; // eslint-disable-next-line no-continue
continue;
}
if (preserveArn && certificateId) {
const certificate = await certificateDAL.findById(certificateId);
if (certificate?.renewedByCertificateId) {
// eslint-disable-next-line no-continue
continue;
}
} }
const certificateName = generateCertificateName(certName, pkiSync); const certificateName = generateCertificateName(certName, pkiSync);
@@ -451,44 +468,47 @@ export const awsCertificateManagerPkiSyncFactory = ({
cert, cert,
privateKey, privateKey,
certificateChain, certificateChain,
existingArn existingArn,
certificateId
}); });
}); }
const certificatesToRemove = Object.values(acmCertificates) const certificatesToRemove = canRemoveCertificates
.filter((acmCert) => { ? Object.values(acmCertificates)
if (!acmCert.arn || !acmCert.Tags) { .filter((acmCert) => {
return false; if (!acmCert.arn || !acmCert.Tags) {
} return false;
}
const certNameTag = findInfisicalCertificateTag(acmCert.Tags); const certNameTag = findInfisicalCertificateTag(acmCert.Tags);
if (!certNameTag || !certNameTag.Value) { if (!certNameTag || !certNameTag.Value) {
return false; return false;
} }
const isActive = activeCertificateNames.some((activeCertName) => { const isActive = activeCertificateNames.some((activeCertName) => {
const certData = certificateMap[activeCertName]; const certData = certificateMap[activeCertName];
if (!certData) return false; if (!certData) return false;
return validateCertificateIdentification(activeCertName, acmCert, certData.alternativeNames); return validateCertificateIdentification(activeCertName, acmCert, certData.alternativeNames);
}); });
if (!isActive) { if (!isActive) {
return true; return true;
} }
if (!preserveArn && isActive) { if (!preserveArn && isActive) {
return true; return true;
} }
return false; return false;
}) })
.map((acmCert) => acmCert.arn!) .map((acmCert) => acmCert.arn!)
.filter((arn) => arn); .filter((arn) => arn)
: [];
const uploadResults = await executeWithConcurrencyLimit( const uploadResults = await executeWithConcurrencyLimit(
setCertificates, setCertificates,
async ({ key, name, cert, privateKey, certificateChain, existingArn }) => { async ({ key, name, cert, privateKey, certificateChain, existingArn, certificateId }) => {
try { try {
const importParams: AWS.ACM.ImportCertificateRequest = { const importParams: AWS.ACM.ImportCertificateRequest = {
Certificate: cert, Certificate: cert,
@@ -549,6 +569,13 @@ export const awsCertificateManagerPkiSyncFactory = ({
} }
} }
if (existingArn && preserveArn && certificateId) {
const currentCertificate = await certificateDAL.findById(certificateId);
if (currentCertificate?.renewedFromCertificateId) {
await certificateSyncDAL.removeCertificates(pkiSync.id, [currentCertificate.renewedFromCertificateId]);
}
}
return { key, name, success: true, response }; return { key, name, success: true, response };
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"; const errorMessage = error instanceof Error ? error.message : "Unknown error";
@@ -644,7 +671,8 @@ export const awsCertificateManagerPkiSyncFactory = ({
const removeCertificates = async ( const removeCertificates = async (
pkiSync: TPkiSyncWithCredentials, pkiSync: TPkiSyncWithCredentials,
certificateNames: string[] certificateNames: string[],
deps?: { certificateSyncDAL?: TCertificateSyncDALFactory; certificateMap?: TCertificateMap }
): Promise<RemoveCertificatesResult> => { ): Promise<RemoveCertificatesResult> => {
const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig; const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig;
const acm = await getAwsAcmClient( const acm = await getAwsAcmClient(
@@ -686,6 +714,43 @@ export const awsCertificateManagerPkiSyncFactory = ({
const failedRemovals = results.filter((result) => result.status === "rejected"); const failedRemovals = results.filter((result) => result.status === "rejected");
if (failedRemovals.length > 0 && deps?.certificateSyncDAL && deps?.certificateMap) {
const certificateNameToArnMap = new Map<string, string>();
for (const certName of certificateNames) {
const matchingCerts = Object.values(acmCertificates).filter((acmCert) =>
validateCertificateIdentification(certName, acmCert)
);
for (const acmCert of matchingCerts) {
if (acmCert.arn) {
certificateNameToArnMap.set(acmCert.arn, certName);
}
}
}
for (const failure of failedRemovals) {
if (failure.status === "rejected") {
const failedArn = certificateArnsToRemove[results.indexOf(failure)];
const certificateName = certificateNameToArnMap.get(failedArn);
if (certificateName && deps.certificateMap[certificateName]?.certificateId) {
const { certificateId } = deps.certificateMap[certificateName];
if (certificateId) {
const errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error";
try {
await deps.certificateSyncDAL.updateSyncStatus(
pkiSync.id,
certificateId,
CertificateSyncStatus.Failed,
`Failed to remove from AWS: ${errorMessage}`
);
} catch (updateError) {
logger.warn(`Failed to update sync status for certificate ${certificateId}:`, String(updateError));
}
}
}
}
}
}
if (failedRemovals.length > 0) { if (failedRemovals.length > 0) {
const failedReasons = failedRemovals.map((failure) => { const failedReasons = failedRemovals.map((failure) => {
if (failure.status === "rejected") { if (failure.status === "rejected") {

View File

@@ -56,4 +56,5 @@ export interface CertificateImportRequest {
privateKey: string; privateKey: string;
certificateChain?: string; certificateChain?: string;
existingArn?: string; existingArn?: string;
certificateId?: string;
} }

View File

@@ -6,6 +6,8 @@ import { request } from "@app/lib/config/request";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault"; import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
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 { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { matchesCertificateNameSchema } from "@app/services/pki-sync/pki-sync-fns"; import { matchesCertificateNameSchema } from "@app/services/pki-sync/pki-sync-fns";
@@ -32,7 +34,9 @@ const extractCertificateNameFromId = (certificateId: string): string => {
}; };
const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyncWithCredentials): boolean => { const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyncWithCredentials): boolean => {
const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined; const syncOptions = pkiSync.syncOptions as
| { certificateNameSchema?: string; canRemoveCertificates?: boolean }
| undefined;
const certificateNameSchema = syncOptions?.certificateNameSchema; const certificateNameSchema = syncOptions?.certificateNameSchema;
if (certificateNameSchema) { if (certificateNameSchema) {
@@ -330,13 +334,19 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
cert: string; cert: string;
privateKey: string; privateKey: string;
certificateChain?: string; certificateChain?: string;
certificateId?: string;
}[] = []; }[] = [];
const syncOptions = pkiSync.syncOptions as
| { certificateNameSchema?: string; canRemoveCertificates?: boolean }
| undefined;
const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true;
// Track which certificates should exist in Azure Key Vault // Track which certificates should exist in Azure Key Vault
const activeCertificateNames = Object.keys(certificateMap); const activeCertificateNames = Object.keys(certificateMap);
// Iterate through certificates to sync to Azure Key Vault // Iterate through certificates to sync to Azure Key Vault
Object.entries(certificateMap).forEach(([certName, { cert, privateKey, certificateChain }]) => { Object.entries(certificateMap).forEach(([certName, { cert, privateKey, certificateChain, certificateId }]) => {
if (disabledAzureKeyVaultCertificateKeys.includes(certName)) { if (disabledAzureKeyVaultCertificateKeys.includes(certName)) {
return; return;
} }
@@ -349,19 +359,22 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
key: certName, key: certName,
cert, cert,
privateKey, privateKey,
certificateChain certificateChain,
certificateId
}); });
} }
}); });
// Identify expired/removed certificates that need to be cleaned up from Azure Key Vault // Identify expired/removed certificates that need to be cleaned up from Azure Key Vault
// Only remove certificates that were managed by Infisical (match naming schema) // Only remove certificates that were managed by Infisical (match naming schema)
const certificatesToRemove = Object.keys(vaultCertificates).filter( const certificatesToRemove = canRemoveCertificates
(vaultCertName) => ? Object.keys(vaultCertificates).filter(
isInfisicalManagedCertificate(vaultCertName, pkiSync) && (vaultCertName) =>
!activeCertificateNames.includes(vaultCertName) && isInfisicalManagedCertificate(vaultCertName, pkiSync) &&
!disabledAzureKeyVaultCertificateKeys.includes(vaultCertName) !activeCertificateNames.includes(vaultCertName) &&
); !disabledAzureKeyVaultCertificateKeys.includes(vaultCertName)
)
: [];
// Upload certificates to Azure Key Vault with rate limiting // Upload certificates to Azure Key Vault with rate limiting
const uploadResults = await executeWithConcurrencyLimit( const uploadResults = await executeWithConcurrencyLimit(
@@ -599,7 +612,11 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
}; };
}; };
const removeCertificates = async (pkiSync: TPkiSyncWithCredentials, certificateNames: string[]) => { const removeCertificates = async (
pkiSync: TPkiSyncWithCredentials,
certificateNames: string[],
deps?: { certificateSyncDAL?: TCertificateSyncDALFactory; certificateMap?: TCertificateMap }
) => {
const { accessToken } = await getAzureConnectionAccessToken(pkiSync.connection.id, appConnectionDAL, kmsService); const { accessToken } = await getAzureConnectionAccessToken(pkiSync.connection.id, appConnectionDAL, kmsService);
// Cast destination config to Azure Key Vault config // Cast destination config to Azure Key Vault config
@@ -648,6 +665,27 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
); );
const failedRemovals = results.filter((result) => result.status === "rejected"); const failedRemovals = results.filter((result) => result.status === "rejected");
if (failedRemovals.length > 0 && deps?.certificateSyncDAL && deps?.certificateMap) {
for (const failure of failedRemovals) {
if (failure.status === "rejected") {
const failedIndex = results.indexOf(failure);
const failedCertName = infisicalManagedCertNames[failedIndex];
if (failedCertName && deps.certificateMap[failedCertName]?.certificateId) {
const { certificateId } = deps.certificateMap[failedCertName];
if (certificateId) {
const errorMessage = (failure.reason as Error)?.message || "Unknown error";
await deps.certificateSyncDAL.updateSyncStatus(
pkiSync.id,
certificateId,
CertificateSyncStatus.Failed,
`Failed to remove from Azure: ${errorMessage}`
);
}
}
}
}
}
if (failedRemovals.length > 0) { if (failedRemovals.length > 0) {
const failedReasons = failedRemovals.map((failure) => { const failedReasons = failedRemovals.map((failure) => {
if (failure.status === "rejected") { if (failure.status === "rejected") {

View File

@@ -50,7 +50,7 @@ export const CreateAzureKeyVaultPkiSyncSchema = z.object({
isAutoSyncEnabled: z.boolean().default(true), isAutoSyncEnabled: z.boolean().default(true),
destinationConfig: AzureKeyVaultPkiSyncConfigSchema, destinationConfig: AzureKeyVaultPkiSyncConfigSchema,
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional().default({}), syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional().default({}),
subscriberId: z.string().optional(), subscriberId: z.string().nullish(),
connectionId: z.string(), connectionId: z.string(),
projectId: z.string().trim().min(1), projectId: z.string().trim().min(1),
certificateIds: z.array(z.string().uuid()).optional() certificateIds: z.array(z.string().uuid()).optional()
@@ -62,7 +62,7 @@ export const UpdateAzureKeyVaultPkiSyncSchema = z.object({
isAutoSyncEnabled: z.boolean().optional(), isAutoSyncEnabled: z.boolean().optional(),
destinationConfig: AzureKeyVaultPkiSyncConfigSchema.optional(), destinationConfig: AzureKeyVaultPkiSyncConfigSchema.optional(),
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional(), syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional(),
subscriberId: z.string().optional(), subscriberId: z.string().nullish(),
connectionId: z.string().optional() connectionId: z.string().optional()
}); });

View File

@@ -4,6 +4,8 @@ import { z, ZodSchema } from "zod";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; 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";
@@ -184,6 +186,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<{ ): Promise<{
uploaded: number; uploaded: number;
@@ -205,7 +209,12 @@ export const PkiSyncFns = {
} }
case PkiSync.AwsCertificateManager: { case PkiSync.AwsCertificateManager: {
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager); checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies); const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({
appConnectionDAL: dependencies.appConnectionDAL,
kmsService: dependencies.kmsService,
certificateDAL: dependencies.certificateDAL,
certificateSyncDAL: dependencies.certificateSyncDAL
});
return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap); return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap);
} }
default: default:
@@ -219,19 +228,33 @@ export const PkiSyncFns = {
dependencies: { dependencies: {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">; appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
certificateSyncDAL: TCertificateSyncDALFactory;
certificateDAL: TCertificateDALFactory;
certificateMap: TCertificateMap;
} }
): Promise<void> => { ): Promise<void> => {
switch (pkiSync.destination) { switch (pkiSync.destination) {
case PkiSync.AzureKeyVault: { case PkiSync.AzureKeyVault: {
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault); checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies); const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies);
await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames); await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames, {
certificateSyncDAL: dependencies.certificateSyncDAL,
certificateMap: dependencies.certificateMap
});
break; break;
} }
case PkiSync.AwsCertificateManager: { case PkiSync.AwsCertificateManager: {
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager); checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies); const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({
await awsCertificateManagerPkiSync.removeCertificates(pkiSync, certificateNames); appConnectionDAL: dependencies.appConnectionDAL,
kmsService: dependencies.kmsService,
certificateDAL: dependencies.certificateDAL,
certificateSyncDAL: dependencies.certificateSyncDAL
});
await awsCertificateManagerPkiSync.removeCertificates(pkiSync, certificateNames, {
certificateSyncDAL: dependencies.certificateSyncDAL,
certificateMap: dependencies.certificateMap
});
break; break;
} }
default: default:

View File

@@ -27,6 +27,7 @@ import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/ce
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 { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal"; import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
import { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums";
import { TPkiSyncDALFactory } from "./pki-sync-dal"; import { TPkiSyncDALFactory } from "./pki-sync-dal";
import { PkiSyncStatus } from "./pki-sync-enums"; import { PkiSyncStatus } from "./pki-sync-enums";
import { PkiSyncError } from "./pki-sync-errors"; import { PkiSyncError } from "./pki-sync-errors";
@@ -57,23 +58,12 @@ type TPkiSyncQueueFactoryDep = {
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">; auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
certificateDAL: Pick< certificateDAL: TCertificateDALFactory;
TCertificateDALFactory,
| "findLatestActiveCertForSubscriber"
| "findAllActiveCertsForSubscriber"
| "findActiveCertificatesByIds"
| "create"
| "findById"
| "find"
>;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">; certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">; certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
certificateSyncDAL: Pick< certificateSyncDAL: TCertificateSyncDALFactory;
TCertificateSyncDALFactory,
"findCertificateIdsByPkiSyncId" | "updateSyncStatus" | "bulkUpdateSyncStatus"
>;
}; };
type PkiSyncActionJob = Job< type PkiSyncActionJob = Job<
@@ -303,7 +293,8 @@ export const pkiSyncQueueFactory = ({
cert: certificatePem, cert: certificatePem,
privateKey: certPrivateKey || "", privateKey: certPrivateKey || "",
certificateChain, certificateChain,
alternativeNames alternativeNames,
certificateId: certificate.id
}; };
certificateMetadata.set(certificateName, { certificateMetadata.set(certificateName, {
@@ -427,7 +418,7 @@ export const pkiSyncQueueFactory = ({
const statusUpdates = Array.from(certificateMetadata.entries()).map(([, metadata]) => ({ const statusUpdates = Array.from(certificateMetadata.entries()).map(([, metadata]) => ({
pkiSyncId: pkiSync.id, pkiSyncId: pkiSync.id,
certificateId: metadata.id, certificateId: metadata.id,
status: "running", status: CertificateSyncStatus.Running,
message: "Syncing certificate to destination" message: "Syncing certificate to destination"
})); }));
@@ -437,7 +428,9 @@ export const pkiSyncQueueFactory = ({
const syncResult = await PkiSyncFns.syncCertificates(pkiSyncWithCredentials, certificateMap, { const syncResult = await PkiSyncFns.syncCertificates(pkiSyncWithCredentials, certificateMap, {
appConnectionDAL, appConnectionDAL,
kmsService kmsService,
certificateDAL,
certificateSyncDAL
}); });
logger.info( logger.info(
@@ -462,7 +455,7 @@ export const pkiSyncQueueFactory = ({
postSyncUpdates.push({ postSyncUpdates.push({
pkiSyncId: pkiSync.id, pkiSyncId: pkiSync.id,
certificateId: metadata.id, certificateId: metadata.id,
status: "succeeded", status: CertificateSyncStatus.Succeeded,
message: "Certificate successfully synced to destination" message: "Certificate successfully synced to destination"
}); });
} }
@@ -476,7 +469,7 @@ export const pkiSyncQueueFactory = ({
postSyncUpdates[updateIndex] = { postSyncUpdates[updateIndex] = {
pkiSyncId: pkiSync.id, pkiSyncId: pkiSync.id,
certificateId: metadata.id, certificateId: metadata.id,
status: "failed", status: CertificateSyncStatus.Failed,
message: `${validationError.error}` message: `${validationError.error}`
}; };
} }
@@ -493,7 +486,7 @@ export const pkiSyncQueueFactory = ({
postSyncUpdates[updateIndex] = { postSyncUpdates[updateIndex] = {
pkiSyncId: pkiSync.id, pkiSyncId: pkiSync.id,
certificateId: metadata.id, certificateId: metadata.id,
status: "failed", status: CertificateSyncStatus.Failed,
message: `Failed to sync certificate: ${failure.error}` message: `Failed to sync certificate: ${failure.error}`
}; };
} }
@@ -699,7 +692,10 @@ export const pkiSyncQueueFactory = ({
Object.keys(certificateMap), Object.keys(certificateMap),
{ {
appConnectionDAL, appConnectionDAL,
kmsService kmsService,
certificateSyncDAL,
certificateDAL,
certificateMap
} }
); );

View File

@@ -610,6 +610,10 @@ export const pkiSyncServiceFactory = ({
certificateStatus: detail.certificateStatus || undefined, certificateStatus: detail.certificateStatus || undefined,
certificateNotBefore: detail.certificateNotBefore || undefined, certificateNotBefore: detail.certificateNotBefore || undefined,
certificateNotAfter: detail.certificateNotAfter || undefined, certificateNotAfter: detail.certificateNotAfter || undefined,
certificateRenewBeforeDays: !detail.certificateRenewedByCertificateId
? detail.certificateRenewBeforeDays
: undefined,
certificateRenewalError: detail.certificateRenewalError || undefined,
pkiSyncName: detail.pkiSyncName || undefined, pkiSyncName: detail.pkiSyncName || undefined,
pkiSyncDestination: detail.pkiSyncDestination || undefined pkiSyncDestination: detail.pkiSyncDestination || undefined
})); }));

View File

@@ -73,7 +73,7 @@ export type TPkiSyncListItem = TPkiSync & {
export type TCertificateMap = Record< export type TCertificateMap = Record<
string, string,
{ cert: string; privateKey: string; certificateChain?: string; alternativeNames?: string[] } { cert: string; privateKey: string; certificateChain?: string; alternativeNames?: string[]; certificateId?: string }
>; >;
export type TCreatePkiSyncDTO = { export type TCreatePkiSyncDTO = {
@@ -176,6 +176,8 @@ export type TPkiSyncCertificate = {
certificateStatus?: string; certificateStatus?: string;
certificateNotBefore?: Date; certificateNotBefore?: Date;
certificateNotAfter?: Date; certificateNotAfter?: Date;
certificateRenewBeforeDays?: number;
certificateRenewalError?: string;
pkiSyncName?: string; pkiSyncName?: string;
pkiSyncDestination?: string; pkiSyncDestination?: string;
}; };

View File

@@ -59,7 +59,7 @@ export const triggerAutoSyncForCertificate = async (
} }
}; };
export const replaceCertificateInSyncs = async ( export const addRenewedCertificateToSyncs = async (
oldCertificateId: string, oldCertificateId: string,
newCertificateId: string, newCertificateId: string,
dependencies: { dependencies: {
@@ -77,18 +77,15 @@ export const replaceCertificateInSyncs = async (
return; return;
} }
const replacementPromises = pkiSyncIds.map(async (pkiSyncId) => { const addPromises = pkiSyncIds.map(async (pkiSyncId) => {
await dependencies.certificateSyncDAL.removeCertificates(pkiSyncId, [oldCertificateId], tx);
await dependencies.certificateSyncDAL.addCertificates(pkiSyncId, [newCertificateId], tx); await dependencies.certificateSyncDAL.addCertificates(pkiSyncId, [newCertificateId], tx);
}); });
await Promise.all(replacementPromises); await Promise.all(addPromises);
logger.info( logger.info(`Successfully added renewed certificate ${newCertificateId} to ${pkiSyncIds.length} PKI sync(s)`);
`Successfully replaced certificate ${oldCertificateId} with ${newCertificateId} in ${pkiSyncIds.length} PKI sync(s)`
);
} catch (error) { } catch (error) {
logger.error(error, `Failed to replace certificate ${oldCertificateId} with ${newCertificateId} in syncs:`); logger.error(error, `Failed to add renewed certificate ${newCertificateId} to syncs:`);
throw error; throw error;
} }
}; };

View File

@@ -71,14 +71,14 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
isChecked={value} isChecked={value}
> >
<p> <p>
Enable Certificate Removal{" "} Enable Inactive Certificate Removal{" "}
<Tooltip <Tooltip
className="max-w-md" className="max-w-md"
content={ content={
<> <>
<p> <p>
When enabled, Infisical will remove certificates from the destination during When enabled, Infisical will remove certificates from the destination during
a sync if they are no longer managed by Infisical. a sync if they are no longer active in Infisical.
</p> </p>
<p className="mt-4"> <p className="mt-4">
Disable this option if you intend to manage some certificates manually Disable this option if you intend to manage some certificates manually

View File

@@ -61,6 +61,8 @@ export type TPkiSyncCertificate = {
certificateStatus?: string; certificateStatus?: string;
certificateNotBefore?: Date; certificateNotBefore?: Date;
certificateNotAfter?: Date; certificateNotAfter?: Date;
certificateRenewBeforeDays?: number;
certificateRenewalError?: string;
pkiSyncName?: string; pkiSyncName?: string;
pkiSyncDestination?: string; pkiSyncDestination?: string;
}; };

View File

@@ -1,6 +1,11 @@
import { useState } from "react"; import { useState } from "react";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { faCertificate, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons"; import {
faCertificate,
faClockRotateLeft,
faEdit,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -49,6 +54,18 @@ const getSyncStatusText = (status?: CertificateSyncStatus | null) => {
return "Unknown"; return "Unknown";
}; };
const getCertificateStatusVariant = (isExpired: boolean, isRevoked: boolean) => {
if (isRevoked) return "danger";
if (isExpired) return "danger";
return "success";
};
const getCertificateStatusText = (isExpired: boolean, isRevoked: boolean) => {
if (isRevoked) return "Revoked";
if (isExpired) return "Expired";
return "Active";
};
export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => { export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
const [isManageModalOpen, setIsManageModalOpen] = useState(false); const [isManageModalOpen, setIsManageModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@@ -128,11 +145,12 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
<Th className="w-1/3">SAN / CN</Th> <Th className="w-3/8">SAN / CN</Th>
<Th className="w-1/3">Serial Number</Th> <Th className="w-1/8">Status</Th>
<Th className="w-1/9">Status</Th> <Th className="w-1/8">Serial Number</Th>
<Th className="w-1/9">Expires</Th> <Th className="w-1/8">Sync Status</Th>
<Th className="w-1/9">Actions</Th> <Th className="w-1/8">Expires At</Th>
<Th className="w-1/8" />
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
@@ -140,6 +158,29 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
const isExpired = syncCert.certificateNotAfter const isExpired = syncCert.certificateNotAfter
? new Date(syncCert.certificateNotAfter) < new Date() ? new Date(syncCert.certificateNotAfter) < new Date()
: false; : false;
const isRevoked = syncCert.certificateStatus === "revoked";
// Calculate auto-renewal timeline
const hasAutoRenewal = Boolean(
syncCert.certificateRenewBeforeDays &&
syncCert.certificateRenewBeforeDays > 0 &&
!syncCert.certificateRenewalError &&
syncCert.certificateNotAfter
);
const daysUntilRenewal =
hasAutoRenewal && syncCert.certificateNotAfter
? (() => {
const expiryDate = new Date(syncCert.certificateNotAfter);
const renewalDate = new Date(
expiryDate.getTime() -
syncCert.certificateRenewBeforeDays! * 24 * 60 * 60 * 1000
);
const now = new Date();
const diffInMs = renewalDate.getTime() - now.getTime();
return Math.max(0, Math.ceil(diffInMs / (24 * 60 * 60 * 1000)));
})()
: null;
const { originalDisplayName } = getCertificateDisplayName( const { originalDisplayName } = getCertificateDisplayName(
{ {
@@ -162,12 +203,22 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
fallback="Unknown" fallback="Unknown"
/> />
</Td> </Td>
<Td>
<Badge variant={getCertificateStatusVariant(isExpired, isRevoked)}>
{getCertificateStatusText(isExpired, isRevoked)}
</Badge>
</Td>
<Td className="max-w-0"> <Td className="max-w-0">
<div <div
className="truncate text-xs" className="truncate text-xs"
title={syncCert.certificateSerialNumber || "Unknown"} title={syncCert.certificateSerialNumber || "Unknown"}
> >
{syncCert.certificateSerialNumber || "Unknown"} {(() => {
const serial = syncCert.certificateSerialNumber;
if (!serial || serial === "Unknown") return "Unknown";
if (serial.length <= 8) return serial;
return `${serial.substring(0, 4)}...${serial.substring(serial.length - 4)}`;
})()}
</div> </div>
</Td> </Td>
<Td> <Td>
@@ -191,7 +242,14 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
: "Unknown"} : "Unknown"}
</span> </span>
</Td> </Td>
<Td className="flex items-center"> <Td className="flex items-center justify-end gap-2 pr-4">
{hasAutoRenewal && daysUntilRenewal !== null && (
<Tooltip content={`Auto-renews in ${daysUntilRenewal}d`}>
<div className="text-primary-500">
<FontAwesomeIcon icon={faClockRotateLeft} size="sm" />
</div>
</Tooltip>
)}
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.Edit} I={ProjectPermissionPkiSyncActions.Edit}
a={permissionSubject} a={permissionSubject}

View File

@@ -65,7 +65,7 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
</Badge> </Badge>
</GenericFieldLabel> </GenericFieldLabel>
*/} */}
<GenericFieldLabel label="Certificate Removal" labelClassName="mb-1"> <GenericFieldLabel label="Inactive Certificate Removal" labelClassName="mb-1">
<Badge variant={canRemoveCertificates ? "success" : "danger"}> <Badge variant={canRemoveCertificates ? "success" : "danger"}>
{canRemoveCertificates ? "Enabled" : "Disabled"} {canRemoveCertificates ? "Enabled" : "Disabled"}
</Badge> </Badge>