mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-10 07:58:15 -05:00
Improve deprecated certs invalidation logic
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
94
backend/src/services/pki-sync/pki-sync-cleanup-queue.ts
Normal file
94
backend/src/services/pki-sync/pki-sync-cleanup-queue.ts
Normal 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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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**.
|
||||

|
||||
|
||||
|
||||
@@ -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>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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.">
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -45,7 +45,10 @@ export const PkiSyncsTab = () => {
|
||||
}, [addSync, handlePopUpOpen, navigateToBase]);
|
||||
|
||||
const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs(
|
||||
currentProject?.id || ""
|
||||
currentProject?.id || "",
|
||||
{
|
||||
refetchInterval: 30000
|
||||
}
|
||||
);
|
||||
|
||||
if (isPkiSyncsPending)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user