diff --git a/backend/src/db/migrations/20250910193000_pki-sync.ts b/backend/src/db/migrations/20250910193000_pki-sync.ts
index 3c4fdd4eb..73dc620d9 100644
--- a/backend/src/db/migrations/20250910193000_pki-sync.ts
+++ b/backend/src/db/migrations/20250910193000_pki-sync.ts
@@ -19,7 +19,7 @@ export async function up(knex: Knex): Promise {
t.uuid("subscriberId");
t.foreign("subscriberId").references("id").inTable(TableName.PkiSubscriber).onDelete("SET NULL");
t.uuid("connectionId").notNullable();
- t.foreign("connectionId").references("id").inTable(TableName.AppConnection).onDelete("CASCADE");
+ t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
t.timestamps(true, true, true);
t.string("syncStatus");
t.string("lastSyncJobId");
diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts
index 6b9daa734..d79120256 100644
--- a/backend/src/ee/services/permission/project-permission.ts
+++ b/backend/src/ee/services/permission/project-permission.ts
@@ -256,7 +256,7 @@ export type SecretSyncSubjectFields = {
};
export type PkiSyncSubjectFields = {
- projectId: string;
+ subscriberName: string;
};
export type DynamicSecretSubjectFields = {
@@ -501,7 +501,17 @@ const SecretSyncConditionV2Schema = z
const PkiSyncConditionSchema = z
.object({
- projectId: z.string()
+ subscriberName: z.union([
+ z.string(),
+ z
+ .object({
+ [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
+ [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
+ [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
+ [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
+ })
+ .partial()
+ ])
})
.partial();
diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts
index 159735053..9e3c1e8c0 100644
--- a/backend/src/queue/queue-service.ts
+++ b/backend/src/queue/queue-service.ts
@@ -27,8 +27,7 @@ import { TCreateUserNotificationDTO } from "@app/services/notification/notificat
import {
TQueuePkiSyncImportCertificatesByIdDTO,
TQueuePkiSyncRemoveCertificatesByIdDTO,
- TQueuePkiSyncSyncCertificatesByIdDTO,
- TQueueSendPkiSyncActionFailedNotificationsDTO
+ TQueuePkiSyncSyncCertificatesByIdDTO
} from "@app/services/pki-sync/pki-sync-types";
import {
TFailedIntegrationSyncEmailsPayload,
@@ -110,7 +109,6 @@ export enum QueueJobs {
PkiSyncSyncCertificates = "pki-sync-sync-certificates",
PkiSyncImportCertificates = "pki-sync-import-certificates",
PkiSyncRemoveCertificates = "pki-sync-remove-certificates",
- PkiSyncSendActionFailedNotifications = "pki-sync-send-action-failed-notifications",
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
@@ -242,10 +240,6 @@ export type TQueueJobTypes = {
| {
name: QueueJobs.PkiSyncRemoveCertificates;
payload: TQueuePkiSyncRemoveCertificatesByIdDTO;
- }
- | {
- name: QueueJobs.PkiSyncSendActionFailedNotifications;
- payload: TQueueSendPkiSyncActionFailedNotificationsDTO;
};
[QueueName.ProjectV3Migration]: {
name: QueueJobs.ProjectV3Migration;
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index 42934c41a..0ccf84b6c 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -1844,52 +1844,6 @@ export const registerRoutes = async (
licenseService
});
- const certificateAuthorityQueue = certificateAuthorityQueueFactory({
- certificateAuthorityCrlDAL,
- certificateAuthorityDAL,
- certificateAuthoritySecretDAL,
- certificateDAL,
- projectDAL,
- kmsService,
- queueService,
- pkiSubscriberDAL,
- certificateBodyDAL,
- certificateSecretDAL,
- externalCertificateAuthorityDAL,
- keyStore,
- appConnectionDAL,
- appConnectionService
- });
-
- const internalCertificateAuthorityService = internalCertificateAuthorityServiceFactory({
- certificateAuthorityDAL,
- certificateAuthorityCertDAL,
- certificateAuthoritySecretDAL,
- certificateAuthorityCrlDAL,
- certificateTemplateDAL,
- certificateAuthorityQueue,
- certificateDAL,
- certificateBodyDAL,
- certificateSecretDAL,
- pkiCollectionDAL,
- pkiCollectionItemDAL,
- projectDAL,
- internalCertificateAuthorityDAL,
- kmsService,
- permissionService
- });
-
- const certificateEstService = certificateEstServiceFactory({
- internalCertificateAuthorityService,
- certificateTemplateService,
- certificateTemplateDAL,
- certificateAuthorityCertDAL,
- certificateAuthorityDAL,
- projectDAL,
- kmsService,
- licenseService
- });
-
const kmipService = kmipServiceFactory({
kmipClientDAL,
permissionService,
@@ -1932,6 +1886,71 @@ export const registerRoutes = async (
gatewayV2Service
});
+ const pkiSyncQueue = pkiSyncQueueFactory({
+ queueService,
+ kmsService,
+ appConnectionDAL,
+ keyStore,
+ pkiSyncDAL,
+ auditLogService,
+ projectDAL,
+ licenseService,
+ certificateDAL,
+ certificateBodyDAL,
+ certificateSecretDAL
+ });
+
+ const internalCaFns = InternalCertificateAuthorityFns({
+ certificateAuthorityDAL,
+ certificateAuthorityCertDAL,
+ certificateAuthoritySecretDAL,
+ certificateAuthorityCrlDAL,
+ certificateDAL,
+ certificateBodyDAL,
+ certificateSecretDAL,
+ projectDAL,
+ kmsService,
+ pkiSyncDAL,
+ pkiSyncQueue
+ });
+
+ const certificateAuthorityQueue = certificateAuthorityQueueFactory({
+ certificateAuthorityCrlDAL,
+ certificateAuthorityDAL,
+ certificateAuthoritySecretDAL,
+ certificateDAL,
+ projectDAL,
+ kmsService,
+ queueService,
+ pkiSubscriberDAL,
+ certificateBodyDAL,
+ certificateSecretDAL,
+ externalCertificateAuthorityDAL,
+ keyStore,
+ appConnectionDAL,
+ appConnectionService,
+ pkiSyncDAL,
+ pkiSyncQueue
+ });
+
+ const internalCertificateAuthorityService = internalCertificateAuthorityServiceFactory({
+ certificateAuthorityDAL,
+ certificateAuthorityCertDAL,
+ certificateAuthoritySecretDAL,
+ certificateAuthorityCrlDAL,
+ certificateTemplateDAL,
+ certificateAuthorityQueue,
+ certificateDAL,
+ certificateBodyDAL,
+ certificateSecretDAL,
+ pkiCollectionDAL,
+ pkiCollectionItemDAL,
+ projectDAL,
+ internalCertificateAuthorityDAL,
+ kmsService,
+ permissionService
+ });
+
const certificateAuthorityService = certificateAuthorityServiceFactory({
certificateAuthorityDAL,
permissionService,
@@ -1944,19 +1963,20 @@ export const registerRoutes = async (
certificateSecretDAL,
kmsService,
pkiSubscriberDAL,
- projectDAL
+ projectDAL,
+ pkiSyncDAL,
+ pkiSyncQueue
});
- const internalCaFns = InternalCertificateAuthorityFns({
- certificateAuthorityDAL,
+ const certificateEstService = certificateEstServiceFactory({
+ internalCertificateAuthorityService,
+ certificateTemplateService,
+ certificateTemplateDAL,
certificateAuthorityCertDAL,
- certificateAuthoritySecretDAL,
- certificateAuthorityCrlDAL,
- certificateDAL,
- certificateBodyDAL,
- certificateSecretDAL,
+ certificateAuthorityDAL,
projectDAL,
- kmsService
+ kmsService,
+ licenseService
});
const pkiSubscriberQueue = pkiSubscriberQueueServiceFactory({
@@ -1969,21 +1989,6 @@ export const registerRoutes = async (
internalCaFns
});
- const pkiSyncQueue = pkiSyncQueueFactory({
- queueService,
- kmsService,
- appConnectionDAL,
- keyStore,
- pkiSyncDAL,
- auditLogService,
- projectMembershipDAL,
- projectDAL,
- licenseService,
- certificateDAL,
- certificateBodyDAL,
- certificateSecretDAL
- });
-
const certificateService = certificateServiceFactory({
certificateDAL,
certificateBodyDAL,
diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts
index 64d9d511e..0332d27a9 100644
--- a/backend/src/server/routes/v1/index.ts
+++ b/backend/src/server/routes/v1/index.ts
@@ -148,6 +148,15 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" });
+ await pkiRouter.register(
+ async (pkiSyncRouter) => {
+ await pkiSyncRouter.register(registerPkiSyncRouter);
+ for await (const [destination, router] of Object.entries(PKI_SYNC_REGISTER_ROUTER_MAP)) {
+ await pkiSyncRouter.register(router, { prefix: `/${destination}` });
+ }
+ },
+ { prefix: "/syncs" }
+ );
},
{ prefix: "/pki" }
);
@@ -157,16 +166,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
await server.register(registerWebhookRouter, { prefix: "/webhooks" });
await server.register(registerIdentityRouter, { prefix: "/identities" });
- await server.register(
- async (pkiSyncRouter) => {
- // register generic pki sync endpoints
- await pkiSyncRouter.register(registerPkiSyncRouter);
- for await (const [destination, router] of Object.entries(PKI_SYNC_REGISTER_ROUTER_MAP)) {
- await pkiSyncRouter.register(router, { prefix: `/${destination}` });
- }
- },
- { prefix: "/pki-syncs" }
- );
await server.register(
async (secretSharingRouter) => {
diff --git a/backend/src/server/routes/v1/pki-sync-routers/azure-key-vault-pki-sync-router.ts b/backend/src/server/routes/v1/pki-sync-routers/azure-key-vault-pki-sync-router.ts
index 66e93b2a6..5dbd89e45 100644
--- a/backend/src/server/routes/v1/pki-sync-routers/azure-key-vault-pki-sync-router.ts
+++ b/backend/src/server/routes/v1/pki-sync-routers/azure-key-vault-pki-sync-router.ts
@@ -1,4 +1,5 @@
import {
+ AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION,
AzureKeyVaultPkiSyncSchema,
CreateAzureKeyVaultPkiSyncSchema,
UpdateAzureKeyVaultPkiSyncSchema
@@ -13,5 +14,9 @@ export const registerAzureKeyVaultPkiSyncRouter = async (server: FastifyZodProvi
server,
responseSchema: AzureKeyVaultPkiSyncSchema,
createSchema: CreateAzureKeyVaultPkiSyncSchema,
- updateSchema: UpdateAzureKeyVaultPkiSyncSchema
+ updateSchema: UpdateAzureKeyVaultPkiSyncSchema,
+ syncOptions: {
+ canImportCertificates: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION.canImportCertificates,
+ canRemoveCertificates: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION.canRemoveCertificates
+ }
});
diff --git a/backend/src/server/routes/v1/pki-sync-routers/pki-sync-endpoints.ts b/backend/src/server/routes/v1/pki-sync-routers/pki-sync-endpoints.ts
index 728a5df0d..e70080e53 100644
--- a/backend/src/server/routes/v1/pki-sync-routers/pki-sync-endpoints.ts
+++ b/backend/src/server/routes/v1/pki-sync-routers/pki-sync-endpoints.ts
@@ -13,7 +13,8 @@ export const registerSyncPkiEndpoints = ({
destination,
createSchema,
updateSchema,
- responseSchema
+ responseSchema,
+ syncOptions
}: {
destination: PkiSync;
server: FastifyZodProvider;
@@ -37,6 +38,10 @@ export const registerSyncPkiEndpoints = ({
subscriberId?: string;
}>;
responseSchema: z.ZodTypeAny;
+ syncOptions: {
+ canImportCertificates: boolean;
+ canRemoveCertificates: boolean;
+ };
}) => {
const destinationName = PKI_SYNC_NAME_MAP[destination];
@@ -57,7 +62,7 @@ export const registerSyncPkiEndpoints = ({
200: z.object({ pkiSyncs: responseSchema.array() })
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId }
@@ -93,19 +98,15 @@ export const registerSyncPkiEndpoints = ({
params: z.object({
pkiSyncId: z.string()
}),
- querystring: z.object({
- projectId: z.string().trim().min(1)
- }),
response: {
- 200: z.object({ pkiSync: responseSchema })
+ 200: responseSchema
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
- const { projectId } = req.query;
- const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId, projectId }, req.permission);
+ const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
@@ -119,7 +120,7 @@ export const registerSyncPkiEndpoints = ({
}
});
- return { pkiSync };
+ return pkiSync;
}
});
@@ -135,10 +136,10 @@ export const registerSyncPkiEndpoints = ({
description: `Create a ${destinationName} PKI Sync for the specified project.`,
body: createSchema,
response: {
- 200: z.object({ pkiSync: responseSchema })
+ 200: responseSchema
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const pkiSync = await server.services.pkiSync.createPkiSync({ ...req.body, destination }, req.permission);
@@ -155,7 +156,7 @@ export const registerSyncPkiEndpoints = ({
}
});
- return { pkiSync };
+ return pkiSync;
}
});
@@ -172,27 +173,20 @@ export const registerSyncPkiEndpoints = ({
params: z.object({
pkiSyncId: z.string()
}),
- querystring: z.object({
- projectId: z.string().trim().min(1)
- }),
body: updateSchema,
response: {
- 200: z.object({ pkiSync: responseSchema })
+ 200: responseSchema
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
- const { projectId } = req.query;
- const pkiSync = await server.services.pkiSync.updatePkiSync(
- { ...req.body, id: pkiSyncId, projectId },
- req.permission
- );
+ const pkiSync = await server.services.pkiSync.updatePkiSync({ ...req.body, id: pkiSyncId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
- projectId,
+ projectId: pkiSync.projectId,
event: {
type: EventType.UPDATE_PKI_SYNC,
metadata: {
@@ -202,7 +196,7 @@ export const registerSyncPkiEndpoints = ({
}
});
- return { pkiSync };
+ return pkiSync;
}
});
@@ -219,23 +213,19 @@ export const registerSyncPkiEndpoints = ({
params: z.object({
pkiSyncId: z.string()
}),
- querystring: z.object({
- projectId: z.string().trim().min(1)
- }),
response: {
- 200: z.object({ pkiSync: responseSchema })
+ 200: responseSchema
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
- const { projectId } = req.query;
- const pkiSync = await server.services.pkiSync.deletePkiSync({ id: pkiSyncId, projectId }, req.permission);
+ const pkiSync = await server.services.pkiSync.deletePkiSync({ id: pkiSyncId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
- projectId,
+ projectId: pkiSync.projectId,
event: {
type: EventType.DELETE_PKI_SYNC,
metadata: {
@@ -246,7 +236,7 @@ export const registerSyncPkiEndpoints = ({
}
});
- return { pkiSync };
+ return pkiSync;
}
});
@@ -263,22 +253,17 @@ export const registerSyncPkiEndpoints = ({
params: z.object({
pkiSyncId: z.string()
}),
- querystring: z.object({
- projectId: z.string().trim().min(1)
- }),
response: {
200: z.object({ message: z.string() })
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
- const { projectId } = req.query;
const result = await server.services.pkiSync.triggerPkiSyncSyncCertificatesById(
{
- id: pkiSyncId,
- projectId
+ id: pkiSyncId
},
req.permission
);
@@ -287,42 +272,40 @@ export const registerSyncPkiEndpoints = ({
}
});
- server.route({
- method: "POST",
- url: "/:pkiSyncId/import",
- config: {
- rateLimit: writeLimit
- },
- schema: {
- hide: false,
- tags: [ApiDocsTags.PkiSyncs],
- description: `Import certificates from the specified ${destinationName} PKI Sync destination.`,
- params: z.object({
- pkiSyncId: z.string()
- }),
- querystring: z.object({
- projectId: z.string().trim().min(1)
- }),
- response: {
- 200: z.object({ message: z.string() })
+ // Only register import route if the destination supports it
+ if (syncOptions.canImportCertificates) {
+ server.route({
+ method: "POST",
+ url: "/:pkiSyncId/import",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ hide: false,
+ tags: [ApiDocsTags.PkiSyncs],
+ description: `Import certificates from the specified ${destinationName} PKI Sync destination.`,
+ params: z.object({
+ pkiSyncId: z.string()
+ }),
+ response: {
+ 200: z.object({ message: z.string() })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const { pkiSyncId } = req.params;
+
+ const result = await server.services.pkiSync.triggerPkiSyncImportCertificatesById(
+ {
+ id: pkiSyncId
+ },
+ req.permission
+ );
+
+ return result;
}
- },
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
- handler: async (req) => {
- const { pkiSyncId } = req.params;
- const { projectId } = req.query;
-
- const result = await server.services.pkiSync.triggerPkiSyncImportCertificatesById(
- {
- id: pkiSyncId,
- projectId
- },
- req.permission
- );
-
- return result;
- }
- });
+ });
+ }
server.route({
method: "POST",
@@ -337,22 +320,17 @@ export const registerSyncPkiEndpoints = ({
params: z.object({
pkiSyncId: z.string()
}),
- querystring: z.object({
- projectId: z.string().trim().min(1)
- }),
response: {
200: z.object({ message: z.string() })
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
- const { projectId } = req.query;
const result = await server.services.pkiSync.triggerPkiSyncRemoveCertificatesById(
{
- id: pkiSyncId,
- projectId
+ id: pkiSyncId
},
req.permission
);
diff --git a/backend/src/server/routes/v1/pki-sync-routers/pki-sync-router.ts b/backend/src/server/routes/v1/pki-sync-routers/pki-sync-router.ts
index c426d7d87..5f1980fbc 100644
--- a/backend/src/server/routes/v1/pki-sync-routers/pki-sync-router.ts
+++ b/backend/src/server/routes/v1/pki-sync-routers/pki-sync-router.ts
@@ -61,7 +61,12 @@ const PkiSyncOptionsSchema = z.object({
connection: z.nativeEnum(AppConnection),
destination: z.nativeEnum(PkiSync),
canImportCertificates: z.boolean(),
- canRemoveCertificates: z.boolean()
+ canRemoveCertificates: z.boolean(),
+ defaultCertificateNameSchema: z.string().optional(),
+ forbiddenCharacters: z.string().optional(),
+ allowedCharacterPattern: z.string().optional(),
+ maxCertificateNameLength: z.number().optional(),
+ minCertificateNameLength: z.number().optional()
});
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
@@ -81,7 +86,7 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: () => {
const pkiSyncOptions = server.services.pkiSync.getPkiSyncOptions();
return { pkiSyncOptions };
@@ -105,7 +110,7 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
200: z.object({ pkiSyncs: PkiSyncSchema.array() })
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId },
@@ -142,19 +147,15 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
params: z.object({
pkiSyncId: z.string()
}),
- querystring: z.object({
- projectId: z.string().trim().min(1)
- }),
response: {
- 200: z.object({ pkiSync: PkiSyncSchema })
+ 200: PkiSyncSchema
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
- const { projectId } = req.query;
- const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId, projectId }, req.permission);
+ const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
@@ -168,7 +169,7 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
}
});
- return { pkiSync };
+ return pkiSync;
}
});
};
diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts
index b725e5584..c0a2fdf86 100644
--- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts
+++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts
@@ -23,6 +23,7 @@ import {
} from "@app/services/certificate/certificate-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
+import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-fns";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
@@ -56,6 +57,12 @@ type TAcmeCertificateAuthorityFnsDeps = {
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
pkiSubscriberDAL: Pick;
+ pkiSyncDAL: {
+ find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise>;
+ };
+ pkiSyncQueue: {
+ queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise;
+ };
projectDAL: Pick;
};
@@ -109,7 +116,9 @@ export const AcmeCertificateAuthorityFns = ({
certificateSecretDAL,
kmsService,
projectDAL,
- pkiSubscriberDAL
+ pkiSubscriberDAL,
+ pkiSyncDAL,
+ pkiSyncQueue
}: TAcmeCertificateAuthorityFnsDeps) => {
const createCertificateAuthority = async ({
name,
@@ -524,6 +533,8 @@ export const AcmeCertificateAuthorityFns = ({
tx
);
});
+
+ await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
};
return {
diff --git a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts
index 25c5590eb..00a4fbf3e 100644
--- a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts
+++ b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts
@@ -26,6 +26,7 @@ import {
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { TPkiSubscriberProperties } from "@app/services/pki-subscriber/pki-subscriber-types";
+import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-fns";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
@@ -55,6 +56,12 @@ type TAzureAdCsCertificateAuthorityFnsDeps = {
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
pkiSubscriberDAL: Pick;
+ pkiSyncDAL: {
+ find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise>;
+ };
+ pkiSyncQueue: {
+ queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise;
+ };
projectDAL: Pick;
};
@@ -584,7 +591,9 @@ export const AzureAdCsCertificateAuthorityFns = ({
certificateSecretDAL,
kmsService,
projectDAL,
- pkiSubscriberDAL
+ pkiSubscriberDAL,
+ pkiSyncDAL,
+ pkiSyncQueue
}: TAzureAdCsCertificateAuthorityFnsDeps) => {
const createCertificateAuthority = async ({
name,
@@ -1024,6 +1033,8 @@ export const AzureAdCsCertificateAuthorityFns = ({
);
});
+ await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
+
return {
certificate: certificatePem,
certificateChain: certificateChainPem,
diff --git a/backend/src/services/certificate-authority/certificate-authority-queue.ts b/backend/src/services/certificate-authority/certificate-authority-queue.ts
index 21f7b71e6..6efd995a7 100644
--- a/backend/src/services/certificate-authority/certificate-authority-queue.ts
+++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts
@@ -50,6 +50,12 @@ type TCertificateAuthorityQueueFactoryDep = {
certificateSecretDAL: Pick;
queueService: TQueueServiceFactory;
pkiSubscriberDAL: Pick;
+ pkiSyncDAL: {
+ find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise>;
+ };
+ pkiSyncQueue: {
+ queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise;
+ };
};
export type TCertificateAuthorityQueueFactory = ReturnType;
@@ -68,7 +74,9 @@ export const certificateAuthorityQueueFactory = ({
externalCertificateAuthorityDAL,
certificateBodyDAL,
certificateSecretDAL,
- pkiSubscriberDAL
+ pkiSubscriberDAL,
+ pkiSyncDAL,
+ pkiSyncQueue
}: TCertificateAuthorityQueueFactoryDep) => {
const acmeFns = AcmeCertificateAuthorityFns({
appConnectionDAL,
@@ -80,7 +88,9 @@ export const certificateAuthorityQueueFactory = ({
certificateSecretDAL,
kmsService,
pkiSubscriberDAL,
- projectDAL
+ projectDAL,
+ pkiSyncDAL,
+ pkiSyncQueue
});
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
@@ -93,7 +103,9 @@ export const certificateAuthorityQueueFactory = ({
certificateSecretDAL,
kmsService,
pkiSubscriberDAL,
- projectDAL
+ projectDAL,
+ pkiSyncDAL,
+ pkiSyncQueue
});
// TODO 1: auto-periodic rotation
diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts
index 02c5a488a..197143d73 100644
--- a/backend/src/services/certificate-authority/certificate-authority-service.ts
+++ b/backend/src/services/certificate-authority/certificate-authority-service.ts
@@ -68,6 +68,12 @@ type TCertificateAuthorityServiceFactoryDep = {
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
pkiSubscriberDAL: Pick;
+ pkiSyncDAL: {
+ find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise>;
+ };
+ pkiSyncQueue: {
+ queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise;
+ };
};
export type TCertificateAuthorityServiceFactory = ReturnType;
@@ -84,7 +90,9 @@ export const certificateAuthorityServiceFactory = ({
certificateBodyDAL,
certificateSecretDAL,
kmsService,
- pkiSubscriberDAL
+ pkiSubscriberDAL,
+ pkiSyncDAL,
+ pkiSyncQueue
}: TCertificateAuthorityServiceFactoryDep) => {
const acmeFns = AcmeCertificateAuthorityFns({
appConnectionDAL,
@@ -96,7 +104,9 @@ export const certificateAuthorityServiceFactory = ({
certificateSecretDAL,
kmsService,
pkiSubscriberDAL,
- projectDAL
+ projectDAL,
+ pkiSyncDAL,
+ pkiSyncQueue
});
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
@@ -109,7 +119,9 @@ export const certificateAuthorityServiceFactory = ({
certificateSecretDAL,
kmsService,
pkiSubscriberDAL,
- projectDAL
+ projectDAL,
+ pkiSyncDAL,
+ pkiSyncQueue
});
const createCertificateAuthority = async (
diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts
index 9f1a5e5c2..8dc17b687 100644
--- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts
+++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts
@@ -19,6 +19,7 @@ import {
TAltNameMapping
} from "@app/services/certificate/certificate-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
+import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-fns";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
@@ -51,6 +52,12 @@ type TInternalCertificateAuthorityFnsDeps = {
certificateDAL: Pick;
certificateBodyDAL: Pick;
certificateSecretDAL: Pick;
+ pkiSyncDAL: {
+ find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise>;
+ };
+ pkiSyncQueue: {
+ queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise;
+ };
};
export const InternalCertificateAuthorityFns = ({
@@ -62,7 +69,9 @@ export const InternalCertificateAuthorityFns = ({
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
- certificateSecretDAL
+ certificateSecretDAL,
+ pkiSyncDAL,
+ pkiSyncQueue
}: TInternalCertificateAuthorityFnsDeps) => {
const issueCertificate = async (
subscriber: TPkiSubscribers,
@@ -251,6 +260,8 @@ export const InternalCertificateAuthorityFns = ({
);
});
+ await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
+
return {
certificate: leafCert.toString("pem"),
certificateChain: certificateChainPem,
diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts
index f20f69949..c79ffd8f8 100644
--- a/backend/src/services/certificate/certificate-service.ts
+++ b/backend/src/services/certificate/certificate-service.ts
@@ -11,7 +11,6 @@ import {
} from "@app/ee/services/permission/project-permission";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
-import { logger } from "@app/lib/logger";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
@@ -23,6 +22,7 @@ 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-fns";
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
@@ -79,28 +79,6 @@ export const certificateServiceFactory = ({
pkiSyncDAL,
pkiSyncQueue
}: TCertificateServiceFactoryDep) => {
- /**
- * Trigger auto sync for PKI syncs connected to a PKI subscriber when certificates are issued/revoked/deleted
- */
- const triggerAutoSyncForSubscriber = async (subscriberId: string) => {
- try {
- // Find all PKI syncs that are connected to this subscriber and have auto sync enabled
- const pkiSyncs = await pkiSyncDAL.find({
- subscriberId,
- isAutoSyncEnabled: true
- });
-
- // Queue sync jobs for each auto sync enabled PKI sync
- for (const pkiSync of pkiSyncs) {
- await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id });
- }
- } catch (error) {
- // Don't throw error to avoid breaking the main certificate operation
- // Just log the auto sync failure
- logger.error(error, `Failed to trigger auto sync for subscriber ${subscriberId}:`);
- }
- };
-
/**
* Return details for certificate with serial number [serialNumber]
*/
@@ -190,7 +168,7 @@ export const certificateServiceFactory = ({
// Trigger auto sync for PKI syncs connected to this certificate's subscriber
if (cert.pkiSubscriberId) {
- await triggerAutoSyncForSubscriber(cert.pkiSubscriberId);
+ await triggerAutoSyncForSubscriber(cert.pkiSubscriberId, { pkiSyncDAL, pkiSyncQueue });
}
return {
@@ -259,7 +237,7 @@ export const certificateServiceFactory = ({
// Trigger auto sync for PKI syncs connected to this certificate's subscriber
if (cert.pkiSubscriberId) {
- await triggerAutoSyncForSubscriber(cert.pkiSubscriberId);
+ await triggerAutoSyncForSubscriber(cert.pkiSubscriberId, { pkiSyncDAL, pkiSyncQueue });
}
// Note: External CA revocation handling would go here for supported CA types
diff --git a/backend/src/services/connection-queue/connection-queue-fns.ts b/backend/src/services/connection-queue/connection-queue-fns.ts
new file mode 100644
index 000000000..8c0fba4a2
--- /dev/null
+++ b/backend/src/services/connection-queue/connection-queue-fns.ts
@@ -0,0 +1,129 @@
+/* eslint-disable no-await-in-loop */
+import { AxiosError } from "axios";
+
+import { logger } from "@app/lib/logger";
+
+export type RateLimitConfig = {
+ MAX_CONCURRENT_REQUESTS: number;
+ BASE_DELAY: number;
+ MAX_DELAY: number;
+ MAX_RETRIES: number;
+ RATE_LIMIT_STATUS_CODES: number[];
+};
+
+export type RateLimitContext = {
+ operation: string;
+ identifier?: string;
+ syncId: string;
+};
+
+export type ConcurrencyContext = {
+ operation: string;
+ syncId: string;
+};
+
+export const sleep = (ms: number): Promise =>
+ new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+
+export const createRateLimitErrorChecker =
+ (config: RateLimitConfig) =>
+ (error: unknown): boolean => {
+ if (error instanceof AxiosError) {
+ return (
+ config.RATE_LIMIT_STATUS_CODES.includes(error.response?.status || 0) ||
+ error.message.toLowerCase().includes("rate limit") ||
+ error.message.toLowerCase().includes("throttl")
+ );
+ }
+ return false;
+ };
+
+export const createRateLimitRetry =
+ (config: RateLimitConfig, isRateLimitError: (error: unknown) => boolean) =>
+ async (fn: () => Promise, context: RateLimitContext, retryCount = 0): Promise => {
+ try {
+ return await fn();
+ } catch (error) {
+ if (isRateLimitError(error) && retryCount < config.MAX_RETRIES) {
+ const delay = Math.min(config.BASE_DELAY * 2 ** retryCount, config.MAX_DELAY);
+
+ logger.warn(
+ {
+ syncId: context.syncId,
+ operation: context.operation,
+ identifier: context.identifier,
+ retryCount: retryCount + 1,
+ delayMs: delay,
+ error: error instanceof AxiosError ? error.message : String(error)
+ },
+ "Rate limit hit, retrying with exponential backoff"
+ );
+
+ await sleep(delay);
+ return createRateLimitRetry(config, isRateLimitError)(fn, context, retryCount + 1);
+ }
+
+ throw error;
+ }
+ };
+
+export const createConcurrencyLimitExecutor =
+ (
+ config: RateLimitConfig,
+ withRateLimitRetry: (fn: () => Promise, context: RateLimitContext, retryCount?: number) => Promise
+ ) =>
+ async (
+ items: T[],
+ executor: (item: T) => Promise,
+ context: ConcurrencyContext,
+ concurrencyLimit = config.MAX_CONCURRENT_REQUESTS
+ ): Promise[]> => {
+ const results: PromiseSettledResult[] = [];
+
+ for (let i = 0; i < items.length; i += concurrencyLimit) {
+ const batch = items.slice(i, i + concurrencyLimit);
+
+ logger.debug(
+ {
+ syncId: context.syncId,
+ operation: context.operation,
+ batchStart: i + 1,
+ batchEnd: Math.min(i + concurrencyLimit, items.length),
+ totalItems: items.length
+ },
+ "Processing batch with rate limit protection"
+ );
+
+ const batchPromises = batch.map((item, batchIndex) =>
+ withRateLimitRetry(() => executor(item), {
+ operation: context.operation,
+ identifier: `batch-${i + batchIndex + 1}`,
+ syncId: context.syncId
+ })
+ );
+
+ const batchResults = await Promise.allSettled(batchPromises);
+ results.push(...batchResults);
+
+ if (i + concurrencyLimit < items.length) {
+ await sleep(100);
+ }
+ }
+
+ return results;
+ };
+
+export const createConnectionQueue = (config: RateLimitConfig) => {
+ const isRateLimitError = createRateLimitErrorChecker(config);
+ const withRateLimitRetry = createRateLimitRetry(config, isRateLimitError);
+ const executeWithConcurrencyLimit = createConcurrencyLimitExecutor(config, withRateLimitRetry);
+
+ return {
+ sleep,
+ isRateLimitError,
+ withRateLimitRetry,
+ executeWithConcurrencyLimit
+ };
+};
diff --git a/backend/src/services/connection-queue/index.ts b/backend/src/services/connection-queue/index.ts
new file mode 100644
index 000000000..7e51d42aa
--- /dev/null
+++ b/backend/src/services/connection-queue/index.ts
@@ -0,0 +1 @@
+export * from "./connection-queue-fns";
diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts
index 43250c8c3..3fc5e841a 100644
--- a/backend/src/services/pki-subscriber/pki-subscriber-service.ts
+++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts
@@ -13,7 +13,6 @@ import {
} from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
-import { logger } from "@app/lib/logger";
import { ms } from "@app/lib/ms";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
@@ -39,6 +38,7 @@ import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
+import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-fns";
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
@@ -106,28 +106,6 @@ export const pkiSubscriberServiceFactory = ({
pkiSyncDAL,
pkiSyncQueue
}: TPkiSubscriberServiceFactoryDep) => {
- /**
- * Trigger auto sync for PKI syncs connected to a PKI subscriber when certificates are issued
- */
- const triggerAutoSyncForSubscriber = async (subscriberId: string) => {
- try {
- // Find all PKI syncs that are connected to this subscriber and have auto sync enabled
- const pkiSyncs = await pkiSyncDAL.find({
- subscriberId,
- isAutoSyncEnabled: true
- });
-
- // Queue sync jobs for each auto sync enabled PKI sync
- for (const pkiSync of pkiSyncs) {
- await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id });
- }
- } catch (error) {
- // Don't throw error to avoid breaking the main certificate operation
- // Just log the auto sync failure
- logger.error(error, `Failed to trigger auto sync for subscriber ${subscriberId}:`);
- }
- };
-
const createSubscriber = async ({
name,
commonName,
@@ -446,7 +424,7 @@ export const pkiSubscriberServiceFactory = ({
const result = await internalCaFns.issueCertificate(subscriber, ca);
// Trigger auto sync for PKI syncs connected to this subscriber after certificate issuance
- await triggerAutoSyncForSubscriber(subscriber.id);
+ await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
return result;
}
@@ -707,7 +685,7 @@ export const pkiSubscriberServiceFactory = ({
});
// Trigger auto sync for PKI syncs connected to this subscriber after certificate signing
- await triggerAutoSyncForSubscriber(subscriber.id);
+ await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
return {
certificate: leafCert.toString("pem"),
diff --git a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-fns.ts b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-fns.ts
index f162cdc44..a2da09924 100644
--- a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-fns.ts
+++ b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-fns.ts
@@ -1,24 +1,61 @@
/* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
+import * as crypto from "crypto";
import { request } from "@app/lib/config/request";
import { logger } from "@app/lib/logger";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
+import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
+import { matchesCertificateNameSchema } from "@app/services/pki-sync/pki-sync-fns";
import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types";
import { PkiSync } from "../pki-sync-enums";
import { PkiSyncError } from "../pki-sync-errors";
-import { GetAzureKeyVaultCertificate, TAzureKeyVaultPkiSyncWithCredentials } from "./azure-key-vault-pki-sync-types";
+import { TPkiSyncWithCredentials } from "../pki-sync-types";
+import { GetAzureKeyVaultCertificate, TAzureKeyVaultPkiSyncConfig } from "./azure-key-vault-pki-sync-types";
+
+const AZURE_RATE_LIMIT_CONFIG: RateLimitConfig = {
+ MAX_CONCURRENT_REQUESTS: 10,
+ BASE_DELAY: 1000,
+ MAX_DELAY: 30000,
+ MAX_RETRIES: 3,
+ RATE_LIMIT_STATUS_CODES: [429, 503]
+};
+
+const azureConnectionQueue = createConnectionQueue(AZURE_RATE_LIMIT_CONFIG);
+
+const { withRateLimitRetry, executeWithConcurrencyLimit } = azureConnectionQueue;
+
+const extractCertificateNameFromId = (certificateId: string): string => {
+ return certificateId.substring(certificateId.lastIndexOf("/") + 1);
+};
+
+const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyncWithCredentials): boolean => {
+ const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined;
+ const certificateNameSchema = syncOptions?.certificateNameSchema;
+
+ if (certificateNameSchema) {
+ const environment = "global";
+ return matchesCertificateNameSchema(certificateName, environment, certificateNameSchema);
+ }
+
+ return certificateName.startsWith("Infisical-PKI-Sync-");
+};
export const AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION = {
name: "Azure Key Vault" as const,
connection: AppConnection.AzureKeyVault,
destination: PkiSync.AzureKeyVault,
canImportCertificates: false,
- canRemoveCertificates: true
+ canRemoveCertificates: true,
+ defaultCertificateNameSchema: "Infisical-PKI-Sync-{{certificateId}}",
+ forbiddenCharacters: "!@#$%^&*()+=[]{}|\\:;\"'<>,.?/~` _",
+ allowedCharacterPattern: "^[a-zA-Z0-9-]{1,127}$",
+ maxCertificateNameLength: 127,
+ minCertificateNameLength: 1
};
type TAzureKeyVaultPkiSyncFactoryDeps = {
@@ -26,19 +63,164 @@ type TAzureKeyVaultPkiSyncFactoryDeps = {
kmsService: Pick;
};
+const parseCertificateX509Props = (certPem: string) => {
+ try {
+ const cert = new crypto.X509Certificate(certPem);
+
+ const { subject } = cert;
+
+ const sans = {
+ dns_names: [] as string[],
+ emails: [] as string[],
+ upns: [] as string[]
+ };
+
+ if (cert.subjectAltName) {
+ const sanEntries = cert.subjectAltName.split(", ");
+ for (const entry of sanEntries) {
+ if (entry.startsWith("DNS:")) {
+ sans.dns_names.push(entry.substring(4));
+ } else if (entry.startsWith("email:")) {
+ sans.emails.push(entry.substring(6));
+ } else if (entry.startsWith("othername:UPN:")) {
+ sans.upns.push(entry.substring(14));
+ }
+ }
+ }
+
+ return {
+ subject,
+ sans
+ };
+ } catch (error) {
+ logger.warn(
+ { error: error instanceof Error ? error.message : String(error) },
+ "Failed to parse certificate X.509 properties, using empty values"
+ );
+ return {
+ subject: "",
+ sans: {
+ dns_names: [],
+ emails: [],
+ upns: []
+ }
+ };
+ }
+};
+
+const parseCertificateKeyProps = (certPem: string) => {
+ try {
+ const publicKeyObject = crypto.createPublicKey(certPem);
+ const keyDetails = publicKeyObject.asymmetricKeyDetails;
+
+ if (!keyDetails) {
+ if (publicKeyObject.asymmetricKeyType === "rsa") {
+ const pubKeyStr = publicKeyObject.export({ type: "spki", format: "der" }).toString("hex");
+ const estimatedBits = pubKeyStr.length * 4;
+
+ let keySize = 2048;
+ if (estimatedBits >= 4000) {
+ keySize = 4096;
+ } else if (estimatedBits >= 3000) {
+ keySize = 3072;
+ } else if (estimatedBits >= 2000) {
+ keySize = 2048;
+ } else if (estimatedBits >= 1000) {
+ keySize = 1024;
+ }
+
+ return {
+ kty: "RSA",
+ key_size: keySize
+ };
+ }
+
+ if (publicKeyObject.asymmetricKeyType === "ec") {
+ return {
+ kty: "EC",
+ curve: "P-256"
+ };
+ }
+
+ return {
+ kty: "RSA",
+ key_size: 2048
+ };
+ }
+
+ if (publicKeyObject.asymmetricKeyType === "rsa") {
+ const modulusLength = keyDetails.modulusLength || 2048;
+ return {
+ kty: "RSA",
+ key_size: modulusLength
+ };
+ }
+
+ if (publicKeyObject.asymmetricKeyType === "ec") {
+ const { namedCurve } = keyDetails;
+ let curveName = "P-256";
+
+ switch (namedCurve) {
+ case "prime256v1":
+ case "secp256r1":
+ curveName = "P-256";
+ break;
+ case "secp384r1":
+ curveName = "P-384";
+ break;
+ case "secp521r1":
+ curveName = "P-521";
+ break;
+ default:
+ curveName = "P-256";
+ }
+
+ return {
+ kty: "EC",
+ curve: curveName
+ };
+ }
+
+ const keyType = publicKeyObject.asymmetricKeyType;
+ if (keyType && !["rsa", "ec"].includes(keyType)) {
+ throw new Error(`Unsupported certificate key type: ${keyType}. Azure Key Vault only supports RSA and EC keys.`);
+ }
+
+ logger.warn({ keyType }, "Unable to determine certificate key type, defaulting to RSA 2048");
+ return {
+ kty: "RSA",
+ key_size: 2048
+ };
+ } catch (error) {
+ logger.warn(
+ { error: error instanceof Error ? error.message : String(error) },
+ "Failed to parse certificate key properties, defaulting to RSA 2048"
+ );
+ return {
+ kty: "RSA",
+ key_size: 2048
+ };
+ }
+};
+
export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TAzureKeyVaultPkiSyncFactoryDeps) => {
- const $getAzureKeyVaultCertificates = async (accessToken: string, vaultBaseUrl: string) => {
+ const $getAzureKeyVaultCertificates = async (accessToken: string, vaultBaseUrl: string, syncId = "unknown") => {
const paginateAzureKeyVaultCertificates = async () => {
let result: GetAzureKeyVaultCertificate[] = [];
let currentUrl = `${vaultBaseUrl}/certificates?api-version=7.4`;
while (currentUrl) {
- const res = await request.get<{ value: GetAzureKeyVaultCertificate[]; nextLink: string }>(currentUrl, {
- headers: {
- Authorization: `Bearer ${accessToken}`
- }
- });
+ const urlToFetch = currentUrl; // Capture current URL to avoid loop function issue
+ const res = await withRateLimitRetry(
+ () =>
+ request.get<{ value: GetAzureKeyVaultCertificate[]; nextLink: string }>(urlToFetch, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`
+ }
+ }),
+ { operation: "list-certificates", syncId }
+ );
result = result.concat(res.data.value);
currentUrl = res.data.nextLink;
@@ -54,48 +236,82 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
// disabled certificates to skip sending updates to
const disabledAzureKeyVaultCertificateKeys = getAzureKeyVaultCertificates
.filter(({ attributes }) => !attributes.enabled)
- .map((getAzureKeyVaultCertificate) => {
- return getAzureKeyVaultCertificate.id.substring(getAzureKeyVaultCertificate.id.lastIndexOf("/") + 1);
- });
+ .map((certificate) => extractCertificateNameFromId(certificate.id));
- let lastSlashIndex: number;
- const res = (
- await Promise.all(
- enabledAzureKeyVaultCertificates.map(async (getAzureKeyVaultCertificate) => {
- if (!lastSlashIndex) {
- lastSlashIndex = getAzureKeyVaultCertificate.id.lastIndexOf("/");
- }
-
- const azureKeyVaultCertificate = await request.get(
- `${getAzureKeyVaultCertificate.id}?api-version=7.4`,
- {
- headers: {
- Authorization: `Bearer ${accessToken}`
- }
- }
- );
-
- let certPem = "";
- if (azureKeyVaultCertificate.data.cer) {
- try {
- // Azure Key Vault stores certificate in base64 DER format
- // We need to convert it to PEM format with proper headers
- const base64Cert = azureKeyVaultCertificate.data.cer;
- certPem = `-----BEGIN CERTIFICATE-----\n${base64Cert.match(/.{1,64}/g)?.join("\n")}\n-----END CERTIFICATE-----`;
- } catch (error) {
- certPem = azureKeyVaultCertificate.data.cer;
+ // Use rate-limited concurrent execution for fetching certificate details
+ const certificateResults = await executeWithConcurrencyLimit(
+ enabledAzureKeyVaultCertificates,
+ async (getAzureKeyVaultCertificate) => {
+ const azureKeyVaultCertificate = await request.get(
+ `${getAzureKeyVaultCertificate.id}?api-version=7.4`,
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`
}
}
+ );
- return {
- ...azureKeyVaultCertificate.data,
- key: getAzureKeyVaultCertificate.id.substring(lastSlashIndex + 1),
- cert: certPem,
- privateKey: "" // Private keys cannot be extracted from Azure Key Vault for security reasons
- };
- })
+ let certPem = "";
+ if (azureKeyVaultCertificate.data.cer) {
+ try {
+ // Azure Key Vault stores certificate in base64 DER format
+ // We need to convert it to PEM format with proper headers
+ const base64Cert = azureKeyVaultCertificate.data.cer;
+ const base64Lines = base64Cert.match(/.{1,64}/g);
+ if (!base64Lines) {
+ throw new Error("Failed to format base64 certificate data");
+ }
+ certPem = `-----BEGIN CERTIFICATE-----\n${base64Lines.join("\n")}\n-----END CERTIFICATE-----`;
+ } catch (error) {
+ logger.warn(
+ {
+ error: error instanceof Error ? error.message : String(error),
+ certificateId: getAzureKeyVaultCertificate.id
+ },
+ "Failed to convert Azure Key Vault certificate to PEM format, skipping certificate"
+ );
+ certPem = ""; // Skip this certificate if we can't convert it properly
+ }
+ }
+
+ return {
+ ...azureKeyVaultCertificate.data,
+ key: extractCertificateNameFromId(getAzureKeyVaultCertificate.id),
+ cert: certPem,
+ privateKey: "" // Private keys cannot be extracted from Azure Key Vault for security reasons
+ };
+ },
+ { operation: "fetch-certificate-details", syncId }
+ );
+
+ const successfulCertificates = certificateResults
+ .filter(
+ (
+ result
+ ): result is PromiseFulfilledResult<
+ GetAzureKeyVaultCertificate & {
+ key: string;
+ cert: string;
+ privateKey: string;
+ }
+ > => result.status === "fulfilled"
)
- ).reduce(
+ .map((result) => result.value);
+
+ // Log any failures
+ const failedFetches = certificateResults.filter((result) => result.status === "rejected");
+ if (failedFetches.length > 0) {
+ logger.warn(
+ {
+ syncId,
+ failedCount: failedFetches.length,
+ totalCount: enabledAzureKeyVaultCertificates.length
+ },
+ "Some certificate details could not be fetched from Azure Key Vault"
+ );
+ }
+
+ const res: Record = successfulCertificates.reduce(
(obj, certificate) => ({
...obj,
[certificate.key]: {
@@ -112,30 +328,16 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
};
};
- const syncCertificates = async (pkiSync: TAzureKeyVaultPkiSyncWithCredentials, certificateMap: TCertificateMap) => {
- logger.info(
- {
- syncId: pkiSync.id,
- vaultUrl: pkiSync.destinationConfig.vaultBaseUrl,
- certificateCount: Object.keys(certificateMap).length
- },
- "Starting Azure Key Vault certificate sync"
- );
-
+ const syncCertificates = async (pkiSync: TPkiSyncWithCredentials, certificateMap: TCertificateMap) => {
const { accessToken } = await getAzureConnectionAccessToken(pkiSync.connection.id, appConnectionDAL, kmsService);
+ // Cast destination config to Azure Key Vault config
+ const destinationConfig = pkiSync.destinationConfig as TAzureKeyVaultPkiSyncConfig;
+
const { vaultCertificates, disabledAzureKeyVaultCertificateKeys } = await $getAzureKeyVaultCertificates(
accessToken,
- pkiSync.destinationConfig.vaultBaseUrl
- );
-
- logger.info(
- {
- syncId: pkiSync.id,
- existingCertCount: Object.keys(vaultCertificates).length,
- disabledCertCount: disabledAzureKeyVaultCertificateKeys.length
- },
- "Retrieved existing certificates from Azure Key Vault"
+ destinationConfig.vaultBaseUrl,
+ pkiSync.id
);
const setCertificates: {
@@ -150,10 +352,6 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
// Iterate through certificates to sync to Azure Key Vault
Object.entries(certificateMap).forEach(([certName, { cert, privateKey }]) => {
if (disabledAzureKeyVaultCertificateKeys.includes(certName)) {
- logger.debug(
- { syncId: pkiSync.id, certificateName: certName },
- "Skipping disabled certificate in Azure Key Vault"
- );
return;
}
@@ -166,124 +364,107 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
cert,
privateKey
});
- logger.debug(
- { syncId: pkiSync.id, certificateName: certName, isUpdate: !!existingCert },
- "Certificate will be uploaded to Azure Key Vault"
- );
- } else {
- logger.debug(
- { syncId: pkiSync.id, certificateName: certName },
- "Certificate already up to date in Azure Key Vault"
- );
}
});
// Identify expired/removed certificates that need to be cleaned up from Azure Key Vault
- // Only remove certificates that were managed by Infisical (start with 'Infisical-')
+ // Only remove certificates that were managed by Infisical (match naming schema)
const certificatesToRemove = Object.keys(vaultCertificates).filter(
(vaultCertName) =>
- vaultCertName.startsWith("Infisical-") &&
+ isInfisicalManagedCertificate(vaultCertName, pkiSync) &&
!activeCertificateNames.includes(vaultCertName) &&
!disabledAzureKeyVaultCertificateKeys.includes(vaultCertName)
);
- logger.info(
- {
- syncId: pkiSync.id,
- certificatesToUpload: setCertificates.length,
- certificatesToRemove: certificatesToRemove.length,
- totalCertificates: Object.keys(certificateMap).length
- },
- "Determined certificates to upload and remove from Azure Key Vault"
- );
+ // Upload certificates to Azure Key Vault with rate limiting
+ const uploadResults = await executeWithConcurrencyLimit(
+ setCertificates,
+ async ({ key, cert, privateKey }) => {
+ 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;
+ if (privateKey) {
+ combinedPem = `${privateKey}\n${cert}`;
+ }
- // Upload certificates to Azure Key Vault
- const uploadPromises = setCertificates.map(async ({ key, cert, privateKey }) => {
- 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;
- if (privateKey) {
- combinedPem = `${privateKey}\n${cert}`;
- }
+ // Convert to base64 for Azure Key Vault import
+ const base64Cert = Buffer.from(combinedPem).toString("base64");
- // Convert to base64 for Azure Key Vault import
- const base64Cert = Buffer.from(combinedPem).toString("base64");
+ // Parse certificate to extract X.509 properties and key properties
+ const x509Props = parseCertificateX509Props(cert);
+ const keyProps = parseCertificateKeyProps(cert);
- const importData = {
- value: base64Cert,
- policy: {
- key_props: {
- exportable: true,
- key_size: 2048,
- kty: "RSA",
- reuse_key: false
+ // Build key_props based on key type
+ const keyPropsConfig = {
+ exportable: true,
+ reuse_key: false,
+ ...keyProps
+ };
+
+ const importData = {
+ value: base64Cert,
+ policy: {
+ key_props: keyPropsConfig,
+ secret_props: {
+ contentType: "application/x-pem-file"
+ },
+ x509_props: x509Props
},
- secret_props: {
- contentType: "application/x-pem-file"
- },
- x509_props: {
- subject: "",
- sans: {
- dns_names: [],
- emails: [],
- upns: []
+ attributes: {
+ enabled: true,
+ exportable: true
+ }
+ };
+
+ const response = await request.post(
+ `${destinationConfig.vaultBaseUrl}/certificates/${encodeURIComponent(key)}/import?api-version=7.4`,
+ importData,
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json"
}
}
- }
- };
+ );
- const response = await request.post(
- `${pkiSync.destinationConfig.vaultBaseUrl}/certificates/${encodeURIComponent(key)}/import?api-version=7.4`,
- importData,
- {
- headers: {
- Authorization: `Bearer ${accessToken}`,
- "Content-Type": "application/json"
+ return { key, success: true, response: response.data as unknown };
+ } catch (error) {
+ if (error instanceof AxiosError) {
+ const errorMessage =
+ error.response?.data && typeof error.response.data === "object" && "error" in error.response.data
+ ? (error.response.data as { error?: { message?: string } }).error?.message || error.message
+ : error.message;
+
+ // Check if the error is due to certificate in deleted but recoverable state
+ const isDeletedButRecoverable =
+ errorMessage.includes("deleted but recoverable state") || errorMessage.includes("name cannot be reused");
+
+ if (isDeletedButRecoverable) {
+ logger.warn(
+ { certificateKey: key, syncId: pkiSync.id },
+ "Certificate exists in deleted but recoverable state in Azure Key Vault - skipping upload"
+ );
+ return { key, success: false, skipped: true, reason: "Certificate in deleted but recoverable state" };
}
+
+ throw new PkiSyncError({
+ message: `Failed to upload certificate ${key} to Azure Key Vault: ${errorMessage}`,
+ cause: error,
+ context: {
+ certificateKey: key,
+ statusCode: error.response?.status,
+ responseData: error.response?.data
+ }
+ });
}
- );
-
- logger.info(
- { syncId: pkiSync.id, certificateName: key },
- "Successfully uploaded certificate to Azure Key Vault"
- );
-
- return { key, success: true, response: response.data as unknown };
- } catch (error) {
- if (error instanceof AxiosError) {
- const errorMessage =
- error.response?.data && typeof error.response.data === "object" && "error" in error.response.data
- ? (error.response.data as { error?: { message?: string } }).error?.message || error.message
- : error.message;
-
- // Check if the error is due to certificate in deleted but recoverable state
- const isDeletedButRecoverable =
- errorMessage.includes("deleted but recoverable state") || errorMessage.includes("name cannot be reused");
-
- if (isDeletedButRecoverable) {
- logger.warn(
- { certificateKey: key, syncId: pkiSync.id },
- "Certificate exists in deleted but recoverable state in Azure Key Vault - skipping upload"
- );
- return { key, success: false, skipped: true, reason: "Certificate in deleted but recoverable state" };
- }
-
- throw new PkiSyncError({
- message: `Failed to upload certificate ${key} to Azure Key Vault: ${errorMessage}`,
- cause: error,
- context: {
- certificateKey: key,
- statusCode: error.response?.status,
- responseData: error.response?.data
- }
- });
+ throw error;
}
- throw error;
- }
- });
+ },
+ { operation: "upload-certificates", syncId: pkiSync.id }
+ );
- const results = await Promise.allSettled(uploadPromises);
+ const results = uploadResults;
const failedUploads = results.filter((result) => result.status === "rejected");
const fulfilledResults = results.filter((result) => result.status === "fulfilled");
@@ -298,52 +479,37 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
let failedRemovals = 0;
if (certificatesToRemove.length > 0) {
- logger.info(
- {
- syncId: pkiSync.id,
- certificatesToRemove: certificatesToRemove.length
- },
- "Removing expired/removed certificates from Azure Key Vault"
- );
-
- const removePromises = certificatesToRemove.map(async (certName) => {
- try {
- await request.delete(
- `${pkiSync.destinationConfig.vaultBaseUrl}/certificates/${encodeURIComponent(certName)}?api-version=7.4`,
- {
- headers: {
- Authorization: `Bearer ${accessToken}`
+ const removeResults = await executeWithConcurrencyLimit(
+ certificatesToRemove,
+ async (certName) => {
+ try {
+ await request.delete(
+ `${destinationConfig.vaultBaseUrl}/certificates/${encodeURIComponent(certName)}?api-version=7.4`,
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`
+ }
}
- }
- );
-
- logger.info(
- { syncId: pkiSync.id, certificateName: certName },
- "Successfully removed expired/removed certificate from Azure Key Vault"
- );
-
- return { key: certName, success: true };
- } catch (error) {
- // If certificate doesn't exist (404), consider it as successfully removed
- if (error instanceof AxiosError && error.response?.status === 404) {
- logger.info(
- { syncId: pkiSync.id, certificateName: certName },
- "Certificate not found in Azure Key Vault during sync cleanup - considering removal successful"
);
- return { key: certName, success: true, alreadyRemoved: true };
+
+ return { key: certName, success: true };
+ } catch (error) {
+ // If certificate doesn't exist (404), consider it as successfully removed
+ if (error instanceof AxiosError && error.response?.status === 404) {
+ return { key: certName, success: true, alreadyRemoved: true };
+ }
+
+ logger.error(
+ { error, syncId: pkiSync.id, certificateName: certName },
+ "Failed to remove expired/removed certificate from Azure Key Vault"
+ );
+
+ // Don't throw here - we want to continue with other operations
+ return { key: certName, success: false, error: error as Error };
}
-
- logger.error(
- { error, syncId: pkiSync.id, certificateName: certName },
- "Failed to remove expired/removed certificate from Azure Key Vault"
- );
-
- // Don't throw here - we want to continue with other operations
- return { key: certName, success: false, error: error as Error };
- }
- });
-
- const removeResults = await Promise.allSettled(removePromises);
+ },
+ { operation: "remove-certificates", syncId: pkiSync.id }
+ );
const successfulRemovals = removeResults.filter(
(result) => result.status === "fulfilled" && result.value.success
);
@@ -362,136 +528,124 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
}
}
- // Log skipped certificates for transparency
+ // Collect detailed information for UI feedback
+ const details: {
+ failedUploads?: Array<{ name: string; error: string }>;
+ failedRemovals?: Array<{ name: string; error: string }>;
+ skippedCertificates?: Array<{ name: string; reason: string }>;
+ } = {};
+
+ // Collect skipped certificate details
if (skippedUploads.length > 0) {
- const skippedNames = skippedUploads.map((result) =>
- result.status === "fulfilled" ? result.value.key : "unknown"
- );
- logger.info(
- {
- syncId: pkiSync.id,
- skippedCertificates: skippedNames,
- skippedCount: skippedUploads.length
- },
- "Some certificates were skipped due to Azure Key Vault constraints"
- );
+ details.skippedCertificates = skippedUploads.map((result) => {
+ const certificateName = result.status === "fulfilled" ? result.value.key : "unknown";
+ return {
+ name: certificateName,
+ reason: "Azure Key Vault constraints or certificate already up to date"
+ };
+ });
}
- logger.info(
- {
- syncId: pkiSync.id,
- successfulUploads: successfulUploads.length,
- failedUploads: failedUploads.length,
- skippedUploads: skippedUploads.length,
- removedCertificates,
- failedRemovals,
- skippedCertificates: Object.keys(certificateMap).length - setCertificates.length
- },
- "Azure Key Vault certificate sync completed"
- );
-
+ // Collect failed upload details
if (failedUploads.length > 0) {
- const failedReasons = failedUploads.map((failure) => {
+ details.failedUploads = failedUploads.map((failure, index) => {
+ const certificateName = setCertificates[index]?.key || "unknown";
+ let errorMessage = "Unknown error";
+
if (failure.status === "rejected") {
- return (failure.reason as Error)?.message || "Unknown error";
+ errorMessage = (failure.reason as Error)?.message || "Unknown error";
}
- return "Unknown error";
+
+ return {
+ name: certificateName,
+ error: errorMessage
+ };
});
logger.error(
{
syncId: pkiSync.id,
- failedReasons,
+ failedUploads: details.failedUploads,
failedCount: failedUploads.length
},
"Some certificates failed to upload to Azure Key Vault"
);
-
- throw new PkiSyncError({
- message: `Failed to upload ${failedUploads.length} certificate(s) to Azure Key Vault`,
- context: {
- failedReasons,
- totalCertificates: setCertificates.length,
- failedCount: failedUploads.length
- }
- });
}
- return {
- uploaded: setCertificates.length,
- removed: removedCertificates,
- failedRemovals,
- skipped: Object.keys(certificateMap).length - setCertificates.length
- };
- };
+ // Collect failed removal details
+ if (failedRemovals > 0) {
+ const failedRemovalNames = certificatesToRemove.slice(-failedRemovals);
+ details.failedRemovals = failedRemovalNames.map((certName) => ({
+ name: certName,
+ error: "Failed to remove from Azure Key Vault"
+ }));
- const importCertificates = async (pkiSync: TAzureKeyVaultPkiSyncWithCredentials): Promise => {
- const { accessToken } = await getAzureConnectionAccessToken(pkiSync.connection.id, appConnectionDAL, kmsService);
-
- const { vaultCertificates } = await $getAzureKeyVaultCertificates(
- accessToken,
- pkiSync.destinationConfig.vaultBaseUrl
- );
-
- return vaultCertificates;
- };
-
- const removeCertificates = async (pkiSync: TAzureKeyVaultPkiSyncWithCredentials, certificateNames: string[]) => {
- const { accessToken } = await getAzureConnectionAccessToken(pkiSync.connection.id, appConnectionDAL, kmsService);
-
- // Only remove certificates that are managed by Infisical (start with 'Infisical-' prefix)
- const infisicalManagedCertNames = certificateNames.filter((certName) => certName.startsWith("Infisical-"));
-
- if (infisicalManagedCertNames.length < certificateNames.length) {
- logger.debug(
+ logger.warn(
{
syncId: pkiSync.id,
- totalRequested: certificateNames.length,
- infisicalManaged: infisicalManagedCertNames.length,
- skipped: certificateNames.length - infisicalManagedCertNames.length
+ failedRemovals: details.failedRemovals,
+ successfulRemovals: removedCertificates
},
- "Filtered out non-Infisical certificates from removal request"
+ "Some expired/removed certificates could not be removed from Azure Key Vault"
);
}
- const removePromises = infisicalManagedCertNames.map(async (certName) => {
- try {
- const response = await request.delete(
- `${pkiSync.destinationConfig.vaultBaseUrl}/certificates/${encodeURIComponent(certName)}?api-version=7.4`,
- {
- headers: {
- Authorization: `Bearer ${accessToken}`
- }
- }
- );
+ return {
+ uploaded: successfulUploads.length,
+ removed: removedCertificates,
+ failedRemovals,
+ skipped: Object.keys(certificateMap).length - setCertificates.length,
+ details: Object.keys(details).length > 0 ? details : undefined
+ };
+ };
- return { key: certName, success: true, response: response.data as unknown };
- } catch (error) {
- if (error instanceof AxiosError) {
- // If certificate doesn't exist (404), consider it as successfully removed
- if (error.response?.status === 404) {
- logger.info(
- { syncId: pkiSync.id, certificateName: certName },
- "Certificate not found in Azure Key Vault - considering removal successful"
- );
- return { key: certName, success: true, alreadyRemoved: true };
- }
+ const removeCertificates = async (pkiSync: TPkiSyncWithCredentials, certificateNames: string[]) => {
+ const { accessToken } = await getAzureConnectionAccessToken(pkiSync.connection.id, appConnectionDAL, kmsService);
- throw new PkiSyncError({
- message: `Failed to remove certificate ${certName} from Azure Key Vault`,
- cause: error,
- context: {
- certificateKey: certName,
- statusCode: error.response?.status,
- responseData: error.response?.data
+ // Cast destination config to Azure Key Vault config
+ const destinationConfig = pkiSync.destinationConfig as TAzureKeyVaultPkiSyncConfig;
+
+ // Only remove certificates that are managed by Infisical (match naming schema)
+ const infisicalManagedCertNames = certificateNames.filter((certName) =>
+ isInfisicalManagedCertificate(certName, pkiSync)
+ );
+
+ const results = await executeWithConcurrencyLimit(
+ infisicalManagedCertNames,
+ async (certName) => {
+ try {
+ const response = await request.delete(
+ `${destinationConfig.vaultBaseUrl}/certificates/${encodeURIComponent(certName)}?api-version=7.4`,
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`
+ }
}
- });
+ );
+
+ return { key: certName, success: true, response: response.data as unknown };
+ } catch (error) {
+ if (error instanceof AxiosError) {
+ // If certificate doesn't exist (404), consider it as successfully removed
+ if (error.response?.status === 404) {
+ return { key: certName, success: true, alreadyRemoved: true };
+ }
+
+ throw new PkiSyncError({
+ message: `Failed to remove certificate ${certName} from Azure Key Vault`,
+ cause: error,
+ context: {
+ certificateKey: certName,
+ statusCode: error.response?.status,
+ responseData: error.response?.data
+ }
+ });
+ }
+ throw error;
}
- throw error;
- }
- });
-
- const results = await Promise.allSettled(removePromises);
+ },
+ { operation: "remove-specific-certificates", syncId: pkiSync.id }
+ );
const failedRemovals = results.filter((result) => result.status === "rejected");
if (failedRemovals.length > 0) {
@@ -521,7 +675,6 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
return {
syncCertificates,
- importCertificates,
removeCertificates
};
};
diff --git a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-schemas.ts b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-schemas.ts
index 27a481675..2701556e7 100644
--- a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-schemas.ts
+++ b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-schemas.ts
@@ -1,14 +1,45 @@
+import RE2 from "re2";
import { z } from "zod";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { PkiSyncSchema } from "@app/services/pki-sync/pki-sync-schemas";
-import { AzureKeyVaultPkiSyncConfigSchema } from "./azure-key-vault-pki-sync-types";
+export const AzureKeyVaultPkiSyncConfigSchema = z.object({
+ vaultBaseUrl: z.string().url()
+});
+
+const AzureKeyVaultPkiSyncOptionsSchema = z.object({
+ canImportCertificates: z.boolean().default(false),
+ canRemoveCertificates: z.boolean().default(true),
+ certificateNameSchema: z
+ .string()
+ .optional()
+ .refine(
+ (schema) => {
+ if (!schema) return true;
+
+ const testName = schema
+ .replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "")
+ .replace(new RE2("\\{\\{environment\\}\\}", "g"), "");
+ const azureNamePattern = new RE2("^[a-zA-Z0-9-]{1,127}$");
+
+ const forbiddenChars = "!@#$%^&*()+=[]{}|\\:;\"'<>,.?/~` _";
+ const hasForbiddenChars = forbiddenChars.split("").some((char) => testName.includes(char));
+
+ return azureNamePattern.test(testName) && !hasForbiddenChars;
+ },
+ {
+ message:
+ "Certificate name schema must result in names that contain only alphanumeric characters and hyphens (a-z, A-Z, 0-9, -) and be 1-127 characters long when compiled for Azure Key Vault"
+ }
+ )
+});
export const AzureKeyVaultPkiSyncSchema = PkiSyncSchema.extend({
destination: z.literal(PkiSync.AzureKeyVault),
- destinationConfig: AzureKeyVaultPkiSyncConfigSchema
+ destinationConfig: AzureKeyVaultPkiSyncConfigSchema,
+ syncOptions: AzureKeyVaultPkiSyncOptionsSchema
});
export const CreateAzureKeyVaultPkiSyncSchema = z.object({
@@ -16,7 +47,7 @@ export const CreateAzureKeyVaultPkiSyncSchema = z.object({
description: z.string().optional(),
isAutoSyncEnabled: z.boolean().default(true),
destinationConfig: AzureKeyVaultPkiSyncConfigSchema,
- syncOptions: z.record(z.unknown()).optional().default({}),
+ syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional().default({}),
subscriberId: z.string().optional(),
connectionId: z.string(),
projectId: z.string().trim().min(1)
@@ -27,7 +58,7 @@ export const UpdateAzureKeyVaultPkiSyncSchema = z.object({
description: z.string().optional(),
isAutoSyncEnabled: z.boolean().optional(),
destinationConfig: AzureKeyVaultPkiSyncConfigSchema.optional(),
- syncOptions: z.record(z.unknown()).optional(),
+ syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional(),
subscriberId: z.string().optional(),
connectionId: z.string().optional()
});
diff --git a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-types.ts b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-types.ts
index 4549896f3..2e69534be 100644
--- a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-types.ts
+++ b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-types.ts
@@ -1,6 +1,13 @@
import { z } from "zod";
-import { TPkiSyncWithCredentials } from "../pki-sync-types";
+import { TAzureKeyVaultConnection } from "@app/services/app-connection/azure-key-vault";
+
+import {
+ AzureKeyVaultPkiSyncConfigSchema,
+ AzureKeyVaultPkiSyncSchema,
+ CreateAzureKeyVaultPkiSyncSchema,
+ UpdateAzureKeyVaultPkiSyncSchema
+} from "./azure-key-vault-pki-sync-schemas";
export type GetAzureKeyVaultCertificate = {
id: string;
@@ -18,12 +25,14 @@ export type GetAzureKeyVaultCertificate = {
cer?: string;
};
-export const AzureKeyVaultPkiSyncConfigSchema = z.object({
- vaultBaseUrl: z.string().url()
-});
-
export type TAzureKeyVaultPkiSyncConfig = z.infer;
-export type TAzureKeyVaultPkiSyncWithCredentials = TPkiSyncWithCredentials & {
- destinationConfig: TAzureKeyVaultPkiSyncConfig;
+export type TAzureKeyVaultPkiSync = z.infer;
+
+export type TAzureKeyVaultPkiSyncInput = z.infer;
+
+export type TAzureKeyVaultPkiSyncUpdate = z.infer;
+
+export type TAzureKeyVaultPkiSyncWithCredentials = TAzureKeyVaultPkiSync & {
+ connection: TAzureKeyVaultConnection;
};
diff --git a/backend/src/services/pki-sync/pki-sync-enums.ts b/backend/src/services/pki-sync/pki-sync-enums.ts
index 6382d4784..fadd70914 100644
--- a/backend/src/services/pki-sync/pki-sync-enums.ts
+++ b/backend/src/services/pki-sync/pki-sync-enums.ts
@@ -3,16 +3,10 @@ export enum PkiSync {
}
export enum PkiSyncStatus {
- Pending = "PENDING",
- Running = "RUNNING",
- Success = "SUCCESS",
- Failed = "FAILED"
-}
-
-export enum PkiSyncImportBehavior {
- ImportAllSecrets = "IMPORT_ALL_SECRETS",
- PreferInfisicalSecrets = "PREFER_INFISICAL_SECRETS",
- PreferExternalSecrets = "PREFER_EXTERNAL_SECRETS"
+ Pending = "pending",
+ Running = "running",
+ Succeeded = "succeeded",
+ Failed = "failed"
}
export enum PkiSyncAction {
diff --git a/backend/src/services/pki-sync/pki-sync-fns.ts b/backend/src/services/pki-sync/pki-sync-fns.ts
index 78535e773..24f3039ca 100644
--- a/backend/src/services/pki-sync/pki-sync-fns.ts
+++ b/backend/src/services/pki-sync/pki-sync-fns.ts
@@ -1,11 +1,16 @@
+import * as handlebars from "handlebars";
import { z, ZodSchema } from "zod";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { BadRequestError } from "@app/lib/errors";
+import { logger } from "@app/lib/logger";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
-import { AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION } from "./azure-key-vault/azure-key-vault-pki-sync-fns";
+import {
+ AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION,
+ azureKeyVaultPkiSyncFactory
+} from "./azure-key-vault/azure-key-vault-pki-sync-fns";
import { PkiSync } from "./pki-sync-enums";
import { TCertificateMap, TPkiSyncWithCredentials } from "./pki-sync-types";
@@ -34,6 +39,18 @@ export const listPkiSyncOptions = () => {
return Object.values(PKI_SYNC_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name));
};
+export const getPkiSyncProviderCapabilities = (destination: PkiSync) => {
+ const providerOption = PKI_SYNC_LIST_OPTIONS[destination];
+ if (!providerOption) {
+ throw new BadRequestError({ message: `Unsupported PKI sync destination: ${destination}` });
+ }
+
+ return {
+ canImportCertificates: providerOption.canImportCertificates,
+ canRemoveCertificates: providerOption.canRemoveCertificates
+ };
+};
+
export const matchesSchema = (schema: T, data: unknown): data is z.infer => {
return schema.safeParse(data).success;
};
@@ -50,9 +67,124 @@ export const parsePkiSyncErrorMessage = (error: unknown): string => {
return "An unknown error occurred during PKI sync operation";
};
+export const applyCertificateNameSchema = (
+ certificateMap: TCertificateMap,
+ environment: string,
+ schema?: string
+): TCertificateMap => {
+ if (!schema) return certificateMap;
+
+ const processedCertificateMap: TCertificateMap = {};
+
+ for (const [certificateId, value] of Object.entries(certificateMap)) {
+ const newName = handlebars.compile(schema)({
+ certificateId,
+ environment
+ });
+
+ processedCertificateMap[newName] = value;
+ }
+
+ return processedCertificateMap;
+};
+
+export const stripCertificateNameSchema = (
+ certificateMap: TCertificateMap,
+ environment: string,
+ schema?: string
+): TCertificateMap => {
+ if (!schema) return certificateMap;
+
+ const compiledSchemaPattern = handlebars.compile(schema)({
+ certificateId: "{{certificateId}}",
+ environment
+ });
+
+ const parts = compiledSchemaPattern.split("{{certificateId}}");
+ const prefix = parts[0];
+ const suffix = parts[parts.length - 1];
+
+ const strippedMap: TCertificateMap = {};
+
+ for (const [name, value] of Object.entries(certificateMap)) {
+ if (!name.startsWith(prefix) || !name.endsWith(suffix)) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ const strippedName = name.slice(prefix.length, name.length - suffix.length);
+ strippedMap[strippedName] = value;
+ }
+
+ return strippedMap;
+};
+
+export const matchesCertificateNameSchema = (name: string, environment: string, schema?: string): boolean => {
+ if (!schema) return true;
+
+ const compiledSchemaPattern = handlebars.compile(schema)({
+ certificateId: "{{certificateId}}",
+ environment
+ });
+
+ if (!compiledSchemaPattern.includes("{{certificateId}}")) {
+ return name === compiledSchemaPattern;
+ }
+
+ const parts = compiledSchemaPattern.split("{{certificateId}}");
+ const prefix = parts[0];
+ const suffix = parts[parts.length - 1];
+
+ if (prefix === "" && suffix === "") return true;
+
+ // If prefix is empty, name must end with suffix
+ if (prefix === "") return name.endsWith(suffix);
+
+ // If suffix is empty, name must start with prefix
+ if (suffix === "") return name.startsWith(prefix);
+
+ // Name must start with prefix and end with suffix
+ return name.startsWith(prefix) && name.endsWith(suffix);
+};
+
+const isAzureKeyVaultPkiSync = (pkiSync: TPkiSyncWithCredentials): boolean => {
+ return pkiSync.destination === PkiSync.AzureKeyVault;
+};
+
+/**
+ * Trigger auto sync for PKI syncs connected to a PKI subscriber when certificates are issued/revoked/deleted
+ */
+export const triggerAutoSyncForSubscriber = async (
+ subscriberId: string,
+ dependencies: {
+ pkiSyncDAL: {
+ find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise>;
+ };
+ pkiSyncQueue: {
+ queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise;
+ };
+ }
+) => {
+ try {
+ const pkiSyncs = await dependencies.pkiSyncDAL.find({
+ subscriberId,
+ isAutoSyncEnabled: true
+ });
+
+ // Queue sync jobs for each auto sync enabled PKI sync
+ const syncPromises = pkiSyncs.map((pkiSync) =>
+ dependencies.pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id })
+ );
+ await Promise.all(syncPromises);
+ } catch (error) {
+ logger.error(error, `Failed to trigger auto sync for subscriber ${subscriberId}:`);
+ }
+};
+
export const PkiSyncFns = {
getCertificates: async (
pkiSync: TPkiSyncWithCredentials,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
dependencies: {
appConnectionDAL: Pick;
kmsService: Pick;
@@ -60,11 +192,8 @@ export const PkiSyncFns = {
): Promise => {
switch (pkiSync.destination) {
case PkiSync.AzureKeyVault: {
- const { azureKeyVaultPkiSyncFactory } = await import("./azure-key-vault/azure-key-vault-pki-sync-fns");
- const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies);
- // Type assertion needed due to destinationConfig type differences
- return azureKeyVaultPkiSync.importCertificates(
- pkiSync as unknown as import("./azure-key-vault/azure-key-vault-pki-sync-types").TAzureKeyVaultPkiSyncWithCredentials
+ throw new Error(
+ "Azure Key Vault does not support importing certificates into Infisical (private keys cannot be extracted)"
);
}
default:
@@ -84,16 +213,19 @@ export const PkiSyncFns = {
removed?: number;
failedRemovals?: number;
skipped: number;
+ details?: {
+ failedUploads?: Array<{ name: string; error: string }>;
+ failedRemovals?: Array<{ name: string; error: string }>;
+ skippedCertificates?: Array<{ name: string; reason: string }>;
+ };
}> => {
switch (pkiSync.destination) {
case PkiSync.AzureKeyVault: {
- const { azureKeyVaultPkiSyncFactory } = await import("./azure-key-vault/azure-key-vault-pki-sync-fns");
+ if (!isAzureKeyVaultPkiSync(pkiSync)) {
+ throw new Error("Invalid Azure Key Vault PKI sync configuration");
+ }
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies);
- // Type assertion needed due to destinationConfig type differences
- return azureKeyVaultPkiSync.syncCertificates(
- pkiSync as unknown as import("./azure-key-vault/azure-key-vault-pki-sync-types").TAzureKeyVaultPkiSyncWithCredentials,
- certificateMap
- );
+ return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap);
}
default:
throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`);
@@ -110,13 +242,11 @@ export const PkiSyncFns = {
): Promise => {
switch (pkiSync.destination) {
case PkiSync.AzureKeyVault: {
- const { azureKeyVaultPkiSyncFactory } = await import("./azure-key-vault/azure-key-vault-pki-sync-fns");
+ if (!isAzureKeyVaultPkiSync(pkiSync)) {
+ throw new Error("Invalid Azure Key Vault PKI sync configuration");
+ }
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies);
- // Type assertion needed due to destinationConfig type differences
- await azureKeyVaultPkiSync.removeCertificates(
- pkiSync as unknown as import("./azure-key-vault/azure-key-vault-pki-sync-types").TAzureKeyVaultPkiSyncWithCredentials,
- certificateNames
- );
+ await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames);
break;
}
default:
diff --git a/backend/src/services/pki-sync/pki-sync-queue.ts b/backend/src/services/pki-sync/pki-sync-queue.ts
index 7740047f5..3393aa558 100644
--- a/backend/src/services/pki-sync/pki-sync-queue.ts
+++ b/backend/src/services/pki-sync/pki-sync-queue.ts
@@ -3,8 +3,8 @@ import opentelemetry from "@opentelemetry/api";
import * as x509 from "@peculiar/x509";
import { AxiosError } from "axios";
import { Job } from "bullmq";
+import handlebars from "handlebars";
-import { ProjectMembershipRole } from "@app/db/schemas";
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
@@ -16,20 +16,17 @@ import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
-import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
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 { CertStatus } from "../certificate/certificate-types";
import { TPkiSyncDALFactory } from "./pki-sync-dal";
-import { PkiSyncAction } from "./pki-sync-enums";
+import { PkiSyncStatus } from "./pki-sync-enums";
import { PkiSyncError } from "./pki-sync-errors";
import { enterprisePkiSyncCheck, parsePkiSyncErrorMessage, PkiSyncFns } from "./pki-sync-fns";
import {
- PkiSyncStatus,
TCertificateMap,
TPkiSyncImportCertificatesDTO,
TPkiSyncRaw,
@@ -38,9 +35,7 @@ import {
TPkiSyncWithCredentials,
TQueuePkiSyncImportCertificatesByIdDTO,
TQueuePkiSyncRemoveCertificatesByIdDTO,
- TQueuePkiSyncSyncCertificatesByIdDTO,
- TQueueSendPkiSyncActionFailedNotificationsDTO,
- TSendPkiSyncFailedNotificationsJobDTO
+ TQueuePkiSyncSyncCertificatesByIdDTO
} from "./pki-sync-types";
export type TPkiSyncQueueFactory = ReturnType;
@@ -55,7 +50,6 @@ type TPkiSyncQueueFactoryDep = {
keyStore: Pick;
pkiSyncDAL: Pick;
auditLogService: Pick;
- projectMembershipDAL: Pick;
projectDAL: TProjectDALFactory;
licenseService: Pick;
certificateDAL: Pick<
@@ -88,7 +82,6 @@ export const pkiSyncQueueFactory = ({
keyStore,
pkiSyncDAL,
auditLogService,
- projectMembershipDAL,
projectDAL,
licenseService,
certificateDAL,
@@ -151,89 +144,6 @@ export const pkiSyncQueueFactory = ({
);
};
- const $createCertificatesInSubscriber = async (
- pkiSync: TPkiSyncWithCredentials,
- certificatesToCreate: Array<{
- name: string;
- certificate: string;
- privateKey?: string;
- }>
- ) => {
- const { projectId, subscriberId } = pkiSync;
-
- if (!subscriberId) {
- throw new Error("PKI Sync subscriber ID is required for certificate creation");
- }
-
- logger.info(`Creating ${certificatesToCreate.length} certificates in PKI subscriber ${subscriberId}`);
-
- for (const certData of certificatesToCreate) {
- try {
- // Validate certificate data
- if (!certData.certificate || certData.certificate.trim() === "") {
- logger.error(`Skipping certificate ${certData.name}: empty certificate data`);
- // eslint-disable-next-line no-continue
- continue;
- }
-
- // Parse certificate to extract metadata
- const cert = new x509.X509Certificate(certData.certificate);
- const { serialNumber } = cert;
- const { notBefore } = cert;
- const { notAfter } = cert;
- const commonName =
- cert.subject
- .split(",")
- .find((part) => part.trim().startsWith("CN="))
- ?.split("=")[1]
- ?.trim() || certData.name;
-
- // Get KMS key for encryption
- const kmsKeyId = await getProjectKmsCertificateKeyId({ projectId, projectDAL, kmsService });
- const kmsEncryptor = await kmsService.encryptWithKmsKey({
- kmsId: kmsKeyId
- });
-
- // Create certificate record
- const createdCert = await certificateDAL.create({
- pkiSubscriberId: subscriberId,
- status: CertStatus.ACTIVE,
- serialNumber,
- notBefore,
- notAfter,
- commonName,
- friendlyName: certData.name,
- projectId
- });
-
- // Create certificate body record with encrypted certificate
- const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
- plainText: Buffer.from(certData.certificate)
- });
-
- await certificateBodyDAL.create({
- certId: createdCert.id,
- encryptedCertificate
- });
-
- if (certData.privateKey) {
- const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
- plainText: Buffer.from(certData.privateKey)
- });
-
- await certificateSecretDAL.create({
- certId: createdCert.id,
- encryptedPrivateKey
- });
- }
-
- logger.info(`Successfully created certificate ${certData.name} with ID ${createdCert.id}`);
- } catch (error) {
- logger.error(`Failed to create certificate ${certData.name}: ${String(error)}`);
- }
- }
- };
-
const $getInfisicalCertificates = async (
pkiSync: TPkiSyncRaw | TPkiSyncWithCredentials
): Promise => {
@@ -254,34 +164,8 @@ export const pkiSyncQueueFactory = ({
subscriberId
});
- logger.info(
- { subscriberId, certificateCount: certificates.length },
- "Found active certificates for PKI sync subscriber"
- );
-
for (const certificate of certificates) {
try {
- // Only sync certificates issued by Infisical (not imported ones)
- if (!certificate.caId) {
- logger.debug(
- { certificateId: certificate.id, subscriberId },
- "Skipping imported certificate - not syncing to destination"
- );
- // eslint-disable-next-line no-continue
- continue;
- }
-
- // Check if certificate is expired
- const now = new Date();
- if (certificate.notAfter < now) {
- logger.debug(
- { certificateId: certificate.id, subscriberId, expiredAt: certificate.notAfter },
- "Skipping expired certificate"
- );
- // eslint-disable-next-line no-continue
- continue;
- }
-
// Get the certificate body and decrypt the certificate data
const certBody = await certificateBodyDAL.findOne({ certId: certificate.id });
@@ -323,19 +207,24 @@ export const pkiSyncQueueFactory = ({
certPrivateKey = undefined;
}
- // Use Infisical-prefixed ID for clear identification in destination
- // Azure Key Vault doesn't allow underscores, so use hyphens and remove UUID hyphens
- const certificateName = `Infisical-${certificate.id.replace(/-/g, "")}`;
+ let certificateName: string;
+ const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined;
+ const certificateNameSchema = syncOptions?.certificateNameSchema;
+
+ if (certificateNameSchema) {
+ const environment = "global";
+ certificateName = handlebars.compile(certificateNameSchema)({
+ certificateId: certificate.id.replace(/-/g, ""),
+ environment
+ });
+ } else {
+ certificateName = `Infisical-${certificate.id.replace(/-/g, "")}`;
+ }
certificateMap[certificateName] = {
cert: certificatePem,
privateKey: certPrivateKey || ""
};
-
- logger.info(
- { certificateId: certificate.id, certificateName, subscriberId },
- "Successfully prepared certificate for PKI sync"
- );
} else {
logger.warn({ certificateId: certificate.id, subscriberId }, "Certificate body not found for certificate");
}
@@ -395,83 +284,8 @@ export const pkiSyncQueueFactory = ({
removeOnFail: true
});
- const $queueSendPkiSyncFailedNotifications = async (payload: TQueueSendPkiSyncActionFailedNotificationsDTO) => {
- if (!appCfg.isSmtpConfigured) return;
-
- await queueService.queue(QueueName.PkiSync, QueueJobs.PkiSyncSendActionFailedNotifications, payload, {
- jobId: `pki-sync-${payload.pkiSync.id}-failed-notifications`,
- attempts: 5,
- delay: 1000 * 60,
- backoff: {
- type: "exponential",
- delay: 3000
- },
- removeOnFail: true,
- removeOnComplete: true
- });
- };
-
- const $importCertificates = async (pkiSync: TPkiSyncWithCredentials): Promise => {
- const {
- projectId,
- destination,
- connection: { orgId }
- } = pkiSync;
-
- await enterprisePkiSyncCheck(
- licenseService,
- orgId,
- destination,
- "Failed to import certificates due to plan restriction. Upgrade plan to access enterprise PKI syncs."
- );
-
- if (!projectId) {
- throw new Error("Invalid PKI Sync source configuration: project no longer exists.");
- }
-
- const importedCertificates = await PkiSyncFns.getCertificates(pkiSync, {
- appConnectionDAL,
- kmsService
- });
-
- if (!Object.keys(importedCertificates).length) return {};
-
- const importedCertificateMap: TCertificateMap = {};
-
- const certificateMap = await $getInfisicalCertificates(pkiSync);
-
- // Compare existing certificates with imported ones and determine which need to be created/updated
- const certificatesToCreate: Array<{
- name: string;
- certificate: string;
- privateKey?: string;
- }> = [];
-
- Object.entries(importedCertificates).forEach(([name, certificateData]) => {
- const { cert: certificate, privateKey } = certificateData;
-
- if (!Object.prototype.hasOwnProperty.call(certificateMap, name)) {
- // Certificate doesn't exist in Infisical, create it
- certificatesToCreate.push({
- name,
- certificate,
- privateKey
- });
- importedCertificateMap[name] = certificateData;
- } else {
- // Certificate exists - could compare and update if needed
- // For now, we'll skip updating existing certificates to avoid conflicts
- importedCertificateMap[name] = certificateData;
- }
- });
-
- // Create new certificates in Infisical
- if (certificatesToCreate.length > 0) {
- logger.info(`PKI Sync Import: Creating ${certificatesToCreate.length} new certificates`);
- await $createCertificatesInSubscriber(pkiSync, certificatesToCreate);
- }
-
- return importedCertificateMap;
+ const $importCertificates = async (): Promise => {
+ throw new Error("Certificate import functionality is not implemented");
};
const $handleSyncCertificatesJob = async (job: TPkiSyncSyncCertificatesDTO, pkiSync: TPkiSyncRaw) => {
@@ -586,24 +400,14 @@ export const pkiSyncQueueFactory = ({
});
if (isSynced || isFinalAttempt) {
- const updatedPkiSync = await pkiSyncDAL.updateById(pkiSync.id, {
+ await pkiSyncDAL.updateById(pkiSync.id, {
syncStatus,
lastSyncJobId: job.id,
lastSyncMessage: syncMessage,
lastSyncedAt: isSynced ? ranAt : undefined
});
-
- if (!isSynced) {
- await $queueSendPkiSyncFailedNotifications({
- pkiSync: updatedPkiSync,
- action: PkiSyncAction.SyncCertificates,
- auditLogInfo
- });
- }
}
}
-
- logger.info("PkiSync Sync Job with ID %s Completed", job.id);
};
const $handleImportCertificatesJob = async (job: TPkiSyncImportCertificatesDTO, pkiSync: TPkiSyncRaw) => {
@@ -624,24 +428,7 @@ export const pkiSyncQueueFactory = ({
let isFinalAttempt = job.attemptsStarted === job.opts.attempts;
try {
- const {
- connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId }
- } = pkiSync;
-
- const credentials = await decryptAppConnectionCredentials({
- orgId,
- encryptedCredentials,
- kmsService,
- projectId: appConnectionProjectId
- });
-
- await $importCertificates({
- ...pkiSync,
- connection: {
- ...pkiSync.connection,
- credentials
- }
- } as TPkiSyncWithCredentials);
+ await $importCertificates();
isSuccess = true;
} catch (err) {
@@ -693,24 +480,14 @@ export const pkiSyncQueueFactory = ({
});
if (isSuccess || isFinalAttempt) {
- const updatedPkiSync = await pkiSyncDAL.updateById(pkiSync.id, {
+ await pkiSyncDAL.updateById(pkiSync.id, {
importStatus,
lastImportJobId: job.id,
lastImportMessage: importMessage,
lastImportedAt: isSuccess ? ranAt : undefined
});
-
- if (!isSuccess) {
- await $queueSendPkiSyncFailedNotifications({
- pkiSync: updatedPkiSync,
- action: PkiSyncAction.ImportCertificates,
- auditLogInfo
- });
- }
}
}
-
- logger.info("PkiSync Import Job with ID %s Completed", job.id);
};
const $handleRemoveCertificatesJob = async (job: TPkiSyncRemoveCertificatesDTO, pkiSync: TPkiSyncRaw) => {
@@ -819,72 +596,19 @@ export const pkiSyncQueueFactory = ({
if (isSuccess && deleteSyncOnComplete) {
await pkiSyncDAL.deleteById(pkiSync.id);
} else {
- const updatedPkiSync = await pkiSyncDAL.updateById(pkiSync.id, {
+ await pkiSyncDAL.updateById(pkiSync.id, {
removeStatus,
lastRemoveJobId: job.id,
lastRemoveMessage: removeMessage,
lastRemovedAt: isSuccess ? ranAt : undefined
});
-
- if (!isSuccess) {
- await $queueSendPkiSyncFailedNotifications({
- pkiSync: updatedPkiSync,
- action: PkiSyncAction.RemoveCertificates,
- auditLogInfo
- });
- }
}
}
}
-
- logger.info("PkiSync Remove Job with ID %s Completed", job.id);
- };
-
- const $sendPkiSyncFailedNotifications = async (job: TSendPkiSyncFailedNotificationsJobDTO) => {
- const {
- data: { pkiSync, auditLogInfo, action }
- } = job;
-
- const { projectId, name, lastSyncMessage, lastRemoveMessage, lastImportMessage } = pkiSync;
-
- const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
- const project = await projectDAL.findById(projectId);
-
- // Filter for project admins similar to secret sync
- let projectAdmins = projectMembers.filter((member) =>
- member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
- );
-
- const triggeredByUserId = auditLogInfo?.actor?.type === ActorType.USER ? auditLogInfo.actor.metadata?.userId : null;
-
- if (triggeredByUserId) {
- // Don't send notification to the user who triggered the action
- projectAdmins = projectAdmins.filter((member) => member.user.id !== triggeredByUserId);
- }
-
- // Get appropriate error message based on action type
- let errorMessage: string | null = null;
- if (action === PkiSyncAction.SyncCertificates) {
- errorMessage = lastSyncMessage || null;
- } else if (action === PkiSyncAction.ImportCertificates) {
- errorMessage = lastImportMessage || null;
- } else {
- errorMessage = lastRemoveMessage || null;
- }
-
- if (projectAdmins.length > 0) {
- logger.info(
- `PKI Sync ${action} failure notification would be sent to ${projectAdmins.length} admin(s) for sync "${name}" in project "${project.name}". Error: ${errorMessage}`
- );
- } else {
- logger.info(
- `PKI Sync ${action} failure occurred for sync "${name}" in project "${project.name}" but no admins to notify. Error: ${errorMessage}`
- );
- }
};
const $handleAcquireLockFailure = async (job: PkiSyncActionJob) => {
- const { syncId, auditLogInfo } = job.data;
+ const { syncId } = job.data;
switch (job.name) {
case QueueJobs.PkiSyncSyncCertificates: {
@@ -895,51 +619,33 @@ export const pkiSyncQueueFactory = ({
return;
}
- const pkiSync = await pkiSyncDAL.updateById(syncId, {
+ await pkiSyncDAL.updateById(syncId, {
syncStatus: PkiSyncStatus.Failed,
lastSyncMessage:
"Failed to run job. This typically happens when a sync is already in progress. Please try again.",
lastSyncJobId: job.id
});
- await $queueSendPkiSyncFailedNotifications({
- pkiSync,
- action: PkiSyncAction.SyncCertificates,
- auditLogInfo
- });
-
break;
}
case QueueJobs.PkiSyncImportCertificates: {
- const pkiSync = await pkiSyncDAL.updateById(syncId, {
+ await pkiSyncDAL.updateById(syncId, {
importStatus: PkiSyncStatus.Failed,
lastImportMessage:
"Failed to run job. This typically happens when a sync is already in progress. Please try again.",
lastImportJobId: job.id
});
- await $queueSendPkiSyncFailedNotifications({
- pkiSync,
- action: PkiSyncAction.ImportCertificates,
- auditLogInfo
- });
-
break;
}
case QueueJobs.PkiSyncRemoveCertificates: {
- const pkiSync = await pkiSyncDAL.updateById(syncId, {
+ await pkiSyncDAL.updateById(syncId, {
removeStatus: PkiSyncStatus.Failed,
lastRemoveMessage:
"Failed to run job. This typically happens when a sync is already in progress. Please try again.",
lastRemoveJobId: job.id
});
- await $queueSendPkiSyncFailedNotifications({
- pkiSync,
- action: PkiSyncAction.RemoveCertificates,
- auditLogInfo
- });
-
break;
}
default:
@@ -949,15 +655,7 @@ export const pkiSyncQueueFactory = ({
};
queueService.start(QueueName.PkiSync, async (job) => {
- if (job.name === QueueJobs.PkiSyncSendActionFailedNotifications) {
- await $sendPkiSyncFailedNotifications(job as TSendPkiSyncFailedNotificationsJobDTO);
- return;
- }
-
- const { syncId } = job.data as
- | TQueuePkiSyncSyncCertificatesByIdDTO
- | TQueuePkiSyncImportCertificatesByIdDTO
- | TQueuePkiSyncRemoveCertificatesByIdDTO;
+ const { syncId } = job.data;
const pkiSync = await pkiSyncDAL.findById(syncId);
@@ -969,10 +667,6 @@ export const pkiSyncQueueFactory = ({
const isConcurrentLimitReached = await $isConnectionConcurrencyLimitReached(connectionId);
if (isConcurrentLimitReached) {
- logger.info(
- `PkiSync Concurrency limit reached [syncId=${syncId}] [job=${job.name}] [connectionId=${connectionId}]`
- );
-
await $handleAcquireLockFailure(job as PkiSyncActionJob);
return;
@@ -988,8 +682,6 @@ export const pkiSyncQueueFactory = ({
5 * 60 * 1000
);
} catch (e) {
- logger.info(`PkiSync Failed to acquire lock [syncId=${syncId}] [job=${job.name}]`);
-
await $handleAcquireLockFailure(job as PkiSyncActionJob);
return;
diff --git a/backend/src/services/pki-sync/pki-sync-schemas.ts b/backend/src/services/pki-sync/pki-sync-schemas.ts
index 4dbcbbc74..339b1eba1 100644
--- a/backend/src/services/pki-sync/pki-sync-schemas.ts
+++ b/backend/src/services/pki-sync/pki-sync-schemas.ts
@@ -1,20 +1,48 @@
+import RE2 from "re2";
import { z } from "zod";
-import { AzureKeyVaultPkiSyncConfigSchema } from "./azure-key-vault/azure-key-vault-pki-sync-types";
import { PkiSync } from "./pki-sync-enums";
// Schema for PKI sync options configuration
export const PkiSyncOptionsSchema = z.object({
- canImportCertificates: z.boolean()
+ canImportCertificates: z.boolean(),
+ canRemoveCertificates: z.boolean().optional(),
+ certificateNameSchema: z
+ .string()
+ .optional()
+ .refine(
+ (val) => {
+ if (!val) return true;
+
+ const allowedOptionalPlaceholders = ["{{environment}}"];
+
+ const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders]
+ .map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters
+ .join("|");
+
+ const allowedContentRegex = new RE2(`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`);
+ const contentIsValid = allowedContentRegex.test(val);
+
+ if (val.trim()) {
+ const certificateIdRegex = new RE2(/\{\{certificateId\}\}/);
+ const certificateIdIsPresent = certificateIdRegex.test(val);
+ return contentIsValid && certificateIdIsPresent;
+ }
+
+ return contentIsValid;
+ },
+ {
+ message:
+ "Certificate name schema must include exactly one {{certificateId}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders."
+ }
+ )
});
// Schema for destination-specific configurations
-export const PkiSyncDestinationConfigSchema = z.discriminatedUnion("destination", [
- z.object({
- destination: z.literal(PkiSync.AzureKeyVault),
- config: AzureKeyVaultPkiSyncConfigSchema
- })
-]);
+export const PkiSyncDestinationConfigSchema = z.object({
+ destination: z.nativeEnum(PkiSync),
+ config: z.record(z.unknown())
+});
// Base PKI sync schema for API responses
export const PkiSyncSchema = z.object({
@@ -33,18 +61,3 @@ export const PkiSyncSchema = z.object({
syncStatus: z.string().nullable().optional(),
lastSyncedAt: z.date().nullable().optional()
});
-
-// Schema for PKI sync list items (includes app connection info)
-export const PkiSyncListItemSchema = PkiSyncSchema.extend({
- appConnectionName: z.string().max(255),
- appConnectionApp: z.string().max(255)
-});
-
-export const PkiSyncDetailsSchema = PkiSyncSchema.extend({
- appConnectionName: z.string().max(255),
- appConnectionApp: z.string().max(255)
-});
-
-export type TPkiSyncSchema = z.infer;
-export type TPkiSyncListItemSchema = z.infer;
-export type TPkiSyncDetailsSchema = z.infer;
diff --git a/backend/src/services/pki-sync/pki-sync-service.ts b/backend/src/services/pki-sync/pki-sync-service.ts
index 142eadd93..f0040a98d 100644
--- a/backend/src/services/pki-sync/pki-sync-service.ts
+++ b/backend/src/services/pki-sync/pki-sync-service.ts
@@ -11,17 +11,15 @@ import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-c
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { TPkiSyncDALFactory } from "./pki-sync-dal";
-import { PkiSync } from "./pki-sync-enums";
-import { enterprisePkiSyncCheck, listPkiSyncOptions } from "./pki-sync-fns";
+import { PkiSync, PkiSyncStatus } from "./pki-sync-enums";
+import { enterprisePkiSyncCheck, getPkiSyncProviderCapabilities, listPkiSyncOptions } from "./pki-sync-fns";
+import { PKI_SYNC_CONNECTION_MAP, PKI_SYNC_NAME_MAP } from "./pki-sync-maps";
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
import {
- PkiSyncStatus,
TCreatePkiSyncDTO,
TDeletePkiSyncDTO,
TFindPkiSyncByIdDTO,
- TFindPkiSyncByNameDTO,
TListPkiSyncsByProjectId,
- TListPkiSyncsBySubscriberId,
TPkiSync,
TTriggerPkiSyncImportCertificatesByIdDTO,
TTriggerPkiSyncRemoveCertificatesByIdDTO,
@@ -30,12 +28,13 @@ import {
} from "./pki-sync-types";
const getDestinationAppType = (destination: PkiSync): AppConnection => {
- switch (destination) {
- case PkiSync.AzureKeyVault:
- return AppConnection.AzureKeyVault;
- default:
- throw new BadRequestError({ message: "Unsupported PKI sync destination" });
+ const appConnection = PKI_SYNC_CONNECTION_MAP[destination];
+ if (!appConnection) {
+ throw new BadRequestError({
+ message: `Unsupported PKI sync destination: ${destination}`
+ });
}
+ return appConnection;
};
type TPkiSyncServiceFactoryDep = {
@@ -85,27 +84,30 @@ export const pkiSyncServiceFactory = ({
projectId
});
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionPkiSyncActions.Create,
- subject(ProjectPermissionSub.PkiSyncs, { projectId })
- );
-
+ let subscriber;
if (subscriberId) {
- const subscriber = await pkiSubscriberDAL.findById(subscriberId);
+ subscriber = await pkiSubscriberDAL.findById(subscriberId);
if (!subscriber || subscriber.projectId !== projectId) {
throw new NotFoundError({ message: "PKI subscriber not found" });
}
}
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionPkiSyncActions.Create,
+ subscriber
+ ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name })
+ : ProjectPermissionSub.PkiSyncs
+ );
+
// Get the destination app type based on PKI sync destination
const destinationApp = getDestinationAppType(destination);
// Validates permission to connect and app is valid for sync destination
await appConnectionService.connectAppConnectionById(destinationApp, connectionId, actor);
- const defaultSyncOptions = {
- canImportCertificates: false,
- canRemoveCertificates: true,
+ const providerCapabilities = getPkiSyncProviderCapabilities(destination);
+ const resolvedSyncOptions = {
+ ...providerCapabilities,
...syncOptions
};
@@ -116,7 +118,7 @@ export const pkiSyncServiceFactory = ({
destination,
isAutoSyncEnabled,
destinationConfig,
- syncOptions: defaultSyncOptions,
+ syncOptions: resolvedSyncOptions,
subscriberId,
connectionId,
projectId,
@@ -141,7 +143,6 @@ export const pkiSyncServiceFactory = ({
const updatePkiSync = async (
{
id,
- projectId,
name,
description,
isAutoSyncEnabled,
@@ -149,31 +150,38 @@ export const pkiSyncServiceFactory = ({
syncOptions,
subscriberId,
connectionId
- }: Omit,
+ }: Omit,
actor: OrgServiceActor
): Promise => {
+ const existingSync = await pkiSyncDAL.findById(id);
+ if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" });
+
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager,
- projectId
+ projectId: existingSync.projectId
});
- const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, projectId);
+ const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, existingSync.projectId);
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
+ let currentSubscriber;
+ if (pkiSync.subscriberId) {
+ currentSubscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
+ }
+
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.Edit,
- subject(ProjectPermissionSub.PkiSyncs, {
- projectId,
- subscriberId: pkiSync.subscriberId
- })
+ currentSubscriber
+ ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: currentSubscriber.name })
+ : ProjectPermissionSub.PkiSyncs
);
if (name && name !== pkiSync.name) {
- const existingPkiSync = await pkiSyncDAL.findByNameAndProjectId(name, projectId);
+ const existingPkiSync = await pkiSyncDAL.findByNameAndProjectId(name, existingSync.projectId);
if (existingPkiSync) {
throw new BadRequestError({ message: "PKI sync with this name already exists" });
}
@@ -181,25 +189,44 @@ export const pkiSyncServiceFactory = ({
if (subscriberId) {
const subscriber = await pkiSubscriberDAL.findById(subscriberId);
- if (!subscriber || subscriber.projectId !== projectId) {
+ if (!subscriber || subscriber.projectId !== existingSync.projectId) {
throw new NotFoundError({ message: "PKI subscriber not found" });
}
}
if (connectionId && connectionId !== pkiSync.connectionId) {
- const destinationApp =
- pkiSync.destination === PkiSync.AzureKeyVault
- ? AppConnection.AzureKeyVault
- : (pkiSync.destination as AppConnection);
+ const destinationApp = getDestinationAppType(pkiSync.destination);
await appConnectionService.connectAppConnectionById(destinationApp, connectionId, actor);
}
+ let resolvedSyncOptions = syncOptions;
+ if (syncOptions) {
+ const providerCapabilities = getPkiSyncProviderCapabilities(pkiSync.destination);
+
+ if (syncOptions.canImportCertificates && !providerCapabilities.canImportCertificates) {
+ throw new BadRequestError({
+ message: `Certificate import is not supported for ${PKI_SYNC_NAME_MAP[pkiSync.destination]} PKI sync destination`
+ });
+ }
+
+ if (syncOptions.canRemoveCertificates === false && providerCapabilities.canRemoveCertificates) {
+ throw new BadRequestError({
+ message: `Certificate removal cannot be disabled for ${PKI_SYNC_NAME_MAP[pkiSync.destination]} PKI sync destination`
+ });
+ }
+
+ resolvedSyncOptions = {
+ ...providerCapabilities,
+ ...syncOptions
+ };
+ }
+
const updatedPkiSync = await pkiSyncDAL.updateById(id, {
name,
description,
isAutoSyncEnabled,
destinationConfig,
- syncOptions,
+ syncOptions: resolvedSyncOptions,
subscriberId,
connectionId
});
@@ -208,31 +235,61 @@ export const pkiSyncServiceFactory = ({
};
const deletePkiSync = async (
- { id, projectId }: Omit,
+ { id }: Omit,
actor: OrgServiceActor
): Promise => {
+ const existingSync = await pkiSyncDAL.findById(id);
+ if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" });
+
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager,
- projectId
+ projectId: existingSync.projectId
});
- const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, projectId);
+ const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, existingSync.projectId);
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
+ let pkiSyncSubscriber;
+ if (pkiSync.subscriberId) {
+ pkiSyncSubscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
+ }
+
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.Delete,
- subject(ProjectPermissionSub.PkiSyncs, {
- projectId,
- subscriberId: pkiSync.subscriberId
- })
+ pkiSyncSubscriber
+ ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: pkiSyncSubscriber.name })
+ : ProjectPermissionSub.PkiSyncs
);
- const deletedPkiSync = await pkiSyncDAL.deleteById(id);
- return deletedPkiSync as TPkiSync;
+ await pkiSyncDAL.deleteById(id);
+ return {
+ ...pkiSync,
+ description: pkiSync.description || undefined,
+ subscriberId: pkiSync.subscriberId || undefined,
+ syncStatus: pkiSync.syncStatus || undefined,
+ lastSyncedAt: pkiSync.lastSyncedAt || undefined,
+ lastSyncJobId: pkiSync.lastSyncJobId || undefined,
+ lastSyncMessage: pkiSync.lastSyncMessage || undefined,
+ importStatus: pkiSync.importStatus || undefined,
+ lastImportJobId: pkiSync.lastImportJobId || undefined,
+ lastImportMessage: pkiSync.lastImportMessage || undefined,
+ lastImportedAt: pkiSync.lastImportedAt || undefined,
+ removeStatus: pkiSync.removeStatus || undefined,
+ lastRemoveJobId: pkiSync.lastRemoveJobId || undefined,
+ lastRemoveMessage: pkiSync.lastRemoveMessage || undefined,
+ lastRemovedAt: pkiSync.lastRemovedAt || undefined,
+ connection: {
+ ...pkiSync.connection,
+ description: pkiSync.connection.description || undefined,
+ gatewayId: pkiSync.connection.gatewayId || undefined,
+ projectId: pkiSync.connection.projectId || undefined,
+ isPlatformManagedCredentials: pkiSync.connection.isPlatformManagedCredentials || undefined
+ }
+ };
};
const listPkiSyncsByProjectId = async ({ projectId }: TListPkiSyncsByProjectId, actor: OrgServiceActor) => {
@@ -245,75 +302,78 @@ export const pkiSyncServiceFactory = ({
projectId
});
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionPkiSyncActions.Read,
- subject(ProjectPermissionSub.PkiSyncs, { projectId })
- );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs);
const pkiSyncs = await pkiSyncDAL.findByProjectId(projectId);
- return pkiSyncs;
- };
-
- const listPkiSyncsBySubscriberId = async ({ subscriberId }: TListPkiSyncsBySubscriberId) => {
- const pkiSyncs = await pkiSyncDAL.findBySubscriberId(subscriberId);
- return pkiSyncs;
+ return pkiSyncs as TPkiSync[];
};
const findPkiSyncById = async ({ id, projectId }: TFindPkiSyncByIdDTO, actor: OrgServiceActor) => {
- const { permission } = await permissionService.getProjectPermission({
- actor: actor.type,
- actorId: actor.id,
- actorAuthMethod: actor.authMethod,
- actorOrgId: actor.orgId,
- actionProjectType: ActionProjectType.CertificateManager,
- projectId
- });
-
- const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, projectId);
+ const pkiSync = await pkiSyncDAL.findById(id);
if (!pkiSync)
throw new NotFoundError({
message: `Could not find PKI Sync with ID "${id}"`
});
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionPkiSyncActions.Read,
- subject(ProjectPermissionSub.PkiSyncs, {
- projectId,
- subscriberId: pkiSync.subscriberId
- })
- );
+ if (projectId && pkiSync.projectId !== projectId) {
+ throw new NotFoundError({
+ message: `Could not find PKI Sync with ID "${id}" in project "${projectId}"`
+ });
+ }
- return pkiSync;
- };
-
- const findPkiSyncByName = async ({ name, projectId }: TFindPkiSyncByNameDTO) => {
- const pkiSync = await pkiSyncDAL.findByNameAndProjectId(name, projectId);
- if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
- return pkiSync;
- };
-
- const triggerPkiSyncSyncCertificatesById = async (
- { id, projectId }: Omit,
- actor: OrgServiceActor
- ) => {
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager,
- projectId
+ projectId: pkiSync.projectId
});
- const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, projectId);
+ let findSubscriber;
+ if (pkiSync.subscriberId) {
+ findSubscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
+ }
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionPkiSyncActions.Read,
+ findSubscriber
+ ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: findSubscriber.name })
+ : ProjectPermissionSub.PkiSyncs
+ );
+
+ return pkiSync as TPkiSync;
+ };
+
+ const triggerPkiSyncSyncCertificatesById = async (
+ { id }: Omit,
+ actor: OrgServiceActor
+ ) => {
+ const existingSync = await pkiSyncDAL.findById(id);
+ if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" });
+
+ const { permission } = await permissionService.getProjectPermission({
+ actor: actor.type,
+ actorId: actor.id,
+ actorAuthMethod: actor.authMethod,
+ actorOrgId: actor.orgId,
+ actionProjectType: ActionProjectType.CertificateManager,
+ projectId: existingSync.projectId
+ });
+
+ const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, existingSync.projectId);
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
+ let syncSubscriber;
+ if (pkiSync.subscriberId) {
+ syncSubscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
+ }
+
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.SyncCertificates,
- subject(ProjectPermissionSub.PkiSyncs, {
- projectId,
- subscriberId: pkiSync.subscriberId
- })
+ syncSubscriber
+ ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: syncSubscriber.name })
+ : ProjectPermissionSub.PkiSyncs
);
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: id });
@@ -322,27 +382,42 @@ export const pkiSyncServiceFactory = ({
};
const triggerPkiSyncImportCertificatesById = async (
- { id, projectId }: Omit,
+ { id }: Omit,
actor: OrgServiceActor
) => {
+ const existingSync = await pkiSyncDAL.findById(id);
+ if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" });
+
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager,
- projectId
+ projectId: existingSync.projectId
});
- const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, projectId);
+ const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, existingSync.projectId);
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
+ // Check if the PKI sync destination supports importing certificates
+ const syncOptions = listPkiSyncOptions().find((option) => option.destination === pkiSync.destination);
+ if (!syncOptions?.canImportCertificates) {
+ throw new BadRequestError({
+ message: `Certificate import is not supported for ${pkiSync.destination} PKI sync destination`
+ });
+ }
+
+ let importSubscriber;
+ if (pkiSync.subscriberId) {
+ importSubscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
+ }
+
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.ImportCertificates,
- subject(ProjectPermissionSub.PkiSyncs, {
- projectId,
- subscriberId: pkiSync.subscriberId
- })
+ importSubscriber
+ ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: importSubscriber.name })
+ : ProjectPermissionSub.PkiSyncs
);
await pkiSyncQueue.queuePkiSyncImportCertificatesById({ syncId: id });
@@ -351,27 +426,34 @@ export const pkiSyncServiceFactory = ({
};
const triggerPkiSyncRemoveCertificatesById = async (
- { id, projectId }: Omit,
+ { id }: Omit,
actor: OrgServiceActor
) => {
+ const existingSync = await pkiSyncDAL.findById(id);
+ if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" });
+
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager,
- projectId
+ projectId: existingSync.projectId
});
- const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, projectId);
+ const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, existingSync.projectId);
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
+ let removeSubscriber;
+ if (pkiSync.subscriberId) {
+ removeSubscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
+ }
+
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.RemoveCertificates,
- subject(ProjectPermissionSub.PkiSyncs, {
- projectId,
- subscriberId: pkiSync.subscriberId
- })
+ removeSubscriber
+ ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: removeSubscriber.name })
+ : ProjectPermissionSub.PkiSyncs
);
await pkiSyncQueue.queuePkiSyncRemoveCertificatesById({ syncId: id });
@@ -388,9 +470,7 @@ export const pkiSyncServiceFactory = ({
updatePkiSync,
deletePkiSync,
listPkiSyncsByProjectId,
- listPkiSyncsBySubscriberId,
findPkiSyncById,
- findPkiSyncByName,
triggerPkiSyncSyncCertificatesById,
triggerPkiSyncImportCertificatesById,
triggerPkiSyncRemoveCertificatesById,
diff --git a/backend/src/services/pki-sync/pki-sync-types.ts b/backend/src/services/pki-sync/pki-sync-types.ts
index 676ea441f..b1ca9115e 100644
--- a/backend/src/services/pki-sync/pki-sync-types.ts
+++ b/backend/src/services/pki-sync/pki-sync-types.ts
@@ -13,7 +13,6 @@ export type TPkiSync = {
description?: string;
destination: PkiSync;
isAutoSyncEnabled: boolean;
- version: number;
destinationConfig: Record;
syncOptions: Record;
projectId: string;
@@ -33,11 +32,23 @@ export type TPkiSync = {
lastRemoveJobId?: string;
lastRemoveMessage?: string;
lastRemovedAt?: Date;
-};
-
-export type TPkiSyncListItem = TPkiSync & {
appConnectionName: string;
appConnectionApp: string;
+ connection: {
+ id: string;
+ name: string;
+ app: string;
+ encryptedCredentials: unknown;
+ orgId: string;
+ projectId?: string;
+ method: string;
+ description?: string;
+ version: number;
+ gatewayId?: string;
+ createdAt: Date;
+ updatedAt: Date;
+ isPlatformManagedCredentials?: boolean;
+ };
};
export type TPkiSyncWithCredentials = TPkiSync & {
@@ -50,6 +61,11 @@ export type TPkiSyncWithCredentials = TPkiSync & {
};
};
+export type TPkiSyncListItem = TPkiSync & {
+ appConnectionName: string;
+ appConnectionApp: string;
+};
+
export type TCertificateMap = Record;
export type TCreatePkiSyncDTO = {
@@ -68,7 +84,7 @@ export type TCreatePkiSyncDTO = {
export type TUpdatePkiSyncDTO = {
id: string;
- projectId: string;
+ projectId?: string;
name?: string;
description?: string;
isAutoSyncEnabled?: boolean;
@@ -82,7 +98,7 @@ export type TUpdatePkiSyncDTO = {
export type TDeletePkiSyncDTO = {
id: string;
- projectId: string;
+ projectId?: string;
auditLogInfo: AuditLogInfo;
};
@@ -90,51 +106,29 @@ export type TListPkiSyncsByProjectId = {
projectId: string;
};
-export type TListPkiSyncsBySubscriberId = {
- subscriberId: string;
-};
-
export type TFindPkiSyncByIdDTO = {
id: string;
- projectId: string;
-};
-
-export type TFindPkiSyncByNameDTO = {
- name: string;
- projectId: string;
+ projectId?: string;
};
export type TTriggerPkiSyncSyncCertificatesByIdDTO = {
id: string;
- projectId: string;
+ projectId?: string;
auditLogInfo: AuditLogInfo;
};
export type TTriggerPkiSyncImportCertificatesByIdDTO = {
id: string;
- projectId: string;
+ projectId?: string;
auditLogInfo: AuditLogInfo;
};
export type TTriggerPkiSyncRemoveCertificatesByIdDTO = {
id: string;
- projectId: string;
+ projectId?: string;
auditLogInfo: AuditLogInfo;
};
-export enum PkiSyncStatus {
- Pending = "pending",
- Running = "running",
- Succeeded = "succeeded",
- Failed = "failed"
-}
-
-export enum PkiSyncAction {
- SyncCertificates = "sync-certificates",
- ImportCertificates = "import-certificates",
- RemoveCertificates = "remove-certificates"
-}
-
export type TPkiSyncRaw = NonNullable>>;
export type TQueuePkiSyncSyncCertificatesByIdDTO = {
@@ -154,12 +148,6 @@ export type TQueuePkiSyncRemoveCertificatesByIdDTO = {
deleteSyncOnComplete?: boolean;
};
-export type TQueueSendPkiSyncActionFailedNotificationsDTO = {
- pkiSync: TPkiSyncRaw;
- auditLogInfo?: AuditLogInfo;
- action: PkiSyncAction;
-};
-
export type TPkiSyncSyncCertificatesDTO = Job<
TQueuePkiSyncSyncCertificatesByIdDTO,
void,
@@ -175,9 +163,3 @@ export type TPkiSyncRemoveCertificatesDTO = Job<
void,
QueueJobs.PkiSyncRemoveCertificates
>;
-
-export type TSendPkiSyncFailedNotificationsJobDTO = Job<
- TQueueSendPkiSyncActionFailedNotificationsDTO,
- void,
- QueueJobs.PkiSyncSendActionFailedNotifications
->;
diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/create.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/create.mdx
deleted file mode 100644
index 497874510..000000000
--- a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/create.mdx
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Create"
-openapi: "POST /api/v1/pki-syncs"
----
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/delete.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/delete.mdx
deleted file mode 100644
index db1f254c3..000000000
--- a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/delete.mdx
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Delete"
-openapi: "DELETE /api/v1/pki-syncs/{pkiSyncId}"
----
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/get-by-id.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/get-by-id.mdx
deleted file mode 100644
index 38860a416..000000000
--- a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/get-by-id.mdx
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Get by ID"
-openapi: "GET /api/v1/pki-syncs/{pkiSyncId}"
----
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/import-certificates.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/import-certificates.mdx
deleted file mode 100644
index fbcad174d..000000000
--- a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/import-certificates.mdx
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Import Certificates"
-openapi: "POST /api/v1/pki-syncs/{pkiSyncId}/import"
----
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/list.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/list.mdx
deleted file mode 100644
index baedf1688..000000000
--- a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/list.mdx
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "List"
-openapi: "GET /api/v1/pki-syncs"
----
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/remove-certificates.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/remove-certificates.mdx
deleted file mode 100644
index 94969c823..000000000
--- a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/remove-certificates.mdx
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Remove Certificates"
-openapi: "POST /api/v1/pki-syncs/{pkiSyncId}/remove"
----
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/sync-certificates.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/sync-certificates.mdx
deleted file mode 100644
index 65ff9e22d..000000000
--- a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/sync-certificates.mdx
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Sync Certificates"
-openapi: "POST /api/v1/pki-syncs/{pkiSyncId}/sync"
----
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/update.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/update.mdx
deleted file mode 100644
index 7067acea3..000000000
--- a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/update.mdx
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Update"
-openapi: "PATCH /api/v1/pki-syncs/{pkiSyncId}"
----
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/certificate-syncs/list.mdx b/docs/api-reference/endpoints/certificate-syncs/list.mdx
index 3bfd8e0c5..6de2c2d1b 100644
--- a/docs/api-reference/endpoints/certificate-syncs/list.mdx
+++ b/docs/api-reference/endpoints/certificate-syncs/list.mdx
@@ -1,4 +1,4 @@
---
title: "List PKI Syncs"
-openapi: "GET /api/v1/pki-syncs"
+openapi: "GET /api/v1/pki/syncs"
---
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/certificate-syncs/options.mdx b/docs/api-reference/endpoints/certificate-syncs/options.mdx
index 132cb26b9..ab2d11e48 100644
--- a/docs/api-reference/endpoints/certificate-syncs/options.mdx
+++ b/docs/api-reference/endpoints/certificate-syncs/options.mdx
@@ -1,4 +1,4 @@
---
title: "Options"
-openapi: "GET /api/v1/pki-syncs/options"
+openapi: "GET /api/v1/pki/syncs/options"
---
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/create.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/create.mdx
new file mode 100644
index 000000000..ab64179ce
--- /dev/null
+++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/create.mdx
@@ -0,0 +1,151 @@
+---
+title: "Create Azure Key Vault PKI Sync"
+openapi: "POST /api/v1/pki/syncs/azure-key-vault"
+---
+
+
+This endpoint creates a new Azure Key Vault PKI sync for a specified project.
+
+
+## Request
+
+
+ Name of the PKI sync (1-64 characters)
+
+
+
+ Description of the PKI sync
+
+
+
+ Whether automatic synchronization is enabled when certificates are issued
+
+
+
+ Azure Key Vault specific configuration
+
+
+ Base URL of the Azure Key Vault (e.g., "https://my-vault.vault.azure.net/")
+
+
+
+
+
+ Sync-specific options and settings
+
+
+
+ ID of the PKI subscriber to connect this sync to. If provided, certificates issued by this subscriber will be automatically synced to Azure Key Vault.
+
+
+
+ ID of the Azure Key Vault app connection to use for this sync
+
+
+
+ ID of the project to create the PKI sync in
+
+
+## Response
+
+Returns the created Azure Key Vault PKI sync object with the same structure as the list endpoint response.
+
+
+ Unique identifier for the created PKI sync
+
+
+
+ Name of the PKI sync
+
+
+
+ Description of the PKI sync
+
+
+
+ Always "azure-key-vault"
+
+
+
+ Whether automatic synchronization is enabled
+
+
+
+ Azure Key Vault specific configuration
+
+
+
+ Sync-specific options and settings
+
+
+
+ ID of the project this sync belongs to
+
+
+
+ ID of the PKI subscriber this sync is connected to
+
+
+
+ ID of the Azure Key Vault app connection used for this sync
+
+
+
+ Timestamp when the PKI sync was created
+
+
+
+ Timestamp when the PKI sync was last updated
+
+
+
+```bash cURL
+curl -X POST "https://app.infisical.com/api/v1/pki/syncs/azure-key-vault" \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Production Azure Key Vault Sync",
+ "description": "Sync certificates to production Key Vault",
+ "isAutoSyncEnabled": true,
+ "destinationConfig": {
+ "vaultBaseUrl": "https://my-vault.vault.azure.net/"
+ },
+ "syncOptions": {},
+ "subscriberId": "sub_12345",
+ "connectionId": "conn_12345",
+ "projectId": "proj_12345"
+ }'
+```
+
+
+
+```json Response
+{
+ "id": "ps_12345",
+ "name": "Production Azure Key Vault Sync",
+ "description": "Sync certificates to production Key Vault",
+ "destination": "azure-key-vault",
+ "isAutoSyncEnabled": true,
+ "destinationConfig": {
+ "vaultBaseUrl": "https://my-vault.vault.azure.net/"
+ },
+ "syncOptions": {},
+ "projectId": "proj_12345",
+ "subscriberId": "sub_12345",
+ "connectionId": "conn_12345",
+ "syncStatus": null,
+ "lastSyncedAt": null,
+ "lastSyncMessage": null,
+ "removeStatus": null,
+ "lastRemovedAt": null,
+ "lastRemoveMessage": null,
+ "connection": {
+ "id": "conn_12345",
+ "name": "Azure Production Connection",
+ "app": "azure-key-vault"
+ },
+ "createdAt": "2023-11-01T10:00:00Z",
+ "updatedAt": "2023-11-01T10:00:00Z"
+}
+```
+
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/delete.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/delete.mdx
new file mode 100644
index 000000000..1e1e6b10d
--- /dev/null
+++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/delete.mdx
@@ -0,0 +1,113 @@
+---
+title: "Delete Azure Key Vault PKI Sync"
+openapi: "DELETE /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}"
+---
+
+
+This action is irreversible. Deleting a PKI sync will stop all automatic certificate synchronization to the Azure Key Vault, but it will not remove certificates that have already been synced.
+
+
+
+This endpoint deletes an existing Azure Key Vault PKI sync.
+
+
+## Request
+
+
+ The ID of the Azure Key Vault PKI sync to delete
+
+
+
+ Project ID for additional authorization (will be inferred if not provided)
+
+
+## Response
+
+Returns the deleted Azure Key Vault PKI sync object.
+
+
+ Unique identifier for the deleted PKI sync
+
+
+
+ Name of the deleted PKI sync
+
+
+
+ Description of the deleted PKI sync
+
+
+
+ Always "azure-key-vault"
+
+
+
+ Whether automatic synchronization was enabled
+
+
+
+ Azure Key Vault specific configuration
+
+
+
+ Sync-specific options and settings
+
+
+
+ ID of the project this sync belonged to
+
+
+
+ ID of the PKI subscriber this sync was connected to
+
+
+
+ ID of the Azure Key Vault app connection that was used for this sync
+
+
+
+ Timestamp when the PKI sync was created
+
+
+
+ Timestamp when the PKI sync was last updated
+
+
+
+```bash cURL
+curl -X DELETE "https://app.infisical.com/api/v1/pki/syncs/azure-key-vault/ps_12345" \
+ -H "Authorization: Bearer "
+```
+
+
+
+```json Response
+{
+ "id": "ps_12345",
+ "name": "Production Azure Key Vault Sync",
+ "description": "Sync certificates to production Key Vault",
+ "destination": "azure-key-vault",
+ "isAutoSyncEnabled": true,
+ "destinationConfig": {
+ "vaultBaseUrl": "https://my-vault.vault.azure.net/"
+ },
+ "syncOptions": {},
+ "projectId": "proj_12345",
+ "subscriberId": "sub_12345",
+ "connectionId": "conn_12345",
+ "syncStatus": "succeeded",
+ "lastSyncedAt": "2023-12-01T10:00:00Z",
+ "lastSyncMessage": "Successfully synced 3 certificates",
+ "removeStatus": null,
+ "lastRemovedAt": null,
+ "lastRemoveMessage": null,
+ "connection": {
+ "id": "conn_12345",
+ "name": "Azure Production Connection",
+ "app": "azure-key-vault"
+ },
+ "createdAt": "2023-11-01T10:00:00Z",
+ "updatedAt": "2023-12-01T10:00:00Z"
+}
+```
+
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/get-by-id.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/get-by-id.mdx
new file mode 100644
index 000000000..a521a68c6
--- /dev/null
+++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/get-by-id.mdx
@@ -0,0 +1,140 @@
+---
+title: "Get Azure Key Vault PKI Sync by ID"
+openapi: "GET /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}"
+---
+
+
+This endpoint retrieves a specific Azure Key Vault PKI sync by its ID.
+
+
+## Request
+
+
+ The ID of the Azure Key Vault PKI sync to retrieve
+
+
+
+ Project ID for additional authorization (will be inferred if not provided)
+
+
+## Response
+
+
+ Unique identifier for the PKI sync
+
+
+
+ Name of the PKI sync
+
+
+
+ Description of the PKI sync
+
+
+
+ Always "azure-key-vault"
+
+
+
+ Whether automatic synchronization is enabled
+
+
+
+ Azure Key Vault specific configuration
+
+
+ Base URL of the Azure Key Vault
+
+
+
+
+
+ Sync-specific options and settings
+
+
+
+ ID of the project this sync belongs to
+
+
+
+ ID of the PKI subscriber this sync is connected to
+
+
+
+ ID of the Azure Key Vault app connection used for this sync
+
+
+
+ Current status of the last sync operation
+
+
+
+ Timestamp of the last successful sync
+
+
+
+ Message from the last sync operation
+
+
+
+ Current status of the last remove operation
+
+
+
+ Timestamp of the last certificate removal
+
+
+
+ Message from the last remove operation
+
+
+
+ Details about the associated Azure Key Vault app connection
+
+
+
+ Timestamp when the PKI sync was created
+
+
+
+ Timestamp when the PKI sync was last updated
+
+
+
+```bash cURL
+curl -X GET "https://app.infisical.com/api/v1/pki/syncs/azure-key-vault/ps_12345" \
+ -H "Authorization: Bearer "
+```
+
+
+
+```json Response
+{
+ "id": "ps_12345",
+ "name": "Production Azure Key Vault Sync",
+ "description": "Sync certificates to production Key Vault",
+ "destination": "azure-key-vault",
+ "isAutoSyncEnabled": true,
+ "destinationConfig": {
+ "vaultBaseUrl": "https://my-vault.vault.azure.net/"
+ },
+ "syncOptions": {},
+ "projectId": "proj_12345",
+ "subscriberId": "sub_12345",
+ "connectionId": "conn_12345",
+ "syncStatus": "succeeded",
+ "lastSyncedAt": "2023-12-01T10:00:00Z",
+ "lastSyncMessage": "Successfully synced 3 certificates",
+ "removeStatus": null,
+ "lastRemovedAt": null,
+ "lastRemoveMessage": null,
+ "connection": {
+ "id": "conn_12345",
+ "name": "Azure Production Connection",
+ "app": "azure-key-vault"
+ },
+ "createdAt": "2023-11-01T10:00:00Z",
+ "updatedAt": "2023-12-01T10:00:00Z"
+}
+```
+
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/list.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/list.mdx
new file mode 100644
index 000000000..a9968de5b
--- /dev/null
+++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/list.mdx
@@ -0,0 +1,138 @@
+---
+title: "List Azure Key Vault PKI Syncs"
+openapi: "GET /api/v1/pki/syncs/azure-key-vault"
+---
+
+
+This endpoint lists all Azure Key Vault PKI syncs for a specified project.
+
+
+## Request
+
+
+ The ID of the project to list Azure Key Vault PKI syncs for
+
+
+## Response
+
+
+Array of Azure Key Vault PKI syncs for the project
+
+
+ Unique identifier for the PKI sync
+
+
+
+ Name of the PKI sync
+
+
+
+ Description of the PKI sync
+
+
+
+ Always "azure-key-vault" for this endpoint
+
+
+
+ Whether automatic synchronization is enabled
+
+
+
+ Azure Key Vault specific configuration
+
+
+ Base URL of the Azure Key Vault (e.g., "https://my-vault.vault.azure.net/")
+
+
+
+
+
+ Sync-specific options and settings
+
+
+
+ ID of the project this sync belongs to
+
+
+
+ ID of the PKI subscriber this sync is connected to
+
+
+
+ ID of the Azure Key Vault app connection used for this sync
+
+
+
+ Current status of the last sync operation ("pending", "running", "succeeded", "failed")
+
+
+
+ Timestamp of the last successful sync
+
+
+
+ Message from the last sync operation
+
+
+
+ Current status of the last remove operation
+
+
+
+ Timestamp of the last certificate removal
+
+
+
+ Message from the last remove operation
+
+
+
+ Details about the associated Azure Key Vault app connection
+
+
+
+ Timestamp when the PKI sync was created
+
+
+
+ Timestamp when the PKI sync was last updated
+
+
+
+
+
+```json Response
+{
+ "pkiSyncs": [
+ {
+ "id": "ps_12345",
+ "name": "Production Azure Key Vault Sync",
+ "description": "Sync certificates to production Key Vault",
+ "destination": "azure-key-vault",
+ "isAutoSyncEnabled": true,
+ "destinationConfig": {
+ "vaultBaseUrl": "https://my-vault.vault.azure.net/"
+ },
+ "syncOptions": {},
+ "projectId": "proj_12345",
+ "subscriberId": "sub_12345",
+ "connectionId": "conn_12345",
+ "syncStatus": "succeeded",
+ "lastSyncedAt": "2023-12-01T10:00:00Z",
+ "lastSyncMessage": "Successfully synced 3 certificates",
+ "removeStatus": null,
+ "lastRemovedAt": null,
+ "lastRemoveMessage": null,
+ "connection": {
+ "id": "conn_12345",
+ "name": "Azure Production Connection",
+ "app": "azure-key-vault"
+ },
+ "createdAt": "2023-11-01T10:00:00Z",
+ "updatedAt": "2023-12-01T10:00:00Z"
+ }
+ ]
+}
+```
+
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates.mdx
new file mode 100644
index 000000000..0c73dba63
--- /dev/null
+++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates.mdx
@@ -0,0 +1,65 @@
+---
+title: "Remove Certificates from Azure Key Vault"
+openapi: "POST /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}/remove"
+---
+
+
+This endpoint permanently removes certificates from Azure Key Vault. Only certificates managed by Infisical (prefixed with "Infisical-") will be removed. This action cannot be undone.
+
+
+
+This endpoint removes certificates from the specified Azure Key Vault that are no longer active in Infisical or are expired. It helps clean up outdated certificates and maintain security hygiene.
+
+
+## Request
+
+
+ The ID of the Azure Key Vault PKI sync to remove certificates from
+
+
+
+ Project ID for additional authorization (will be inferred if not provided)
+
+
+## Response
+
+
+ Success message confirming the remove operation has been triggered
+
+
+
+```bash cURL
+curl -X POST "https://app.infisical.com/api/v1/pki/syncs/azure-key-vault/ps_12345/remove" \
+ -H "Authorization: Bearer "
+```
+
+
+
+```json Response
+{
+ "message": "Remove operation has been triggered successfully"
+}
+```
+
+
+## Behavior
+
+When this endpoint is called:
+
+1. **Certificate Identification**: Identifies certificates in Azure Key Vault that are managed by Infisical (prefixed with "Infisical-")
+2. **Status Check**: Compares against active certificates in the connected PKI subscriber
+3. **Selective Removal**: Removes only certificates that are:
+ - Expired or revoked in Infisical
+ - No longer present in the PKI subscriber
+ - Managed by Infisical (prefixed with "Infisical-")
+4. **Status Tracking**: The remove status is updated and can be monitored through the PKI sync object
+
+
+- Only certificates with the "Infisical-" prefix are considered for removal
+- Certificates not managed by Infisical remain untouched
+- Disabled certificates in Azure Key Vault are skipped during the removal process
+
+
+
+This operation requires appropriate permissions in the Azure Key Vault. Ensure your Azure Key Vault app connection has sufficient permissions to delete certificates.
+
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/sync-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/sync-certificates.mdx
new file mode 100644
index 000000000..fbb89d088
--- /dev/null
+++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/sync-certificates.mdx
@@ -0,0 +1,57 @@
+---
+title: "Sync Certificates to Azure Key Vault"
+openapi: "POST /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}/sync"
+---
+
+
+This endpoint triggers a manual synchronization of certificates from Infisical to the specified Azure Key Vault. It will upload all active certificates from the connected PKI subscriber to the Azure Key Vault, creating or updating certificates as needed.
+
+
+
+Certificates are uploaded to Azure Key Vault with their certificate and private key combined in PEM format. The certificate key properties (RSA/ECDSA type and key size) are automatically detected and configured appropriately.
+
+
+## Request
+
+
+ The ID of the Azure Key Vault PKI sync to trigger synchronization for
+
+
+
+ Project ID for additional authorization (will be inferred if not provided)
+
+
+## Response
+
+
+ Success message confirming the sync operation has been triggered
+
+
+
+```bash cURL
+curl -X POST "https://app.infisical.com/api/v1/pki/syncs/azure-key-vault/ps_12345/sync" \
+ -H "Authorization: Bearer "
+```
+
+
+
+```json Response
+{
+ "message": "Sync operation has been triggered successfully"
+}
+```
+
+
+## Behavior
+
+When this endpoint is called:
+
+1. **Certificate Collection**: All active (non-expired) certificates from the connected PKI subscriber are collected
+2. **Key Property Detection**: Each certificate's key properties (RSA/ECDSA type, key size, curve) are automatically detected
+3. **Azure Key Vault Upload**: Certificates are uploaded to Azure Key Vault with the correct key properties
+4. **Certificate Naming**: Certificates are prefixed with "Infisical-" in Azure Key Vault for identification
+5. **Status Tracking**: The sync status is updated and can be monitored through the PKI sync object
+
+
+This operation requires appropriate permissions in the Azure Key Vault. Ensure your Azure Key Vault app connection has sufficient permissions to create and update certificates.
+
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/update.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/update.mdx
new file mode 100644
index 000000000..23563179d
--- /dev/null
+++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/update.mdx
@@ -0,0 +1,150 @@
+---
+title: "Update Azure Key Vault PKI Sync"
+openapi: "PATCH /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}"
+---
+
+
+This endpoint updates an existing Azure Key Vault PKI sync.
+
+
+## Request
+
+
+ The ID of the Azure Key Vault PKI sync to update
+
+
+
+ Project ID for additional authorization (will be inferred if not provided)
+
+
+
+ Name of the PKI sync (1-64 characters)
+
+
+
+ Description of the PKI sync
+
+
+
+ Whether automatic synchronization is enabled when certificates are issued
+
+
+
+ Azure Key Vault specific configuration
+
+
+ Base URL of the Azure Key Vault (e.g., "https://my-vault.vault.azure.net/")
+
+
+
+
+
+ Sync-specific options and settings
+
+
+
+ ID of the PKI subscriber to connect this sync to. If provided, certificates issued by this subscriber will be automatically synced to Azure Key Vault.
+
+
+
+ ID of the Azure Key Vault app connection to use for this sync
+
+
+## Response
+
+Returns the updated Azure Key Vault PKI sync object.
+
+
+ Unique identifier for the PKI sync
+
+
+
+ Updated name of the PKI sync
+
+
+
+ Updated description of the PKI sync
+
+
+
+ Always "azure-key-vault"
+
+
+
+ Updated automatic synchronization setting
+
+
+
+ Updated Azure Key Vault specific configuration
+
+
+
+ Updated sync-specific options and settings
+
+
+
+ ID of the project this sync belongs to
+
+
+
+ Updated PKI subscriber ID this sync is connected to
+
+
+
+ Updated Azure Key Vault app connection ID
+
+
+
+ Timestamp when the PKI sync was created
+
+
+
+ Timestamp when the PKI sync was last updated
+
+
+
+```bash cURL
+curl -X PATCH "https://app.infisical.com/api/v1/pki/syncs/azure-key-vault/ps_12345" \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "Updated Production Azure Key Vault Sync",
+ "isAutoSyncEnabled": false,
+ "destinationConfig": {
+ "vaultBaseUrl": "https://my-new-vault.vault.azure.net/"
+ }
+ }'
+```
+
+
+
+```json Response
+{
+ "id": "ps_12345",
+ "name": "Updated Production Azure Key Vault Sync",
+ "description": "Sync certificates to production Key Vault",
+ "destination": "azure-key-vault",
+ "isAutoSyncEnabled": false,
+ "destinationConfig": {
+ "vaultBaseUrl": "https://my-new-vault.vault.azure.net/"
+ },
+ "syncOptions": {},
+ "projectId": "proj_12345",
+ "subscriberId": "sub_12345",
+ "connectionId": "conn_12345",
+ "syncStatus": "succeeded",
+ "lastSyncedAt": "2023-12-01T10:00:00Z",
+ "lastSyncMessage": "Successfully synced 3 certificates",
+ "removeStatus": null,
+ "lastRemovedAt": null,
+ "lastRemoveMessage": null,
+ "connection": {
+ "id": "conn_12345",
+ "name": "Azure Production Connection",
+ "app": "azure-key-vault"
+ },
+ "createdAt": "2023-11-01T10:00:00Z",
+ "updatedAt": "2023-12-01T11:30:00Z"
+}
+```
+
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/pki/syncs/get-by-id.mdx b/docs/api-reference/endpoints/pki/syncs/get-by-id.mdx
new file mode 100644
index 000000000..853cc26ea
--- /dev/null
+++ b/docs/api-reference/endpoints/pki/syncs/get-by-id.mdx
@@ -0,0 +1,139 @@
+---
+title: "Get PKI Sync by ID"
+openapi: "GET /api/v1/pki/syncs/{pkiSyncId}"
+---
+
+
+This endpoint retrieves a specific PKI sync by its ID.
+
+
+## Request
+
+
+ The ID of the PKI sync to retrieve
+
+
+
+ Project ID for additional authorization (will be inferred if not provided)
+
+
+## Response
+
+
+ Unique identifier for the PKI sync
+
+
+
+ Name of the PKI sync
+
+
+
+ Description of the PKI sync
+
+
+
+ PKI sync destination type (e.g., "azure-key-vault")
+
+
+
+ Whether automatic synchronization is enabled
+
+
+
+ Configuration specific to the destination
+
+
+
+ Sync-specific options and settings
+
+
+
+ ID of the project this sync belongs to
+
+
+
+ ID of the PKI subscriber this sync is connected to
+
+
+
+ ID of the app connection used for this sync
+
+
+
+ Current status of the last sync operation
+
+
+
+ Timestamp of the last successful sync
+
+
+
+ Message from the last sync operation
+
+
+
+ Current status of the last remove operation
+
+
+
+ Timestamp of the last certificate removal
+
+
+
+ Message from the last remove operation
+
+
+
+ Details about the associated app connection
+
+
+ Connection ID
+
+
+ Connection name
+
+
+ App type
+
+
+
+
+
+ Timestamp when the PKI sync was created
+
+
+
+ Timestamp when the PKI sync was last updated
+
+
+
+```json Response
+{
+ "id": "ps_12345",
+ "name": "Production Azure Key Vault Sync",
+ "description": "Sync certificates to production Key Vault",
+ "destination": "azure-key-vault",
+ "isAutoSyncEnabled": true,
+ "destinationConfig": {
+ "vaultUrl": "https://my-vault.vault.azure.net/"
+ },
+ "syncOptions": {},
+ "projectId": "proj_12345",
+ "subscriberId": "sub_12345",
+ "connectionId": "conn_12345",
+ "syncStatus": "succeeded",
+ "lastSyncedAt": "2023-12-01T10:00:00Z",
+ "lastSyncMessage": "Successfully synced 3 certificates",
+ "removeStatus": null,
+ "lastRemovedAt": null,
+ "lastRemoveMessage": null,
+ "connection": {
+ "id": "conn_12345",
+ "name": "Azure Production Connection",
+ "app": "azure-key-vault"
+ },
+ "createdAt": "2023-11-01T10:00:00Z",
+ "updatedAt": "2023-12-01T10:00:00Z"
+}
+```
+
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/pki/syncs/list.mdx b/docs/api-reference/endpoints/pki/syncs/list.mdx
new file mode 100644
index 000000000..463b5bd06
--- /dev/null
+++ b/docs/api-reference/endpoints/pki/syncs/list.mdx
@@ -0,0 +1,144 @@
+---
+title: "List PKI Syncs"
+openapi: "GET /api/v1/pki/syncs"
+---
+
+
+This endpoint lists all PKI syncs across all destinations for a specified project.
+
+
+## Request
+
+
+ The ID of the project to list PKI syncs for
+
+
+## Response
+
+
+Array of PKI syncs for the project
+
+
+ Unique identifier for the PKI sync
+
+
+
+ Name of the PKI sync
+
+
+
+ Description of the PKI sync
+
+
+
+ PKI sync destination type (e.g., "azure-key-vault")
+
+
+
+ Whether automatic synchronization is enabled
+
+
+
+ Configuration specific to the destination
+
+
+
+ Sync-specific options and settings
+
+
+
+ ID of the project this sync belongs to
+
+
+
+ ID of the PKI subscriber this sync is connected to
+
+
+
+ ID of the app connection used for this sync
+
+
+
+ Current status of the last sync operation
+
+
+
+ Timestamp of the last successful sync
+
+
+
+ Message from the last sync operation
+
+
+
+ Current status of the last remove operation
+
+
+
+ Timestamp of the last certificate removal
+
+
+
+ Message from the last remove operation
+
+
+
+ Details about the associated app connection
+
+
+ Connection ID
+
+
+ Connection name
+
+
+ App type
+
+
+
+
+
+ Timestamp when the PKI sync was created
+
+
+
+ Timestamp when the PKI sync was last updated
+
+
+
+
+
+```json Response
+{
+ "pkiSyncs": [
+ {
+ "id": "ps_12345",
+ "name": "Production Azure Key Vault Sync",
+ "description": "Sync certificates to production Key Vault",
+ "destination": "azure-key-vault",
+ "isAutoSyncEnabled": true,
+ "destinationConfig": {
+ "vaultUrl": "https://my-vault.vault.azure.net/"
+ },
+ "syncOptions": {},
+ "projectId": "proj_12345",
+ "subscriberId": "sub_12345",
+ "connectionId": "conn_12345",
+ "syncStatus": "succeeded",
+ "lastSyncedAt": "2023-12-01T10:00:00Z",
+ "lastSyncMessage": "Successfully synced 3 certificates",
+ "removeStatus": null,
+ "lastRemovedAt": null,
+ "lastRemoveMessage": null,
+ "connection": {
+ "id": "conn_12345",
+ "name": "Azure Production Connection",
+ "app": "azure-key-vault"
+ },
+ "createdAt": "2023-11-01T10:00:00Z",
+ "updatedAt": "2023-12-01T10:00:00Z"
+ }
+ ]
+}
+```
+
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/pki/syncs/options.mdx b/docs/api-reference/endpoints/pki/syncs/options.mdx
new file mode 100644
index 000000000..e9bf0e57e
--- /dev/null
+++ b/docs/api-reference/endpoints/pki/syncs/options.mdx
@@ -0,0 +1,57 @@
+---
+title: "List PKI Sync Options"
+openapi: "GET /api/v1/pki/syncs/options"
+---
+
+
+This endpoint lists all available PKI sync destination options and their capabilities.
+
+
+## Request
+
+
+ Project ID (for authorization purposes, but the options are global)
+
+
+## Response
+
+
+Array of available PKI sync options
+
+
+ Display name of the PKI sync destination
+
+
+
+ App connection type required for this destination
+
+
+
+ PKI sync destination identifier
+
+
+
+ Whether this destination supports importing certificates from the destination to Infisical
+
+
+
+ Whether this destination supports removing certificates from the destination
+
+
+
+
+
+```json Response
+{
+ "pkiSyncOptions": [
+ {
+ "name": "Azure Key Vault",
+ "connection": "azure-key-vault",
+ "destination": "azure-key-vault",
+ "canImportCertificates": false,
+ "canRemoveCertificates": true
+ }
+ ]
+}
+```
+
\ No newline at end of file
diff --git a/docs/docs.json b/docs/docs.json
index c264c2ef1..b1af69970 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -2492,19 +2492,18 @@
{
"group": "Certificate Syncs",
"pages": [
- "api-reference/endpoints/certificate-syncs/list",
- "api-reference/endpoints/certificate-syncs/options",
+ "api-reference/endpoints/pki/syncs/list",
+ "api-reference/endpoints/pki/syncs/get-by-id",
{
"group": "Azure Key Vault",
"pages": [
- "api-reference/endpoints/certificate-syncs/azure-key-vault/list",
- "api-reference/endpoints/certificate-syncs/azure-key-vault/get-by-id",
- "api-reference/endpoints/certificate-syncs/azure-key-vault/create",
- "api-reference/endpoints/certificate-syncs/azure-key-vault/update",
- "api-reference/endpoints/certificate-syncs/azure-key-vault/delete",
- "api-reference/endpoints/certificate-syncs/azure-key-vault/sync-certificates",
- "api-reference/endpoints/certificate-syncs/azure-key-vault/import-certificates",
- "api-reference/endpoints/certificate-syncs/azure-key-vault/remove-certificates"
+ "api-reference/endpoints/pki/syncs/azure-key-vault/list",
+ "api-reference/endpoints/pki/syncs/azure-key-vault/get-by-id",
+ "api-reference/endpoints/pki/syncs/azure-key-vault/create",
+ "api-reference/endpoints/pki/syncs/azure-key-vault/update",
+ "api-reference/endpoints/pki/syncs/azure-key-vault/delete",
+ "api-reference/endpoints/pki/syncs/azure-key-vault/sync-certificates",
+ "api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates"
]
}
]
@@ -2647,9 +2646,9 @@
"href": "https://infisical.com"
},
"api": {
- "openapi": "https://5e8f77f30103.ngrok-free.app/api/docs/json",
+ "openapi": "https://api.infisical.com/api/docs/json",
"mdx": {
- "server": ["https://5e8f77f30103.ngrok-free.app"]
+ "server": ["https://api.infisical.com"]
}
},
"appearance": {
diff --git a/docs/documentation/platform/pki/certificate-syncs.mdx b/docs/documentation/platform/pki/certificate-syncs.mdx
index 51191035e..77bcfb9f8 100644
--- a/docs/documentation/platform/pki/certificate-syncs.mdx
+++ b/docs/documentation/platform/pki/certificate-syncs.mdx
@@ -1,8 +1,102 @@
---
-sidebarTitle: "Explore Options"
-description: "Browse and search through all available certificate syncs for Infisical PKI."
+sidebarTitle: "Certificate Syncs"
+title: "PKI Certificate Syncs"
+description: "Automatically synchronize your PKI certificates to external destinations and maintain certificate lifecycle management."
+---
+
+# PKI Certificate Syncs
+
+PKI Certificate Syncs enable automatic synchronization of certificates from Infisical to external destinations like cloud key management services. This ensures your certificates are consistently deployed and managed across your infrastructure.
+
+## Overview
+
+Certificate syncs work by:
+
+1. **Connecting to PKI Subscribers**: Link syncs to PKI subscribers to automatically sync certificates when they're issued
+2. **Destination Integration**: Configure destinations like Azure Key Vault through app connections
+3. **Automatic Synchronization**: Certificates are automatically pushed to destinations when issued or when manually triggered
+4. **Lifecycle Management**: Remove expired or revoked certificates from destinations to maintain security hygiene
+
+## Supported Destinations
+
+### Azure Key Vault
+
+Azure Key Vault integration supports:
+
+- ✅ **Certificate Upload**: Sync certificates with their private keys to Azure Key Vault
+- ✅ **Certificate Removal**: Clean up expired or revoked certificates
+- ✅ **Auto Key Detection**: Automatically detect and configure RSA/ECDSA key properties
+- ❌ **Certificate Import**: Cannot import certificates from Azure Key Vault to Infisical (Azure security limitation)
+
+**Key Features:**
+- Certificates are uploaded with both certificate and private key in PEM format
+- Key properties (RSA/ECDSA type, key size, curve) are automatically detected from certificates
+- Certificates are prefixed with "Infisical-" for identification and management
+- Respects Azure Key Vault rate limits with automatic retry logic
+
+## Configuration
+
+### Prerequisites
+
+1. **App Connection**: Create an Azure Key Vault app connection with appropriate permissions
+2. **PKI Subscriber**: Set up a PKI subscriber to issue certificates
+3. **Azure Permissions**: Ensure the connection has certificate create/update/delete permissions in the target Key Vault
+
+### Setting Up a Sync
+
+1. Navigate to your project's PKI section
+2. Go to the Certificate Syncs tab
+3. Create a new sync:
+ - **Name**: Descriptive name for the sync
+ - **Connection**: Select your Azure Key Vault app connection
+ - **Destination Config**: Specify the Azure Key Vault URL
+ - **PKI Subscriber**: Link to a specific subscriber (optional)
+ - **Auto Sync**: Enable automatic synchronization on certificate issuance
+ - **Certificate Name Schema**: Customize how certificate names are generated (optional)
+
+### Sync Options
+
+- **Auto Sync Enabled**: Automatically sync certificates when they're issued
+- **Manual Sync**: Trigger synchronization on-demand via API or UI
+- **Selective Removal**: Only remove certificates managed by Infisical
+
+## API Reference
+
+All PKI sync operations are available via REST API:
+
+- **[List PKI Sync Options](/api-reference/endpoints/pki/syncs/options)**: Get available sync destinations
+- **[List PKI Syncs](/api-reference/endpoints/pki/syncs/list)**: List all syncs for a project
+- **[Get PKI Sync](/api-reference/endpoints/pki/syncs/get-by-id)**: Get sync details by ID
+
+### Azure Key Vault Specific
+
+- **[Create Azure Key Vault Sync](/api-reference/endpoints/pki/syncs/azure-key-vault/create)**
+- **[Update Azure Key Vault Sync](/api-reference/endpoints/pki/syncs/azure-key-vault/update)**
+- **[Delete Azure Key Vault Sync](/api-reference/endpoints/pki/syncs/azure-key-vault/delete)**
+- **[Sync Certificates](/api-reference/endpoints/pki/syncs/azure-key-vault/sync-certificates)**
+- **[Remove Certificates](/api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates)**
+
+## Security Considerations
+
+- **Least Privilege**: Grant minimal required permissions to app connections
+- **Certificate Prefixing**: Only certificates with "Infisical-" prefix are managed by syncs
+- **Audit Logs**: All sync operations are logged and auditable
+- **Rate Limiting**: Built-in rate limiting prevents overwhelming destination services
+
+## Monitoring and Troubleshooting
+
+Each PKI sync tracks:
+- **Sync Status**: Last sync operation status and message
+- **Remove Status**: Last certificate removal status and message
+- **Timestamps**: When operations were last performed
+- **Error Messages**: Detailed error information for failed operations
+
+Use these fields to monitor sync health and troubleshoot issues.
+
---
import { CertificateSyncsBrowser } from "/snippets/CertificateSyncsBrowser.jsx";
+## Browse Available Syncs
+
\ No newline at end of file
diff --git a/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx b/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx
index f6f3c62de..55da5232b 100644
--- a/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx
+++ b/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx
@@ -45,6 +45,7 @@ description: "Learn how to configure an Azure Key Vault Certificate Sync for Inf
- **Auto-Sync Enabled**: If enabled, certificates will automatically be synced from the source PKI subscriber when changes occur. Disable to enforce manual syncing only.
- **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}}`.
6. Configure the **Details** of your Azure Key Vault Certificate Sync, then click **Next**.

@@ -60,13 +61,13 @@ description: "Learn how to configure an Azure Key Vault Certificate Sync for Inf
- To create an **Azure Key Vault Certificate Sync**, make an API request to the [Create Azure Key Vault Certificate Sync](/api-reference/endpoints/certificate-syncs/azure-key-vault/create) API endpoint.
+ To create an **Azure Key Vault Certificate Sync**, make an API request to the [Create Azure Key Vault Certificate Sync](/api-reference/endpoints/pki/syncs/azure-key-vault/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
- --url https://app.infisical.com/api/v1/pki-syncs \
+ --url https://app.infisical.com/api/v1/pki/syncs/azure-key-vault \
--header 'Content-Type: application/json' \
--data '{
"name": "my-key-vault-cert-sync",
@@ -77,7 +78,8 @@ description: "Learn how to configure an Azure Key Vault Certificate Sync for Inf
"destination": "azure-key-vault",
"isAutoSyncEnabled": true,
"syncOptions": {
- "canRemoveCertificates": true
+ "canRemoveCertificates": true,
+ "certificateNameSchema": "myapp-{{certificateId}}"
},
"destinationConfig": {
"vaultBaseUrl": "https://my-key-vault.vault.azure.net"
@@ -99,7 +101,8 @@ description: "Learn how to configure an Azure Key Vault Certificate Sync for Inf
"vaultBaseUrl": "https://my-key-vault.vault.azure.net"
},
"syncOptions": {
- "canRemoveCertificates": true
+ "canRemoveCertificates": true,
+ "certificateNameSchema": "myapp-{{certificateId}}"
},
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"subscriberId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
@@ -125,12 +128,17 @@ Your Azure Key Vault Certificate Sync will:
Azure Key Vault Certificate Syncs support both automatic and manual synchronization modes. When auto-sync is enabled, certificates are automatically deployed as they are issued or renewed.
-## Manual Certificate Import
+## Manual Certificate Sync
-You can manually import existing certificates from your PKI subscriber to Azure Key Vault using the import certificates functionality. This is useful for:
+You can manually trigger certificate synchronization from your PKI subscriber to Azure Key Vault using the sync certificates functionality. This is useful for:
-- Initial setup when you have existing certificates to migrate
-- One-time imports of specific certificates
+- Initial setup when you have existing certificates to deploy
+- One-time sync of specific certificates
- Testing certificate sync configurations
+- Force sync after making changes
-To manually import certificates, use the [Import Certificates](/api-reference/endpoints/certificate-syncs/azure-key-vault/import) API endpoint or the manual import option in the Infisical UI.
\ No newline at end of file
+To manually sync certificates, use the [Sync Certificates](/api-reference/endpoints/pki/syncs/azure-key-vault/sync-certificates) API endpoint or the manual sync option in the Infisical UI.
+
+
+Azure Key Vault does not support importing certificates back into Infisical due to security limitations where private keys cannot be extracted from Azure Key Vault.
+
\ No newline at end of file
diff --git a/docs/documentation/platform/pki/certificate-syncs/overview.mdx b/docs/documentation/platform/pki/certificate-syncs/overview.mdx
index b20c7ff8e..085729416 100644
--- a/docs/documentation/platform/pki/certificate-syncs/overview.mdx
+++ b/docs/documentation/platform/pki/certificate-syncs/overview.mdx
@@ -75,13 +75,15 @@ via the UI or API for the third-party service you intend to sync certificates to
2. Create Certificate Sync: Configure a Certificate Sync in the desired project by specifying the following parameters via the UI or API:
- Source: The PKI subscriber you wish to retrieve certificates from.
- Destination: The App Connection to utilize and the destination endpoint to deploy certificates to. These can vary between services.
- - Options: Customize how certificates should be synced, such as whether or not certificates should be removed from the destination when they expire.
+ - Options: Customize how certificates should be synced, including:
+ - Whether certificates should be removed from the destination when they expire
+ - Certificate naming schema to control how certificate names are generated in the destination
- Certificate Syncs are the source of truth for connected third-party services. Any certificate,
- including associated data, not present or managed by Infisical before syncing will be
- overwritten, and changes made directly in the connected service outside of Infisical may also
- be overwritten by future syncs.
+ Certificate Syncs manage certificates that are prefixed with "Infisical-" in the destination. Only
+ certificates managed by Infisical will be affected during sync operations. Certificates not created or
+ managed by Infisical will remain untouched, and changes made to Infisical-managed certificates directly
+ in the destination service may be overwritten by future syncs.
@@ -95,6 +97,31 @@ via the UI or API for the third-party service you intend to sync certificates to
contact us at team@infisical.com to make a request.
+## Certificate Naming
+
+Certificate Syncs support flexible certificate naming through configurable naming schemas. This allows you to customize how certificate names appear in your destination services.
+
+### Default Naming
+
+By default, certificates are named using the pattern `Infisical-{certificateId}` where `{certificateId}` is the unique identifier of the certificate with hyphens removed for compatibility with services like Azure Key Vault.
+
+### Custom Naming Schema
+
+You can customize certificate naming by providing a **Certificate Name Schema** when creating or updating a Certificate Sync. The schema supports the following placeholders:
+
+- `{{certificateId}}` - The unique certificate identifier (required)
+- `{{environment}}` - The environment context (always "global" for PKI syncs)
+
+**Examples:**
+- `myapp-{{certificateId}}` → `myapp-abc123def456`
+- `{{environment}}-cert-{{certificateId}}` → `global-cert-abc123def456`
+- `ssl/{{certificateId}}` → `ssl/abc123def456`
+
+**Rules:**
+- Must include exactly one `{{certificateId}}` placeholder
+- Only alphanumeric characters, dashes (-), underscores (_), and slashes (/) are allowed
+- Certificate names matching your schema will be managed by Infisical during sync operations
+
## Certificate Management
Certificate Syncs handle the full lifecycle of certificate management:
diff --git a/docs/images/certificate-syncs/azure-key-vault/upgrade-path-tool.png b/docs/images/certificate-syncs/azure-key-vault/upgrade-path-tool.png
deleted file mode 100644
index a8a538aaf..000000000
Binary files a/docs/images/certificate-syncs/azure-key-vault/upgrade-path-tool.png and /dev/null differ
diff --git a/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx b/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx
index d2ed96ae0..26911f139 100644
--- a/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx
+++ b/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx
@@ -15,7 +15,7 @@ type ContentProps = {
};
const Content = ({ pkiSync, onComplete }: ContentProps) => {
- const { id: syncId, destination, projectId } = pkiSync;
+ const { id: syncId, destination } = pkiSync;
const destinationName = PKI_SYNC_MAP[destination].name;
const triggerImportCertificates = useTriggerPkiSyncImportCertificates();
@@ -24,7 +24,6 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => {
try {
await triggerImportCertificates.mutateAsync({
syncId,
- projectId,
destination
});
@@ -57,7 +56,7 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => {
This operation will retrieve certificates from {destinationName} and make them available in
- your PKI collection. Only certificates that are not already imported will be processed.
+ your PKI subscriber. Only certificates that are not already imported will be processed.
- Certificate Syncs are the source of truth for connected third-party services. Any
- certificate, including associated data, not present or imported in Infisical before
- syncing will be overwritten, and changes made directly in the connected service outside
- of infisical may also be overwritten by future syncs.
+ Certificate Syncs manage certificates that are prefixed with "Infisical-" in
+ the destination. Only certificates managed by Infisical will be affected during sync
+ operations. Certificates not created or managed by Infisical will remain untouched, and
+ changes made to Infisical-managed certificates directly in the destination service may
+ be overwritten by future syncs.