From caee0b34e88bc28f3a2f8925565fd97b8ad5e94c Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Thu, 30 Oct 2025 14:54:07 -0300 Subject: [PATCH] PKI Syncs ui and removal improvements --- .../v1/pki-sync-routers/pki-sync-router.ts | 2 + .../certificate-sync/certificate-sync-dal.ts | 9 ++ .../certificate-sync-enums.ts | 3 +- .../certificate-v3/certificate-v3-service.ts | 4 +- .../aws-certificate-manager-pki-sync-fns.ts | 131 +++++++++++++----- .../aws-certificate-manager-pki-sync-types.ts | 1 + .../azure-key-vault-pki-sync-fns.ts | 58 ++++++-- .../azure-key-vault-pki-sync-schemas.ts | 4 +- backend/src/services/pki-sync/pki-sync-fns.ts | 31 ++++- .../src/services/pki-sync/pki-sync-queue.ts | 36 +++-- .../src/services/pki-sync/pki-sync-service.ts | 4 + .../src/services/pki-sync/pki-sync-types.ts | 4 +- .../src/services/pki-sync/pki-sync-utils.ts | 13 +- .../PkiSyncOptionsFields.tsx | 4 +- .../src/hooks/api/pkiSyncs/types/common.ts | 2 + .../components/PkiSyncCertificatesSection.tsx | 74 ++++++++-- .../PkiSyncOptionsSection.tsx | 2 +- 17 files changed, 290 insertions(+), 92 deletions(-) diff --git a/backend/src/server/routes/v1/pki-sync-routers/pki-sync-router.ts b/backend/src/server/routes/v1/pki-sync-routers/pki-sync-router.ts index 204f38aa4..45e5464f2 100644 --- a/backend/src/server/routes/v1/pki-sync-routers/pki-sync-router.ts +++ b/backend/src/server/routes/v1/pki-sync-routers/pki-sync-router.ts @@ -93,6 +93,8 @@ const PkiSyncCertificateSchema = z.object({ certificateStatus: z.string().optional(), certificateNotBefore: z.date().optional(), certificateNotAfter: z.date().optional(), + certificateRenewBeforeDays: z.number().optional(), + certificateRenewalError: z.string().optional(), pkiSyncName: z.string().optional(), pkiSyncDestination: z.string().optional() }); diff --git a/backend/src/services/certificate-sync/certificate-sync-dal.ts b/backend/src/services/certificate-sync/certificate-sync-dal.ts index 174bc6141..904a18a75 100644 --- a/backend/src/services/certificate-sync/certificate-sync-dal.ts +++ b/backend/src/services/certificate-sync/certificate-sync-dal.ts @@ -184,6 +184,9 @@ export const certificateSyncDALFactory = (db: TDbClient) => { certificateStatus?: string; certificateNotBefore?: Date; certificateNotAfter?: Date; + certificateRenewBeforeDays?: number; + certificateRenewedByCertificateId?: string; + certificateRenewalError?: string; pkiSyncName?: string; pkiSyncDestination?: string; })[]; @@ -216,6 +219,9 @@ export const certificateSyncDALFactory = (db: TDbClient) => { db.ref("status").withSchema(TableName.Certificate).as("certificateStatus"), db.ref("notBefore").withSchema(TableName.Certificate).as("certificateNotBefore"), 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("destination").withSchema(TableName.PkiSync).as("pkiSyncDestination") ) @@ -235,6 +241,9 @@ export const certificateSyncDALFactory = (db: TDbClient) => { certificateStatus?: string; certificateNotBefore?: Date; certificateNotAfter?: Date; + certificateRenewBeforeDays?: number; + certificateRenewedByCertificateId?: string; + certificateRenewalError?: string; pkiSyncName?: string; pkiSyncDestination?: string; })[]; diff --git a/backend/src/services/certificate-sync/certificate-sync-enums.ts b/backend/src/services/certificate-sync/certificate-sync-enums.ts index 97c08b339..7b9eedafb 100644 --- a/backend/src/services/certificate-sync/certificate-sync-enums.ts +++ b/backend/src/services/certificate-sync/certificate-sync-enums.ts @@ -2,5 +2,6 @@ export enum CertificateSyncStatus { Pending = "pending", Syncing = "syncing", Succeeded = "succeeded", - Failed = "failed" + Failed = "failed", + Running = "running" } diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index 6f5870f4f..1cff10f3c 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -51,7 +51,7 @@ import { import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal"; import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal"; 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 { TCertificateFromProfileResponse, TCertificateOrderResponse, @@ -885,7 +885,7 @@ export const certificateV3ServiceFactory = ({ tx ); - await replaceCertificateInSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx); + await addRenewedCertificateToSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx); return { certificate, diff --git a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts index 774da6000..0e416f016 100644 --- a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts +++ b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts @@ -15,6 +15,9 @@ import { AwsConnectionAssumeRoleCredentialsSchema } from "@app/services/app-connection/aws/aws-connection-schemas"; 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 { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types"; @@ -182,6 +185,8 @@ const generateCertificateName = (certificateName: string, pkiSync: TPkiSyncWithC type TAwsCertificateManagerPkiSyncFactoryDeps = { appConnectionDAL: Pick; kmsService: Pick; + certificateSyncDAL: Pick; + certificateDAL: Pick; }; const getAwsAcmClient = async ( @@ -240,7 +245,9 @@ const getAwsAcmClient = async ( export const awsCertificateManagerPkiSyncFactory = ({ kmsService, - appConnectionDAL + appConnectionDAL, + certificateSyncDAL, + certificateDAL }: TAwsCertificateManagerPkiSyncFactoryDeps) => { const deleteCertificateFromAcm = async ( acm: AWS.ACM, @@ -415,11 +422,12 @@ export const awsCertificateManagerPkiSyncFactory = ({ const validationErrors: Array<{ name: string; error: string }> = []; 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 canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true; - Object.entries(certificateMap).forEach(([certName, certData]) => { - const { cert, privateKey, certificateChain, alternativeNames } = certData; + for (const [certName, certData] of Object.entries(certificateMap)) { + const { cert, privateKey, certificateChain, alternativeNames, certificateId } = certData; try { validateCertificateContent(cert, privateKey); @@ -429,7 +437,16 @@ export const awsCertificateManagerPkiSyncFactory = ({ name: certName, 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); @@ -451,44 +468,47 @@ export const awsCertificateManagerPkiSyncFactory = ({ cert, privateKey, certificateChain, - existingArn + existingArn, + certificateId }); - }); + } - const certificatesToRemove = Object.values(acmCertificates) - .filter((acmCert) => { - if (!acmCert.arn || !acmCert.Tags) { - return false; - } + const certificatesToRemove = canRemoveCertificates + ? Object.values(acmCertificates) + .filter((acmCert) => { + if (!acmCert.arn || !acmCert.Tags) { + return false; + } - const certNameTag = findInfisicalCertificateTag(acmCert.Tags); - if (!certNameTag || !certNameTag.Value) { - return false; - } + const certNameTag = findInfisicalCertificateTag(acmCert.Tags); + if (!certNameTag || !certNameTag.Value) { + return false; + } - const isActive = activeCertificateNames.some((activeCertName) => { - const certData = certificateMap[activeCertName]; - if (!certData) return false; + const isActive = activeCertificateNames.some((activeCertName) => { + const certData = certificateMap[activeCertName]; + if (!certData) return false; - return validateCertificateIdentification(activeCertName, acmCert, certData.alternativeNames); - }); + return validateCertificateIdentification(activeCertName, acmCert, certData.alternativeNames); + }); - if (!isActive) { - return true; - } + if (!isActive) { + return true; + } - if (!preserveArn && isActive) { - return true; - } + if (!preserveArn && isActive) { + return true; + } - return false; - }) - .map((acmCert) => acmCert.arn!) - .filter((arn) => arn); + return false; + }) + .map((acmCert) => acmCert.arn!) + .filter((arn) => arn) + : []; const uploadResults = await executeWithConcurrencyLimit( setCertificates, - async ({ key, name, cert, privateKey, certificateChain, existingArn }) => { + async ({ key, name, cert, privateKey, certificateChain, existingArn, certificateId }) => { try { const importParams: AWS.ACM.ImportCertificateRequest = { 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 }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; @@ -644,7 +671,8 @@ export const awsCertificateManagerPkiSyncFactory = ({ const removeCertificates = async ( pkiSync: TPkiSyncWithCredentials, - certificateNames: string[] + certificateNames: string[], + deps?: { certificateSyncDAL?: TCertificateSyncDALFactory; certificateMap?: TCertificateMap } ): Promise => { const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig; const acm = await getAwsAcmClient( @@ -686,6 +714,43 @@ export const awsCertificateManagerPkiSyncFactory = ({ const failedRemovals = results.filter((result) => result.status === "rejected"); + if (failedRemovals.length > 0 && deps?.certificateSyncDAL && deps?.certificateMap) { + const certificateNameToArnMap = new Map(); + 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) { const failedReasons = failedRemovals.map((failure) => { if (failure.status === "rejected") { diff --git a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts index c9770c5b2..8b2b8b87e 100644 --- a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts +++ b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts @@ -56,4 +56,5 @@ export interface CertificateImportRequest { privateKey: string; certificateChain?: string; existingArn?: string; + certificateId?: string; } diff --git a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-fns.ts b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-fns.ts index b423f42db..f2ae23400 100644 --- a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-fns.ts +++ b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-fns.ts @@ -6,6 +6,8 @@ import { request } from "@app/lib/config/request"; import { logger } from "@app/lib/logger"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; 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 { TKmsServiceFactory } from "@app/services/kms/kms-service"; 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 syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined; + const syncOptions = pkiSync.syncOptions as + | { certificateNameSchema?: string; canRemoveCertificates?: boolean } + | undefined; const certificateNameSchema = syncOptions?.certificateNameSchema; if (certificateNameSchema) { @@ -330,13 +334,19 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA cert: string; privateKey: 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 const activeCertificateNames = Object.keys(certificateMap); // 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)) { return; } @@ -349,19 +359,22 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA key: certName, cert, privateKey, - certificateChain + certificateChain, + certificateId }); } }); // 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) - const certificatesToRemove = Object.keys(vaultCertificates).filter( - (vaultCertName) => - isInfisicalManagedCertificate(vaultCertName, pkiSync) && - !activeCertificateNames.includes(vaultCertName) && - !disabledAzureKeyVaultCertificateKeys.includes(vaultCertName) - ); + const certificatesToRemove = canRemoveCertificates + ? Object.keys(vaultCertificates).filter( + (vaultCertName) => + isInfisicalManagedCertificate(vaultCertName, pkiSync) && + !activeCertificateNames.includes(vaultCertName) && + !disabledAzureKeyVaultCertificateKeys.includes(vaultCertName) + ) + : []; // Upload certificates to Azure Key Vault with rate limiting 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); // 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"); + 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) { const failedReasons = failedRemovals.map((failure) => { if (failure.status === "rejected") { diff --git a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-schemas.ts b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-schemas.ts index 4aac20cc0..7e2c875da 100644 --- a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-schemas.ts +++ b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-schemas.ts @@ -50,7 +50,7 @@ export const CreateAzureKeyVaultPkiSyncSchema = z.object({ isAutoSyncEnabled: z.boolean().default(true), destinationConfig: AzureKeyVaultPkiSyncConfigSchema, syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional().default({}), - subscriberId: z.string().optional(), + subscriberId: z.string().nullish(), connectionId: z.string(), projectId: z.string().trim().min(1), certificateIds: z.array(z.string().uuid()).optional() @@ -62,7 +62,7 @@ export const UpdateAzureKeyVaultPkiSyncSchema = z.object({ isAutoSyncEnabled: z.boolean().optional(), destinationConfig: AzureKeyVaultPkiSyncConfigSchema.optional(), syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional(), - subscriberId: z.string().optional(), + subscriberId: z.string().nullish(), connectionId: z.string().optional() }); diff --git a/backend/src/services/pki-sync/pki-sync-fns.ts b/backend/src/services/pki-sync/pki-sync-fns.ts index 7fbcd0773..be424a37d 100644 --- a/backend/src/services/pki-sync/pki-sync-fns.ts +++ b/backend/src/services/pki-sync/pki-sync-fns.ts @@ -4,6 +4,8 @@ import { z, ZodSchema } from "zod"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { BadRequestError } from "@app/lib/errors"; 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 { 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: { appConnectionDAL: Pick; kmsService: Pick; + certificateDAL: TCertificateDALFactory; + certificateSyncDAL: TCertificateSyncDALFactory; } ): Promise<{ uploaded: number; @@ -205,7 +209,12 @@ export const PkiSyncFns = { } case 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); } default: @@ -219,19 +228,33 @@ export const PkiSyncFns = { dependencies: { appConnectionDAL: Pick; kmsService: Pick; + certificateSyncDAL: TCertificateSyncDALFactory; + certificateDAL: TCertificateDALFactory; + certificateMap: TCertificateMap; } ): Promise => { switch (pkiSync.destination) { case PkiSync.AzureKeyVault: { checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault); const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies); - await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames); + await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames, { + certificateSyncDAL: dependencies.certificateSyncDAL, + certificateMap: dependencies.certificateMap + }); break; } case PkiSync.AwsCertificateManager: { checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager); - const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies); - await awsCertificateManagerPkiSync.removeCertificates(pkiSync, certificateNames); + const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({ + appConnectionDAL: dependencies.appConnectionDAL, + kmsService: dependencies.kmsService, + certificateDAL: dependencies.certificateDAL, + certificateSyncDAL: dependencies.certificateSyncDAL + }); + await awsCertificateManagerPkiSync.removeCertificates(pkiSync, certificateNames, { + certificateSyncDAL: dependencies.certificateSyncDAL, + certificateMap: dependencies.certificateMap + }); break; } default: diff --git a/backend/src/services/pki-sync/pki-sync-queue.ts b/backend/src/services/pki-sync/pki-sync-queue.ts index 5889e2210..608162ead 100644 --- a/backend/src/services/pki-sync/pki-sync-queue.ts +++ b/backend/src/services/pki-sync/pki-sync-queue.ts @@ -27,6 +27,7 @@ import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/ce import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; import { getCaCertChain } from "../certificate-authority/certificate-authority-fns"; import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal"; +import { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums"; import { TPkiSyncDALFactory } from "./pki-sync-dal"; import { PkiSyncStatus } from "./pki-sync-enums"; import { PkiSyncError } from "./pki-sync-errors"; @@ -57,23 +58,12 @@ type TPkiSyncQueueFactoryDep = { auditLogService: Pick; projectDAL: TProjectDALFactory; licenseService: Pick; - certificateDAL: Pick< - TCertificateDALFactory, - | "findLatestActiveCertForSubscriber" - | "findAllActiveCertsForSubscriber" - | "findActiveCertificatesByIds" - | "create" - | "findById" - | "find" - >; + certificateDAL: TCertificateDALFactory; certificateBodyDAL: Pick; certificateSecretDAL: Pick; certificateAuthorityDAL: Pick; certificateAuthorityCertDAL: Pick; - certificateSyncDAL: Pick< - TCertificateSyncDALFactory, - "findCertificateIdsByPkiSyncId" | "updateSyncStatus" | "bulkUpdateSyncStatus" - >; + certificateSyncDAL: TCertificateSyncDALFactory; }; type PkiSyncActionJob = Job< @@ -303,7 +293,8 @@ export const pkiSyncQueueFactory = ({ cert: certificatePem, privateKey: certPrivateKey || "", certificateChain, - alternativeNames + alternativeNames, + certificateId: certificate.id }; certificateMetadata.set(certificateName, { @@ -427,7 +418,7 @@ export const pkiSyncQueueFactory = ({ const statusUpdates = Array.from(certificateMetadata.entries()).map(([, metadata]) => ({ pkiSyncId: pkiSync.id, certificateId: metadata.id, - status: "running", + status: CertificateSyncStatus.Running, message: "Syncing certificate to destination" })); @@ -437,7 +428,9 @@ export const pkiSyncQueueFactory = ({ const syncResult = await PkiSyncFns.syncCertificates(pkiSyncWithCredentials, certificateMap, { appConnectionDAL, - kmsService + kmsService, + certificateDAL, + certificateSyncDAL }); logger.info( @@ -462,7 +455,7 @@ export const pkiSyncQueueFactory = ({ postSyncUpdates.push({ pkiSyncId: pkiSync.id, certificateId: metadata.id, - status: "succeeded", + status: CertificateSyncStatus.Succeeded, message: "Certificate successfully synced to destination" }); } @@ -476,7 +469,7 @@ export const pkiSyncQueueFactory = ({ postSyncUpdates[updateIndex] = { pkiSyncId: pkiSync.id, certificateId: metadata.id, - status: "failed", + status: CertificateSyncStatus.Failed, message: `${validationError.error}` }; } @@ -493,7 +486,7 @@ export const pkiSyncQueueFactory = ({ postSyncUpdates[updateIndex] = { pkiSyncId: pkiSync.id, certificateId: metadata.id, - status: "failed", + status: CertificateSyncStatus.Failed, message: `Failed to sync certificate: ${failure.error}` }; } @@ -699,7 +692,10 @@ export const pkiSyncQueueFactory = ({ Object.keys(certificateMap), { appConnectionDAL, - kmsService + kmsService, + certificateSyncDAL, + certificateDAL, + certificateMap } ); diff --git a/backend/src/services/pki-sync/pki-sync-service.ts b/backend/src/services/pki-sync/pki-sync-service.ts index c3bda528e..920b7b866 100644 --- a/backend/src/services/pki-sync/pki-sync-service.ts +++ b/backend/src/services/pki-sync/pki-sync-service.ts @@ -610,6 +610,10 @@ export const pkiSyncServiceFactory = ({ certificateStatus: detail.certificateStatus || undefined, certificateNotBefore: detail.certificateNotBefore || undefined, certificateNotAfter: detail.certificateNotAfter || undefined, + certificateRenewBeforeDays: !detail.certificateRenewedByCertificateId + ? detail.certificateRenewBeforeDays + : undefined, + certificateRenewalError: detail.certificateRenewalError || undefined, pkiSyncName: detail.pkiSyncName || undefined, pkiSyncDestination: detail.pkiSyncDestination || undefined })); diff --git a/backend/src/services/pki-sync/pki-sync-types.ts b/backend/src/services/pki-sync/pki-sync-types.ts index a9e236470..f42f64a1b 100644 --- a/backend/src/services/pki-sync/pki-sync-types.ts +++ b/backend/src/services/pki-sync/pki-sync-types.ts @@ -73,7 +73,7 @@ export type TPkiSyncListItem = TPkiSync & { export type TCertificateMap = Record< string, - { cert: string; privateKey: string; certificateChain?: string; alternativeNames?: string[] } + { cert: string; privateKey: string; certificateChain?: string; alternativeNames?: string[]; certificateId?: string } >; export type TCreatePkiSyncDTO = { @@ -176,6 +176,8 @@ export type TPkiSyncCertificate = { certificateStatus?: string; certificateNotBefore?: Date; certificateNotAfter?: Date; + certificateRenewBeforeDays?: number; + certificateRenewalError?: string; pkiSyncName?: string; pkiSyncDestination?: string; }; diff --git a/backend/src/services/pki-sync/pki-sync-utils.ts b/backend/src/services/pki-sync/pki-sync-utils.ts index 3a62c1d21..3e278de48 100644 --- a/backend/src/services/pki-sync/pki-sync-utils.ts +++ b/backend/src/services/pki-sync/pki-sync-utils.ts @@ -59,7 +59,7 @@ export const triggerAutoSyncForCertificate = async ( } }; -export const replaceCertificateInSyncs = async ( +export const addRenewedCertificateToSyncs = async ( oldCertificateId: string, newCertificateId: string, dependencies: { @@ -77,18 +77,15 @@ export const replaceCertificateInSyncs = async ( return; } - const replacementPromises = pkiSyncIds.map(async (pkiSyncId) => { - await dependencies.certificateSyncDAL.removeCertificates(pkiSyncId, [oldCertificateId], tx); + const addPromises = pkiSyncIds.map(async (pkiSyncId) => { await dependencies.certificateSyncDAL.addCertificates(pkiSyncId, [newCertificateId], tx); }); - await Promise.all(replacementPromises); + await Promise.all(addPromises); - logger.info( - `Successfully replaced certificate ${oldCertificateId} with ${newCertificateId} in ${pkiSyncIds.length} PKI sync(s)` - ); + logger.info(`Successfully added renewed certificate ${newCertificateId} to ${pkiSyncIds.length} PKI sync(s)`); } 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; } }; diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx index c646d6ca2..82b11fa33 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx @@ -71,14 +71,14 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => { isChecked={value} >

- Enable Certificate Removal{" "} + Enable Inactive Certificate Removal{" "}

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.

Disable this option if you intend to manage some certificates manually diff --git a/frontend/src/hooks/api/pkiSyncs/types/common.ts b/frontend/src/hooks/api/pkiSyncs/types/common.ts index db6a6c4db..77dc785ee 100644 --- a/frontend/src/hooks/api/pkiSyncs/types/common.ts +++ b/frontend/src/hooks/api/pkiSyncs/types/common.ts @@ -61,6 +61,8 @@ export type TPkiSyncCertificate = { certificateStatus?: string; certificateNotBefore?: Date; certificateNotAfter?: Date; + certificateRenewBeforeDays?: number; + certificateRenewalError?: string; pkiSyncName?: string; pkiSyncDestination?: string; }; diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncCertificatesSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncCertificatesSection.tsx index 2fd6aad3a..3a313a33d 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncCertificatesSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncCertificatesSection.tsx @@ -1,6 +1,11 @@ import { useState } from "react"; 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 { createNotification } from "@app/components/notifications"; @@ -49,6 +54,18 @@ const getSyncStatusText = (status?: CertificateSyncStatus | null) => { 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) => { const [isManageModalOpen, setIsManageModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -128,11 +145,12 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => { - - - - - + + + + + + @@ -140,6 +158,29 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => { const isExpired = syncCert.certificateNotAfter ? new Date(syncCert.certificateNotAfter) < new Date() : 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( { @@ -162,12 +203,22 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => { fallback="Unknown" /> + -
SAN / CNSerial NumberStatusExpiresActionsSAN / CNStatusSerial NumberSync StatusExpires At
+ + {getCertificateStatusText(isExpired, isRevoked)} + +
- {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)}`; + })()}
@@ -191,7 +242,14 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => { : "Unknown"} + + {hasAutoRenewal && daysUntilRenewal !== null && ( + +
+ +
+
+ )} { */} - + {canRemoveCertificates ? "Enabled" : "Disabled"}