Improve deprecated certs invalidation logic

This commit is contained in:
Carlos Monastyrski
2025-09-22 18:19:00 -03:00
parent 82f402513d
commit fb8db77578
24 changed files with 487 additions and 58 deletions
+6
View File
@@ -51,6 +51,7 @@ export enum QueueName {
AuditLogPrune = "audit-log-prune", AuditLogPrune = "audit-log-prune",
DailyResourceCleanUp = "daily-resource-cleanup", DailyResourceCleanUp = "daily-resource-cleanup",
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert", DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
PkiSyncCleanup = "pki-sync-cleanup",
PkiSubscriber = "pki-subscriber", PkiSubscriber = "pki-subscriber",
TelemetryInstanceStats = "telemtry-self-hosted-stats", TelemetryInstanceStats = "telemtry-self-hosted-stats",
IntegrationSync = "sync-integrations", IntegrationSync = "sync-integrations",
@@ -86,6 +87,7 @@ export enum QueueJobs {
AuditLogPrune = "audit-log-prune-job", AuditLogPrune = "audit-log-prune-job",
DailyResourceCleanUp = "daily-resource-cleanup-job", DailyResourceCleanUp = "daily-resource-cleanup-job",
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert", DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
PkiSyncCleanup = "pki-sync-cleanup-job",
SecWebhook = "secret-webhook-trigger", SecWebhook = "secret-webhook-trigger",
TelemetryInstanceStats = "telemetry-self-hosted-stats", TelemetryInstanceStats = "telemetry-self-hosted-stats",
IntegrationSync = "secret-integration-pull", IntegrationSync = "secret-integration-pull",
@@ -151,6 +153,10 @@ export type TQueueJobTypes = {
name: QueueJobs.DailyExpiringPkiItemAlert; name: QueueJobs.DailyExpiringPkiItemAlert;
payload: undefined; payload: undefined;
}; };
[QueueName.PkiSyncCleanup]: {
name: QueueJobs.PkiSyncCleanup;
payload: undefined;
};
[QueueName.AuditLogPrune]: { [QueueName.AuditLogPrune]: {
name: QueueJobs.AuditLogPrune; name: QueueJobs.AuditLogPrune;
payload: undefined; payload: undefined;
+11 -1
View File
@@ -248,6 +248,7 @@ import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-co
import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { pkiSubscriberQueueServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-queue"; import { pkiSubscriberQueueServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-queue";
import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service"; import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
import { pkiSyncCleanupQueueServiceFactory } from "@app/services/pki-sync/pki-sync-cleanup-queue";
import { pkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal"; import { pkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
import { pkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue"; import { pkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
import { pkiSyncServiceFactory } from "@app/services/pki-sync/pki-sync-service"; import { pkiSyncServiceFactory } from "@app/services/pki-sync/pki-sync-service";
@@ -1906,7 +1907,15 @@ export const registerRoutes = async (
licenseService, licenseService,
certificateDAL, certificateDAL,
certificateBodyDAL, certificateBodyDAL,
certificateSecretDAL certificateSecretDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL
});
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
queueService,
pkiSyncDAL,
pkiSyncQueue
}); });
const internalCaFns = InternalCertificateAuthorityFns({ const internalCaFns = InternalCertificateAuthorityFns({
@@ -2104,6 +2113,7 @@ export const registerRoutes = async (
await telemetryQueue.startTelemetryCheck(); await telemetryQueue.startTelemetryCheck();
await telemetryQueue.startAggregatedEventsJob(); await telemetryQueue.startAggregatedEventsJob();
await dailyResourceCleanUp.init(); await dailyResourceCleanUp.init();
await pkiSyncCleanup.init();
await dailyReminderQueueService.startDailyRemindersJob(); await dailyReminderQueueService.startDailyRemindersJob();
await dailyReminderQueueService.startSecretReminderMigrationJob(); await dailyReminderQueueService.startSecretReminderMigrationJob();
await dailyExpiringPkiItemAlert.startSendingAlerts(); await dailyExpiringPkiItemAlert.startSendingAlerts();
@@ -53,7 +53,14 @@ const PkiSyncSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
isPlatformManagedCredentials: z.boolean().nullable().optional() isPlatformManagedCredentials: z.boolean().nullable().optional()
}) }),
subscriber: z
.object({
id: z.string(),
name: z.string()
})
.nullable()
.optional()
}); });
const PkiSyncOptionsSchema = z.object({ const PkiSyncOptionsSchema = z.object({
@@ -1,5 +1,5 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName, TCertificates } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
@@ -93,11 +93,33 @@ export const certificateDALFactory = (db: TDbClient) => {
} }
}; };
const findExpiredSyncedCertificates = async (): Promise<TCertificates[]> => {
try {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
const certs = await db
.replicaNode()(TableName.Certificate)
.where("notAfter", ">=", yesterday)
.where("notAfter", "<", today)
.whereNotNull("pkiSubscriberId");
return certs;
} catch (error) {
throw new DatabaseError({ error, name: "Find expired synced certificates" });
}
};
return { return {
...certificateOrm, ...certificateOrm,
countCertificatesInProject, countCertificatesInProject,
countCertificatesForPkiSubscriber, countCertificatesForPkiSubscriber,
findLatestActiveCertForSubscriber, findLatestActiveCertForSubscriber,
findAllActiveCertsForSubscriber findAllActiveCertsForSubscriber,
findExpiredSyncedCertificates
}; };
}; };
@@ -22,8 +22,8 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal"; import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal"; import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal"; import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue"; import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
@@ -43,7 +43,7 @@ export const AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION = {
connection: AppConnection.AzureKeyVault, connection: AppConnection.AzureKeyVault,
destination: PkiSync.AzureKeyVault, destination: PkiSync.AzureKeyVault,
canImportCertificates: false, canImportCertificates: false,
canRemoveCertificates: false, canRemoveCertificates: true,
defaultCertificateNameSchema: "Infisical-PKI-Sync-{{certificateId}}", defaultCertificateNameSchema: "Infisical-PKI-Sync-{{certificateId}}",
forbiddenCharacters: AZURE_KEY_VAULT_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS, forbiddenCharacters: AZURE_KEY_VAULT_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS,
allowedCharacterPattern: AZURE_KEY_VAULT_CERTIFICATE_NAMING.ALLOWED_CHARACTER_PATTERN, allowedCharacterPattern: AZURE_KEY_VAULT_CERTIFICATE_NAMING.ALLOWED_CHARACTER_PATTERN,
@@ -329,13 +329,14 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
key: string; key: string;
cert: string; cert: string;
privateKey: string; privateKey: string;
certificateChain?: string;
}[] = []; }[] = [];
// 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 }]) => { Object.entries(certificateMap).forEach(([certName, { cert, privateKey, certificateChain }]) => {
if (disabledAzureKeyVaultCertificateKeys.includes(certName)) { if (disabledAzureKeyVaultCertificateKeys.includes(certName)) {
return; return;
} }
@@ -347,7 +348,8 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
setCertificates.push({ setCertificates.push({
key: certName, key: certName,
cert, cert,
privateKey privateKey,
certificateChain
}); });
} }
}); });
@@ -364,13 +366,26 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
// 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(
setCertificates, setCertificates,
async ({ key, cert, privateKey }) => { async ({ key, cert, privateKey, certificateChain }) => {
try { try {
// Combine certificate and private key in PEM format for Azure Key Vault // Combine private key, certificate, and certificate chain in PEM format for Azure Key Vault
// Azure Key Vault accepts PEM format with both cert and private key let combinedPem = "";
let combinedPem = cert;
if (privateKey) { if (privateKey) {
combinedPem = `${privateKey}\n${cert}`; combinedPem = privateKey.trim();
}
if (combinedPem) {
combinedPem = `${combinedPem}\n${cert.trim()}`;
} else {
combinedPem = cert.trim();
}
if (certificateChain) {
const trimmedChain = certificateChain.trim();
if (trimmedChain) {
combinedPem = `${combinedPem}\n${trimmedChain}`;
}
} }
// Convert to base64 for Azure Key Vault import // Convert to base64 for Azure Key Vault import
@@ -0,0 +1,94 @@
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TPkiSyncDALFactory } from "./pki-sync-dal";
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
type TPkiSyncCleanupQueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
pkiSyncDAL: Pick<TPkiSyncDALFactory, "findPkiSyncsWithExpiredCertificates">;
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
};
export type TPkiSyncCleanupQueueServiceFactory = ReturnType<typeof pkiSyncCleanupQueueServiceFactory>;
export const pkiSyncCleanupQueueServiceFactory = ({
queueService,
pkiSyncDAL,
pkiSyncQueue
}: TPkiSyncCleanupQueueServiceFactoryDep) => {
const appCfg = getConfig();
const syncExpiredCertificatesForPkiSyncs = async () => {
try {
const pkiSyncsWithExpiredCerts = await pkiSyncDAL.findPkiSyncsWithExpiredCertificates();
if (pkiSyncsWithExpiredCerts.length === 0) {
logger.info("No PKI syncs found with certificates that expired the previous day");
return;
}
logger.info(
`Found ${pkiSyncsWithExpiredCerts.length} PKI sync(s) with certificates that expired the previous day`
);
// Trigger sync for each PKI sync that has expired certificates
for (const { id: syncId, subscriberId } of pkiSyncsWithExpiredCerts) {
try {
// eslint-disable-next-line no-await-in-loop
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({
syncId
});
logger.info(
`Successfully queued PKI sync ${syncId} for subscriber ${subscriberId} due to expired certificates`
);
} catch (error) {
logger.error(error, `Failed to queue PKI sync ${syncId} for subscriber ${subscriberId}`);
}
}
} catch (error) {
logger.error(error, "Failed to sync expired certificates for PKI syncs");
throw error;
}
};
const init = async () => {
if (appCfg.isSecondaryInstance) {
return;
}
await queueService.stopRepeatableJob(
QueueName.PkiSyncCleanup,
QueueJobs.PkiSyncCleanup,
{ pattern: "0 0 * * *", utc: true },
QueueName.PkiSyncCleanup // just a job id
);
await queueService.startPg<QueueName.PkiSyncCleanup>(
QueueJobs.PkiSyncCleanup,
async () => {
try {
logger.info(`${QueueName.PkiSyncCleanup}: queue task started`);
await syncExpiredCertificatesForPkiSyncs();
logger.info(`${QueueName.PkiSyncCleanup}: queue task completed`);
} catch (error) {
logger.error(error, `${QueueName.PkiSyncCleanup}: PKI sync cleanup failed`);
throw error;
}
},
{
batchSize: 1,
workerCount: 1,
pollingIntervalSeconds: 1
}
);
await queueService.schedulePg(QueueJobs.PkiSyncCleanup, "0 0 * * *", undefined, { tz: "UTC" });
};
return {
init,
syncExpiredCertificatesForPkiSyncs
};
};
+132 -1
View File
@@ -42,6 +42,49 @@ const basePkiSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: PkiSyncF
return query; return query;
}; };
const basePkiSyncWithSubscriberQuery = ({
filter,
db,
tx
}: {
db: TDbClient;
filter?: PkiSyncFindFilter;
tx?: Knex;
}) => {
const query = (tx || db.replicaNode())(TableName.PkiSync)
.leftJoin(TableName.AppConnection, `${TableName.PkiSync}.connectionId`, `${TableName.AppConnection}.id`)
.leftJoin(TableName.PkiSubscriber, `${TableName.PkiSync}.subscriberId`, `${TableName.PkiSubscriber}.id`)
.select(selectAllTableCols(TableName.PkiSync))
.select(
// app connection fields
db.ref("name").withSchema(TableName.AppConnection).as("appConnectionName"),
db.ref("app").withSchema(TableName.AppConnection).as("appConnectionApp"),
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("appConnectionEncryptedCredentials"),
db.ref("orgId").withSchema(TableName.AppConnection).as("appConnectionOrgId"),
db.ref("projectId").withSchema(TableName.AppConnection).as("appConnectionProjectId"),
db.ref("method").withSchema(TableName.AppConnection).as("appConnectionMethod"),
db.ref("description").withSchema(TableName.AppConnection).as("appConnectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("appConnectionVersion"),
db.ref("gatewayId").withSchema(TableName.AppConnection).as("appConnectionGatewayId"),
db.ref("createdAt").withSchema(TableName.AppConnection).as("appConnectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("appConnectionUpdatedAt"),
db
.ref("isPlatformManagedCredentials")
.withSchema(TableName.AppConnection)
.as("appConnectionIsPlatformManagedCredentials"),
// pki subscriber fields
db.ref("id").withSchema(TableName.PkiSubscriber).as("pkiSubscriberId"),
db.ref("name").withSchema(TableName.PkiSubscriber).as("subscriberName")
);
if (filter) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PkiSync, filter)));
}
return query;
};
const expandPkiSync = (pkiSync: Awaited<ReturnType<typeof basePkiSyncQuery>>[number]) => { const expandPkiSync = (pkiSync: Awaited<ReturnType<typeof basePkiSyncQuery>>[number]) => {
const { const {
appConnectionName, appConnectionName,
@@ -84,6 +127,51 @@ const expandPkiSync = (pkiSync: Awaited<ReturnType<typeof basePkiSyncQuery>>[num
}; };
}; };
const expandPkiSyncWithSubscriber = (pkiSync: Awaited<ReturnType<typeof basePkiSyncWithSubscriberQuery>>[number]) => {
const {
appConnectionName,
appConnectionApp,
appConnectionEncryptedCredentials,
appConnectionOrgId,
appConnectionProjectId,
appConnectionMethod,
appConnectionDescription,
appConnectionVersion,
appConnectionGatewayId,
appConnectionCreatedAt,
appConnectionUpdatedAt,
appConnectionIsPlatformManagedCredentials,
pkiSubscriberId,
subscriberName,
...el
} = pkiSync;
return {
...el,
destination: el.destination as PkiSync,
destinationConfig: el.destinationConfig as Record<string, unknown>,
syncOptions: el.syncOptions as Record<string, unknown>,
appConnectionName,
appConnectionApp,
connection: {
id: el.connectionId,
name: appConnectionName,
app: appConnectionApp,
encryptedCredentials: appConnectionEncryptedCredentials,
orgId: appConnectionOrgId,
projectId: appConnectionProjectId,
method: appConnectionMethod,
description: appConnectionDescription,
version: appConnectionVersion,
gatewayId: appConnectionGatewayId,
createdAt: appConnectionCreatedAt,
updatedAt: appConnectionUpdatedAt,
isPlatformManagedCredentials: appConnectionIsPlatformManagedCredentials
},
subscriber: pkiSubscriberId && subscriberName ? { id: pkiSubscriberId, name: subscriberName } : null
};
};
export const pkiSyncDALFactory = (db: TDbClient) => { export const pkiSyncDALFactory = (db: TDbClient) => {
const pkiSyncOrm = ormify(db, TableName.PkiSync); const pkiSyncOrm = ormify(db, TableName.PkiSync);
@@ -96,6 +184,15 @@ export const pkiSyncDALFactory = (db: TDbClient) => {
} }
}; };
const findByProjectIdWithSubscribers = async (projectId: string, tx?: Knex) => {
try {
const pkiSyncs = await basePkiSyncWithSubscriberQuery({ filter: { projectId }, db, tx });
return pkiSyncs.map(expandPkiSyncWithSubscriber);
} catch (error) {
throw new DatabaseError({ error, name: "Find By Project ID With Subscribers - PKI Sync" });
}
};
const findBySubscriberId = async (subscriberId: string, tx?: Knex) => { const findBySubscriberId = async (subscriberId: string, tx?: Knex) => {
try { try {
const pkiSyncs = await basePkiSyncQuery({ filter: { subscriberId }, db, tx }); const pkiSyncs = await basePkiSyncQuery({ filter: { subscriberId }, db, tx });
@@ -168,9 +265,42 @@ export const pkiSyncDALFactory = (db: TDbClient) => {
return expandPkiSync(pkiSync); return expandPkiSync(pkiSync);
}; };
const findPkiSyncsWithExpiredCertificates = async (): Promise<Array<{ id: string; subscriberId: string }>> => {
try {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
const pkiSyncs = (await db
.replicaNode()(TableName.PkiSync)
.select(`${TableName.PkiSync}.id`, `${TableName.PkiSync}.subscriberId`)
.innerJoin(
TableName.Certificate,
`${TableName.PkiSync}.subscriberId`,
`${TableName.Certificate}.pkiSubscriberId`
)
.where(`${TableName.Certificate}.notAfter`, ">=", yesterday)
.where(`${TableName.Certificate}.notAfter`, "<", today)
.whereNotNull(`${TableName.Certificate}.pkiSubscriberId`)
.whereNotNull(`${TableName.PkiSync}.subscriberId`)
.groupBy(`${TableName.PkiSync}.id`, `${TableName.PkiSync}.subscriberId`)) as Array<{
id: string;
subscriberId: string;
}>;
return pkiSyncs;
} catch (error) {
throw new DatabaseError({ error, name: "Find PKI syncs with expired certificates" });
}
};
return { return {
...pkiSyncOrm, ...pkiSyncOrm,
findByProjectId, findByProjectId,
findByProjectIdWithSubscribers,
findBySubscriberId, findBySubscriberId,
findByIdAndProjectId, findByIdAndProjectId,
findByNameAndProjectId, findByNameAndProjectId,
@@ -178,6 +308,7 @@ export const pkiSyncDALFactory = (db: TDbClient) => {
findOne, findOne,
find, find,
create, create,
updateById updateById,
findPkiSyncsWithExpiredCertificates
}; };
}; };
@@ -22,6 +22,9 @@ import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"
import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { getCertificateCredentials } from "../certificate/certificate-fns"; import { getCertificateCredentials } from "../certificate/certificate-fns";
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { getCaCertChain } from "../certificate-authority/certificate-authority-fns";
import { 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";
@@ -58,6 +61,8 @@ type TPkiSyncQueueFactoryDep = {
>; >;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">; certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
}; };
type PkiSyncActionJob = Job< type PkiSyncActionJob = Job<
@@ -86,7 +91,9 @@ export const pkiSyncQueueFactory = ({
licenseService, licenseService,
certificateDAL, certificateDAL,
certificateBodyDAL, certificateBodyDAL,
certificateSecretDAL certificateSecretDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL
}: TPkiSyncQueueFactoryDep) => { }: TPkiSyncQueueFactoryDep) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -207,6 +214,32 @@ export const pkiSyncQueueFactory = ({
certPrivateKey = undefined; certPrivateKey = undefined;
} }
let certificateChain: string | undefined;
try {
if (certBody.encryptedCertificateChain) {
const decryptedCertChain = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificateChain
});
certificateChain = decryptedCertChain.toString();
} else if (certificate.caCertId) {
const { caCert, caCertChain } = await getCaCertChain({
caCertId: certificate.caCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
certificateChain = `${caCert}\n${caCertChain}`.trim();
}
} catch (chainError) {
logger.warn(
{ certificateId: certificate.id, subscriberId, error: chainError },
"Certificate chain not found or could not be decrypted - certificate may be imported or chain was not stored"
);
// Continue without certificate chain
certificateChain = undefined;
}
let certificateName: string; let certificateName: string;
const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined; const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined;
const certificateNameSchema = syncOptions?.certificateNameSchema; const certificateNameSchema = syncOptions?.certificateNameSchema;
@@ -223,7 +256,8 @@ export const pkiSyncQueueFactory = ({
certificateMap[certificateName] = { certificateMap[certificateName] = {
cert: certificatePem, cert: certificatePem,
privateKey: certPrivateKey || "" privateKey: certPrivateKey || "",
certificateChain
}; };
} else { } else {
logger.warn({ certificateId: certificate.id, subscriberId }, "Certificate body not found for certificate"); logger.warn({ certificateId: certificate.id, subscriberId }, "Certificate body not found for certificate");
@@ -649,8 +683,7 @@ export const pkiSyncQueueFactory = ({
break; break;
} }
default: default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled PKI Sync Job ${String(job.name)}`);
throw new Error(`Unhandled PKI Sync Job ${job.name}`);
} }
}; };
@@ -701,8 +734,7 @@ export const pkiSyncQueueFactory = ({
await $handleRemoveCertificatesJob(job as TPkiSyncRemoveCertificatesDTO, pkiSync); await $handleRemoveCertificatesJob(job as TPkiSyncRemoveCertificatesDTO, pkiSync);
break; break;
default: default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled PKI Sync Job ${String(job.name)}`);
throw new Error(`Unhandled PKI Sync Job ${job.name}`);
} }
} finally { } finally {
if (job.name === QueueJobs.PkiSyncSyncCertificates) { if (job.name === QueueJobs.PkiSyncSyncCertificates) {
@@ -38,7 +38,10 @@ const getDestinationAppType = (destination: PkiSync): AppConnection => {
}; };
type TPkiSyncServiceFactoryDep = { type TPkiSyncServiceFactoryDep = {
pkiSyncDAL: TPkiSyncDALFactory; pkiSyncDAL: Pick<
TPkiSyncDALFactory,
"findById" | "findByProjectIdWithSubscribers" | "findByNameAndProjectId" | "create" | "updateById" | "deleteById"
>;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">; pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">; appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -262,7 +265,10 @@ export const pkiSyncServiceFactory = ({
return pkiSyncDAL.deleteById(id); return pkiSyncDAL.deleteById(id);
}; };
const listPkiSyncsByProjectId = async ({ projectId }: TListPkiSyncsByProjectId, actor: OrgServiceActor) => { const listPkiSyncsByProjectId = async (
{ projectId }: TListPkiSyncsByProjectId,
actor: OrgServiceActor
): Promise<TPkiSync[]> => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor: actor.type, actor: actor.type,
actorId: actor.id, actorId: actor.id,
@@ -274,8 +280,9 @@ export const pkiSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs);
const pkiSyncs = await pkiSyncDAL.findByProjectId(projectId); const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId);
return pkiSyncs as TPkiSync[];
return pkiSyncsWithSubscribers as TPkiSync[];
}; };
const findPkiSyncById = async ({ id, projectId }: TFindPkiSyncByIdDTO, actor: OrgServiceActor) => { const findPkiSyncById = async ({ id, projectId }: TFindPkiSyncByIdDTO, actor: OrgServiceActor) => {
@@ -307,7 +314,12 @@ export const pkiSyncServiceFactory = ({
: ProjectPermissionSub.PkiSyncs : ProjectPermissionSub.PkiSyncs
); );
return pkiSync as TPkiSync; const result = {
...pkiSync,
subscriber: findSubscriber ? { id: findSubscriber.id, name: findSubscriber.name } : null
} as TPkiSync;
return result;
}; };
const triggerPkiSyncSyncCertificatesById = async ( const triggerPkiSyncSyncCertificatesById = async (
@@ -49,6 +49,10 @@ export type TPkiSync = {
updatedAt: Date; updatedAt: Date;
isPlatformManagedCredentials?: boolean; isPlatformManagedCredentials?: boolean;
}; };
subscriber?: {
id: string;
name: string;
} | null;
}; };
export type TPkiSyncWithCredentials = TPkiSync & { export type TPkiSyncWithCredentials = TPkiSync & {
@@ -66,7 +70,7 @@ export type TPkiSyncListItem = TPkiSync & {
appConnectionApp: string; appConnectionApp: string;
}; };
export type TCertificateMap = Record<string, { cert: string; privateKey: string }>; export type TCertificateMap = Record<string, { cert: string; privateKey: string; certificateChain?: string }>;
export type TCreatePkiSyncDTO = { export type TCreatePkiSyncDTO = {
name: string; name: string;
@@ -47,6 +47,12 @@ description: "Learn how to configure an Azure Key Vault Certificate Sync for Inf
- **Enable Certificate Removal**: If enabled, Infisical will remove expired certificates from the destination during sync operations. Disable this option if you intend to manage certificate cleanup manually. - **Enable Certificate Removal**: If enabled, Infisical will remove expired certificates from the destination during sync operations. Disable this option if you intend to manage certificate cleanup manually.
- **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}}`.
<Tip>
**Azure Key Vault Soft Delete**: When certificates are removed from Azure Key Vault, they are placed in a soft-deleted state rather than being permanently deleted. This means:
- Subsequent syncs will not re-add these soft-deleted certificates automatically
- To resync removed certificates, you must either manually **purge** them from Azure Key Vault or **recover** them through the Azure portal/CLI
</Tip>
6. Configure the **Details** of your Azure Key Vault Certificate Sync, then click **Next**. 6. Configure the **Details** of your Azure Key Vault Certificate Sync, then click **Next**.
![Configure Details](/images/certificate-syncs/azure-key-vault/vault-details.png) ![Configure Details](/images/certificate-syncs/azure-key-vault/vault-details.png)
@@ -51,11 +51,6 @@ export const DeletePkiSyncModal = ({ isOpen, onOpenChange, pkiSync, onComplete }
title={`Are you sure you want to delete ${name}?`} title={`Are you sure you want to delete ${name}?`}
deleteKey={name} deleteKey={name}
onDeleteApproved={handleDeletePkiSync} onDeleteApproved={handleDeletePkiSync}
> />
<p className="mt-4 text-sm text-bunker-300">
This action will also remove all certificates that were synced by this configuration from
the {PKI_SYNC_MAP[destination].name} destination.
</p>
</DeleteActionModal>
); );
}; };
@@ -15,7 +15,7 @@ type ContentProps = {
}; };
const Content = ({ pkiSync, onComplete }: ContentProps) => { const Content = ({ pkiSync, onComplete }: ContentProps) => {
const { id: syncId, destination } = pkiSync; const { id: syncId, destination, projectId } = pkiSync;
const destinationName = PKI_SYNC_MAP[destination].name; const destinationName = PKI_SYNC_MAP[destination].name;
const triggerImportCertificates = useTriggerPkiSyncImportCertificates(); const triggerImportCertificates = useTriggerPkiSyncImportCertificates();
@@ -24,7 +24,8 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => {
try { try {
await triggerImportCertificates.mutateAsync({ await triggerImportCertificates.mutateAsync({
syncId, syncId,
destination destination,
projectId
}); });
createNotification({ createNotification({
@@ -15,7 +15,7 @@ type ContentProps = {
}; };
const Content = ({ pkiSync, onComplete }: ContentProps) => { const Content = ({ pkiSync, onComplete }: ContentProps) => {
const { id: syncId, destination } = pkiSync; const { id: syncId, destination, projectId } = pkiSync;
const destinationName = PKI_SYNC_MAP[destination].name; const destinationName = PKI_SYNC_MAP[destination].name;
const triggerRemoveCertificates = useTriggerPkiSyncRemoveCertificates(); const triggerRemoveCertificates = useTriggerPkiSyncRemoveCertificates();
@@ -24,7 +24,8 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => {
try { try {
await triggerRemoveCertificates.mutateAsync({ await triggerRemoveCertificates.mutateAsync({
syncId, syncId,
destination destination,
projectId
}); });
createNotification({ createNotification({
@@ -67,9 +67,6 @@ export const PkiSyncReviewFields = () => {
{isAutoSyncEnabled ? "Enabled" : "Disabled"} {isAutoSyncEnabled ? "Enabled" : "Disabled"}
</Badge> </Badge>
</GenericFieldLabel> </GenericFieldLabel>
<GenericFieldLabel label="Upload Certificates">
<Badge variant="success">Always Enabled</Badge>
</GenericFieldLabel>
{/* Hidden for now - Import certificates functionality disabled {/* Hidden for now - Import certificates functionality disabled
{syncOptions?.canImportCertificates !== undefined && ( {syncOptions?.canImportCertificates !== undefined && (
<GenericFieldLabel label="Import Certificates"> <GenericFieldLabel label="Import Certificates">
+100 -12
View File
@@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { PkiSyncStatus } from "@app/hooks/api/pkiSyncs/enums";
import { pkiSyncKeys } from "@app/hooks/api/pkiSyncs/queries"; import { pkiSyncKeys } from "@app/hooks/api/pkiSyncs/queries";
import { import {
TCreatePkiSyncDTO, TCreatePkiSyncDTO,
@@ -69,10 +70,39 @@ export const useTriggerPkiSyncSyncCertificates = () => {
return data; return data;
}, },
onSuccess: (_, { syncId }) => { onMutate: async ({ syncId, projectId }) => {
// Invalidate all PKI sync queries since we don't have projectId here await queryClient.cancelQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) });
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.all }); await queryClient.cancelQueries({ queryKey: pkiSyncKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: ["pkiSync", syncId] });
const previousPkiSync = queryClient.getQueryData(pkiSyncKeys.byId(syncId, projectId));
if (previousPkiSync) {
queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), {
...previousPkiSync,
syncStatus: PkiSyncStatus.Pending
});
}
return { previousPkiSync };
},
onSuccess: (_, { syncId, projectId }) => {
const currentData = queryClient.getQueryData(pkiSyncKeys.byId(syncId, projectId));
if (currentData) {
queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), {
...currentData,
syncStatus: PkiSyncStatus.Pending
});
}
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) });
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.list(projectId) });
}, 2000); // Wait 2 seconds before refetching
},
onError: (_, { syncId, projectId }, context) => {
if (context?.previousPkiSync) {
queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), context.previousPkiSync);
}
} }
}); });
}; };
@@ -85,10 +115,39 @@ export const useTriggerPkiSyncImportCertificates = () => {
return data; return data;
}, },
onSuccess: (_, { syncId }) => { onMutate: async ({ syncId, projectId }) => {
// Invalidate all PKI sync queries since we don't have projectId here await queryClient.cancelQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) });
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.all }); await queryClient.cancelQueries({ queryKey: pkiSyncKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: ["pkiSync", syncId] });
const previousPkiSync = queryClient.getQueryData(pkiSyncKeys.byId(syncId, projectId));
if (previousPkiSync) {
queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), {
...previousPkiSync,
importStatus: PkiSyncStatus.Pending
});
}
return { previousPkiSync };
},
onSuccess: (_, { syncId, projectId }) => {
const currentData = queryClient.getQueryData(pkiSyncKeys.byId(syncId, projectId));
if (currentData) {
queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), {
...currentData,
importStatus: PkiSyncStatus.Pending
});
}
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) });
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.list(projectId) });
}, 2000); // Wait 2 seconds before refetching
},
onError: (_, { syncId, projectId }, context) => {
if (context?.previousPkiSync) {
queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), context.previousPkiSync);
}
} }
}); });
}; };
@@ -103,10 +162,39 @@ export const useTriggerPkiSyncRemoveCertificates = () => {
return data; return data;
}, },
onSuccess: (_, { syncId }) => { onMutate: async ({ syncId, projectId }) => {
// Invalidate all PKI sync queries since we don't have projectId here await queryClient.cancelQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) });
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.all }); await queryClient.cancelQueries({ queryKey: pkiSyncKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: ["pkiSync", syncId] });
const previousPkiSync = queryClient.getQueryData(pkiSyncKeys.byId(syncId, projectId));
if (previousPkiSync) {
queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), {
...previousPkiSync,
removeStatus: PkiSyncStatus.Pending
});
}
return { previousPkiSync };
},
onSuccess: (_, { syncId, projectId }) => {
const currentData = queryClient.getQueryData(pkiSyncKeys.byId(syncId, projectId));
if (currentData) {
queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), {
...currentData,
removeStatus: PkiSyncStatus.Pending
});
}
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) });
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.list(projectId) });
}, 2000); // Wait 2 seconds before refetching
},
onError: (_, { syncId, projectId }, context) => {
if (context?.previousPkiSync) {
queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), context.previousPkiSync);
}
} }
}); });
}; };
@@ -48,16 +48,19 @@ export type TDeletePkiSyncDTO = {
export type TTriggerPkiSyncSyncCertificatesDTO = { export type TTriggerPkiSyncSyncCertificatesDTO = {
syncId: string; syncId: string;
destination: PkiSync; destination: PkiSync;
projectId: string;
}; };
export type TTriggerPkiSyncImportCertificatesDTO = { export type TTriggerPkiSyncImportCertificatesDTO = {
syncId: string; syncId: string;
destination: PkiSync; destination: PkiSync;
projectId: string;
}; };
export type TTriggerPkiSyncRemoveCertificatesDTO = { export type TTriggerPkiSyncRemoveCertificatesDTO = {
syncId: string; syncId: string;
destination: PkiSync; destination: PkiSync;
projectId: string;
}; };
export * from "./common"; export * from "./common";
@@ -164,7 +164,7 @@ export const PkiSyncRow = ({
</div> </div>
</Td> </Td>
{subscriberId ? ( {subscriberId ? (
<PkiSyncTableCell primaryText={subscriberId} secondaryText="PKI Subscriber" /> <PkiSyncTableCell primaryText={pkiSync.subscriber?.name || subscriberId} secondaryText="PKI Subscriber" />
) : ( ) : (
<Td> <Td>
<Tooltip content="The PKI subscriber for this sync has been deleted. Configure a new source or remove this sync."> <Tooltip content="The PKI subscriber for this sync has been deleted. Configure a new source or remove this sync.">
@@ -252,7 +252,8 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
try { try {
await triggerSync.mutateAsync({ await triggerSync.mutateAsync({
syncId: pkiSync.id, syncId: pkiSync.id,
destination: pkiSync.destination destination: pkiSync.destination,
projectId: pkiSync.projectId
}); });
createNotification({ createNotification({
@@ -45,7 +45,10 @@ export const PkiSyncsTab = () => {
}, [addSync, handlePopUpOpen, navigateToBase]); }, [addSync, handlePopUpOpen, navigateToBase]);
const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs( const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs(
currentProject?.id || "" currentProject?.id || "",
{
refetchInterval: 30000
}
); );
if (isPkiSyncsPending) if (isPkiSyncsPending)
@@ -89,7 +89,8 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
try { try {
await triggerSyncMutation.mutateAsync({ await triggerSyncMutation.mutateAsync({
syncId: id, syncId: id,
destination destination,
projectId
}); });
createNotification({ createNotification({
text: "PKI sync job queued successfully", text: "PKI sync job queued successfully",
@@ -102,7 +103,7 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
type: "error" type: "error"
}); });
} }
}, [triggerSyncMutation, id, destination]); }, [triggerSyncMutation, id, destination, projectId]);
const handleToggleAutoSync = useCallback(async () => { const handleToggleAutoSync = useCallback(async () => {
try { try {
@@ -22,7 +22,7 @@ type Props = {
}; };
export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => { export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => {
const { subscriberId } = pkiSync; const { subscriberId, subscriber } = pkiSync;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriberId || "" subscriberId: subscriberId || ""
@@ -65,7 +65,7 @@ export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => {
<div> <div>
<div className="space-y-3"> <div className="space-y-3">
<GenericFieldLabel label="PKI Subscriber"> <GenericFieldLabel label="PKI Subscriber">
{subscriberId || "Deleted"} {subscriber ? subscriber.name : "Deleted"}
</GenericFieldLabel> </GenericFieldLabel>
</div> </div>
</div> </div>