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",
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;
+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 { 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
@@ -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;
};
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;