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

View File

@@ -51,6 +51,7 @@ export enum QueueName {
AuditLogPrune = "audit-log-prune",
DailyResourceCleanUp = "daily-resource-cleanup",
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
PkiSyncCleanup = "pki-sync-cleanup",
PkiSubscriber = "pki-subscriber",
TelemetryInstanceStats = "telemtry-self-hosted-stats",
IntegrationSync = "sync-integrations",
@@ -86,6 +87,7 @@ export enum QueueJobs {
AuditLogPrune = "audit-log-prune-job",
DailyResourceCleanUp = "daily-resource-cleanup-job",
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
PkiSyncCleanup = "pki-sync-cleanup-job",
SecWebhook = "secret-webhook-trigger",
TelemetryInstanceStats = "telemetry-self-hosted-stats",
IntegrationSync = "secret-integration-pull",
@@ -151,6 +153,10 @@ export type TQueueJobTypes = {
name: QueueJobs.DailyExpiringPkiItemAlert;
payload: undefined;
};
[QueueName.PkiSyncCleanup]: {
name: QueueJobs.PkiSyncCleanup;
payload: undefined;
};
[QueueName.AuditLogPrune]: {
name: QueueJobs.AuditLogPrune;
payload: undefined;

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 { pkiSubscriberQueueServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-queue";
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 { pkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
import { pkiSyncServiceFactory } from "@app/services/pki-sync/pki-sync-service";
@@ -1906,7 +1907,15 @@ export const registerRoutes = async (
licenseService,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL
certificateSecretDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL
});
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
queueService,
pkiSyncDAL,
pkiSyncQueue
});
const internalCaFns = InternalCertificateAuthorityFns({
@@ -2104,6 +2113,7 @@ export const registerRoutes = async (
await telemetryQueue.startTelemetryCheck();
await telemetryQueue.startAggregatedEventsJob();
await dailyResourceCleanUp.init();
await pkiSyncCleanup.init();
await dailyReminderQueueService.startDailyRemindersJob();
await dailyReminderQueueService.startSecretReminderMigrationJob();
await dailyExpiringPkiItemAlert.startSendingAlerts();

View File

@@ -53,7 +53,14 @@ const PkiSyncSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
isPlatformManagedCredentials: z.boolean().nullable().optional()
})
}),
subscriber: z
.object({
id: z.string(),
name: z.string()
})
.nullable()
.optional()
});
const PkiSyncOptionsSchema = z.object({

View File

@@ -1,5 +1,5 @@
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 { 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 {
...certificateOrm,
countCertificatesInProject,
countCertificatesForPkiSubscriber,
findLatestActiveCertForSubscriber,
findAllActiveCertsForSubscriber
findAllActiveCertsForSubscriber,
findExpiredSyncedCertificates
};
};

View File

@@ -22,8 +22,8 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-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 { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";

View File

@@ -43,7 +43,7 @@ export const AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION = {
connection: AppConnection.AzureKeyVault,
destination: PkiSync.AzureKeyVault,
canImportCertificates: false,
canRemoveCertificates: false,
canRemoveCertificates: true,
defaultCertificateNameSchema: "Infisical-PKI-Sync-{{certificateId}}",
forbiddenCharacters: AZURE_KEY_VAULT_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS,
allowedCharacterPattern: AZURE_KEY_VAULT_CERTIFICATE_NAMING.ALLOWED_CHARACTER_PATTERN,

View File

@@ -329,13 +329,14 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
key: string;
cert: string;
privateKey: string;
certificateChain?: string;
}[] = [];
// 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 }]) => {
Object.entries(certificateMap).forEach(([certName, { cert, privateKey, certificateChain }]) => {
if (disabledAzureKeyVaultCertificateKeys.includes(certName)) {
return;
}
@@ -347,7 +348,8 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
setCertificates.push({
key: certName,
cert,
privateKey
privateKey,
certificateChain
});
}
});
@@ -364,13 +366,26 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
// Upload certificates to Azure Key Vault with rate limiting
const uploadResults = await executeWithConcurrencyLimit(
setCertificates,
async ({ key, cert, privateKey }) => {
async ({ key, cert, privateKey, certificateChain }) => {
try {
// Combine certificate and private key in PEM format for Azure Key Vault
// Azure Key Vault accepts PEM format with both cert and private key
let combinedPem = cert;
// Combine private key, certificate, and certificate chain in PEM format for Azure Key Vault
let combinedPem = "";
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

View File

@@ -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
};
};

View File

@@ -42,6 +42,49 @@ const basePkiSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: PkiSyncF
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 {
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) => {
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) => {
try {
const pkiSyncs = await basePkiSyncQuery({ filter: { subscriberId }, db, tx });
@@ -168,9 +265,42 @@ export const pkiSyncDALFactory = (db: TDbClient) => {
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 {
...pkiSyncOrm,
findByProjectId,
findByProjectIdWithSubscribers,
findBySubscriberId,
findByIdAndProjectId,
findByNameAndProjectId,
@@ -178,6 +308,7 @@ export const pkiSyncDALFactory = (db: TDbClient) => {
findOne,
find,
create,
updateById
updateById,
findPkiSyncsWithExpiredCertificates
};
};

View File

@@ -22,6 +22,9 @@ import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"
import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { getCertificateCredentials } from "../certificate/certificate-fns";
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 { PkiSyncStatus } from "./pki-sync-enums";
import { PkiSyncError } from "./pki-sync-errors";
@@ -58,6 +61,8 @@ type TPkiSyncQueueFactoryDep = {
>;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
};
type PkiSyncActionJob = Job<
@@ -86,7 +91,9 @@ export const pkiSyncQueueFactory = ({
licenseService,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL
certificateSecretDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL
}: TPkiSyncQueueFactoryDep) => {
const appCfg = getConfig();
@@ -207,6 +214,32 @@ export const pkiSyncQueueFactory = ({
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;
const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined;
const certificateNameSchema = syncOptions?.certificateNameSchema;
@@ -223,7 +256,8 @@ export const pkiSyncQueueFactory = ({
certificateMap[certificateName] = {
cert: certificatePem,
privateKey: certPrivateKey || ""
privateKey: certPrivateKey || "",
certificateChain
};
} else {
logger.warn({ certificateId: certificate.id, subscriberId }, "Certificate body not found for certificate");
@@ -649,8 +683,7 @@ export const pkiSyncQueueFactory = ({
break;
}
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled PKI Sync Job ${job.name}`);
throw new Error(`Unhandled PKI Sync Job ${String(job.name)}`);
}
};
@@ -701,8 +734,7 @@ export const pkiSyncQueueFactory = ({
await $handleRemoveCertificatesJob(job as TPkiSyncRemoveCertificatesDTO, pkiSync);
break;
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled PKI Sync Job ${job.name}`);
throw new Error(`Unhandled PKI Sync Job ${String(job.name)}`);
}
} finally {
if (job.name === QueueJobs.PkiSyncSyncCertificates) {

View File

@@ -38,7 +38,10 @@ const getDestinationAppType = (destination: PkiSync): AppConnection => {
};
type TPkiSyncServiceFactoryDep = {
pkiSyncDAL: TPkiSyncDALFactory;
pkiSyncDAL: Pick<
TPkiSyncDALFactory,
"findById" | "findByProjectIdWithSubscribers" | "findByNameAndProjectId" | "create" | "updateById" | "deleteById"
>;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -262,7 +265,10 @@ export const pkiSyncServiceFactory = ({
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({
actor: actor.type,
actorId: actor.id,
@@ -274,8 +280,9 @@ export const pkiSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs);
const pkiSyncs = await pkiSyncDAL.findByProjectId(projectId);
return pkiSyncs as TPkiSync[];
const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId);
return pkiSyncsWithSubscribers as TPkiSync[];
};
const findPkiSyncById = async ({ id, projectId }: TFindPkiSyncByIdDTO, actor: OrgServiceActor) => {
@@ -307,7 +314,12 @@ export const pkiSyncServiceFactory = ({
: 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 (

View File

@@ -49,6 +49,10 @@ export type TPkiSync = {
updatedAt: Date;
isPlatformManagedCredentials?: boolean;
};
subscriber?: {
id: string;
name: string;
} | null;
};
export type TPkiSyncWithCredentials = TPkiSync & {
@@ -66,7 +70,7 @@ export type TPkiSyncListItem = TPkiSync & {
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 = {
name: string;

View File

@@ -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.
- **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**.
![Configure Details](/images/certificate-syncs/azure-key-vault/vault-details.png)

View File

@@ -51,11 +51,6 @@ export const DeletePkiSyncModal = ({ isOpen, onOpenChange, pkiSync, onComplete }
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
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>
/>
);
};

View File

@@ -15,7 +15,7 @@ type 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 triggerImportCertificates = useTriggerPkiSyncImportCertificates();
@@ -24,7 +24,8 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => {
try {
await triggerImportCertificates.mutateAsync({
syncId,
destination
destination,
projectId
});
createNotification({

View File

@@ -15,7 +15,7 @@ type 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 triggerRemoveCertificates = useTriggerPkiSyncRemoveCertificates();
@@ -24,7 +24,8 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => {
try {
await triggerRemoveCertificates.mutateAsync({
syncId,
destination
destination,
projectId
});
createNotification({

View File

@@ -67,9 +67,6 @@ export const PkiSyncReviewFields = () => {
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
<GenericFieldLabel label="Upload Certificates">
<Badge variant="success">Always Enabled</Badge>
</GenericFieldLabel>
{/* Hidden for now - Import certificates functionality disabled
{syncOptions?.canImportCertificates !== undefined && (
<GenericFieldLabel label="Import Certificates">

View File

@@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { PkiSyncStatus } from "@app/hooks/api/pkiSyncs/enums";
import { pkiSyncKeys } from "@app/hooks/api/pkiSyncs/queries";
import {
TCreatePkiSyncDTO,
@@ -69,10 +70,39 @@ export const useTriggerPkiSyncSyncCertificates = () => {
return data;
},
onSuccess: (_, { syncId }) => {
// Invalidate all PKI sync queries since we don't have projectId here
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.all });
queryClient.invalidateQueries({ queryKey: ["pkiSync", syncId] });
onMutate: async ({ syncId, projectId }) => {
await queryClient.cancelQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) });
await queryClient.cancelQueries({ queryKey: pkiSyncKeys.list(projectId) });
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;
},
onSuccess: (_, { syncId }) => {
// Invalidate all PKI sync queries since we don't have projectId here
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.all });
queryClient.invalidateQueries({ queryKey: ["pkiSync", syncId] });
onMutate: async ({ syncId, projectId }) => {
await queryClient.cancelQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) });
await queryClient.cancelQueries({ queryKey: pkiSyncKeys.list(projectId) });
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;
},
onSuccess: (_, { syncId }) => {
// Invalidate all PKI sync queries since we don't have projectId here
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.all });
queryClient.invalidateQueries({ queryKey: ["pkiSync", syncId] });
onMutate: async ({ syncId, projectId }) => {
await queryClient.cancelQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) });
await queryClient.cancelQueries({ queryKey: pkiSyncKeys.list(projectId) });
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);
}
}
});
};

View File

@@ -48,16 +48,19 @@ export type TDeletePkiSyncDTO = {
export type TTriggerPkiSyncSyncCertificatesDTO = {
syncId: string;
destination: PkiSync;
projectId: string;
};
export type TTriggerPkiSyncImportCertificatesDTO = {
syncId: string;
destination: PkiSync;
projectId: string;
};
export type TTriggerPkiSyncRemoveCertificatesDTO = {
syncId: string;
destination: PkiSync;
projectId: string;
};
export * from "./common";

View File

@@ -164,7 +164,7 @@ export const PkiSyncRow = ({
</div>
</Td>
{subscriberId ? (
<PkiSyncTableCell primaryText={subscriberId} secondaryText="PKI Subscriber" />
<PkiSyncTableCell primaryText={pkiSync.subscriber?.name || subscriberId} secondaryText="PKI Subscriber" />
) : (
<Td>
<Tooltip content="The PKI subscriber for this sync has been deleted. Configure a new source or remove this sync.">

View File

@@ -252,7 +252,8 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
try {
await triggerSync.mutateAsync({
syncId: pkiSync.id,
destination: pkiSync.destination
destination: pkiSync.destination,
projectId: pkiSync.projectId
});
createNotification({

View File

@@ -45,7 +45,10 @@ export const PkiSyncsTab = () => {
}, [addSync, handlePopUpOpen, navigateToBase]);
const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs(
currentProject?.id || ""
currentProject?.id || "",
{
refetchInterval: 30000
}
);
if (isPkiSyncsPending)

View File

@@ -89,7 +89,8 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
try {
await triggerSyncMutation.mutateAsync({
syncId: id,
destination
destination,
projectId
});
createNotification({
text: "PKI sync job queued successfully",
@@ -102,7 +103,7 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
type: "error"
});
}
}, [triggerSyncMutation, id, destination]);
}, [triggerSyncMutation, id, destination, projectId]);
const handleToggleAutoSync = useCallback(async () => {
try {

View File

@@ -22,7 +22,7 @@ type Props = {
};
export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => {
const { subscriberId } = pkiSync;
const { subscriberId, subscriber } = pkiSync;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriberId || ""
@@ -65,7 +65,7 @@ export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => {
<div>
<div className="space-y-3">
<GenericFieldLabel label="PKI Subscriber">
{subscriberId || "Deleted"}
{subscriber ? subscriber.name : "Deleted"}
</GenericFieldLabel>
</div>
</div>