Add PKI Syncs docs and a few improvements on the router

This commit is contained in:
Carlos Monastyrski
2025-09-17 10:49:31 -03:00
parent bc04fc6113
commit 8130be5e2f
52 changed files with 1124 additions and 690 deletions

View File

@@ -50,6 +50,7 @@ export enum ApiDocsTags {
IdentitySpecificPrivilegesV2 = "Identity Specific Privileges V2",
AppConnections = "App Connections",
SecretSyncs = "Secret Syncs",
PkiSyncs = "PKI Syncs",
Integrations = "Integrations",
ServiceTokens = "Service Tokens",
AuditLogs = "Audit Logs",

View File

@@ -48,7 +48,7 @@ import { registerPasswordRouter } from "./password-router";
import { registerPkiAlertRouter } from "./pki-alert-router";
import { registerPkiCollectionRouter } from "./pki-collection-router";
import { registerPkiSubscriberRouter } from "./pki-subscriber-router";
import { registerPkiSyncRouter } from "./pki-sync-router";
import { PKI_SYNC_REGISTER_ROUTER_MAP, registerPkiSyncRouter } from "./pki-sync-routers";
import { registerProjectEnvRouter } from "./project-env-router";
import { registerProjectKeyRouter } from "./project-key-router";
import { registerProjectMembershipRouter } from "./project-membership-router";
@@ -157,7 +157,16 @@ 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(registerPkiSyncRouter, { prefix: "/pki-syncs" });
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) => {

View File

@@ -1,540 +0,0 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { logger } from "@app/lib/logger";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { AzureKeyVaultPkiSyncConfigSchema } from "@app/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-types";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { PkiSyncDetailsSchema, PkiSyncListItemSchema, PkiSyncSchema } from "@app/services/pki-sync/pki-sync-schemas";
import { TCreatePkiSyncDTO, TUpdatePkiSyncDTO } from "@app/services/pki-sync/pki-sync-types";
const CreatePkiSyncRequestBodySchema = z.object({
name: z.string().trim().min(1).max(64),
description: z.string().optional(),
destination: z.nativeEnum(PkiSync),
isAutoSyncEnabled: z.boolean().default(true),
destinationConfig: z
.discriminatedUnion("destination", [
z.object({
destination: z.literal(PkiSync.AzureKeyVault),
config: AzureKeyVaultPkiSyncConfigSchema
})
])
.transform(({ config }) => config),
syncOptions: z.record(z.unknown()).default({}),
subscriberId: z.string().optional(),
connectionId: z.string(),
projectId: z.string().trim().min(1)
});
const UpdatePkiSyncRequestBodySchema = z.object({
name: z.string().trim().min(1).max(64).optional(),
description: z.string().optional(),
isAutoSyncEnabled: z.boolean().optional(),
destinationConfig: z.record(z.unknown()).optional(),
syncOptions: z.record(z.unknown()).optional(),
subscriberId: z.string().optional(),
connectionId: z.string().optional()
});
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/options",
config: {
rateLimit: readLimit
},
schema: {
description: "Get PKI sync options",
security: [
{
bearerAuth: []
}
],
response: {
200: {
description: "PKI sync options retrieved successfully",
content: {
"application/json": {
schema: z.object({
pkiSyncOptions: z.array(
z.object({
name: z.string(),
destination: z.nativeEnum(PkiSync),
canImportCertificates: z.boolean(),
canRemoveCertificates: z.boolean(),
enterprise: z.boolean().optional()
})
)
})
}
}
}
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async () => {
const pkiSyncOptions = [
{
name: "Azure Key Vault",
destination: PkiSync.AzureKeyVault,
canImportCertificates: true,
canRemoveCertificates: true,
enterprise: false
}
];
return { pkiSyncOptions };
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
description: "Create PKI sync",
security: [
{
bearerAuth: []
}
],
requestBody: {
content: {
"application/json": {
schema: CreatePkiSyncRequestBodySchema
}
}
},
response: {
200: {
description: "PKI sync created successfully",
content: {
"application/json": {
schema: z.object({
pkiSync: PkiSyncSchema
})
}
}
}
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
const requestBody = CreatePkiSyncRequestBodySchema.parse(req.body);
const createData: Omit<TCreatePkiSyncDTO, "auditLogInfo"> = requestBody;
try {
const pkiSync = await server.services.pkiSync.createPkiSync(createData, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: createData.projectId,
event: {
type: EventType.CREATE_PKI_SYNC,
metadata: {
pkiSyncId: pkiSync.id,
name: pkiSync.name,
destination: pkiSync.destination
}
}
});
return { pkiSync };
} catch (error) {
logger.error("Failed to create PKI sync");
logger.error(error);
throw error;
}
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
description: "List PKI syncs",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: {
description: "PKI syncs retrieved successfully",
content: {
"application/json": {
schema: z.object({
pkiSyncs: z.array(PkiSyncListItemSchema)
})
}
}
}
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId(
{
projectId: req.query.projectId
},
req.permission
);
return { pkiSyncs };
}
});
server.route({
method: "GET",
url: "/:pkiSyncId",
config: {
rateLimit: readLimit
},
schema: {
description: "Get PKI sync by ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
pkiSyncId: z.string()
}),
querystring: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: {
description: "PKI sync retrieved successfully",
content: {
"application/json": {
schema: z.object({
pkiSync: PkiSyncDetailsSchema
})
}
}
}
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
const pkiSync = await server.services.pkiSync.findPkiSyncById(
{
id: req.params.pkiSyncId,
projectId: req.query.projectId
},
req.permission
);
return { pkiSync };
}
});
server.route({
method: "PATCH",
url: "/:pkiSyncId",
config: {
rateLimit: readLimit
},
schema: {
description: "Update PKI sync",
security: [
{
bearerAuth: []
}
],
params: z.object({
pkiSyncId: z.string()
}),
querystring: z.object({
projectId: z.string().trim().min(1)
}),
requestBody: {
content: {
"application/json": {
schema: UpdatePkiSyncRequestBodySchema
}
}
},
response: {
200: {
description: "PKI sync updated successfully",
content: {
"application/json": {
schema: z.object({
pkiSync: PkiSyncSchema
})
}
}
}
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
const requestBody = UpdatePkiSyncRequestBodySchema.parse(req.body);
const updateData: Omit<TUpdatePkiSyncDTO, "auditLogInfo"> = {
id: req.params.pkiSyncId,
projectId: req.query.projectId,
...requestBody
};
try {
const pkiSync = await server.services.pkiSync.updatePkiSync(updateData, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.projectId,
event: {
type: EventType.UPDATE_PKI_SYNC,
metadata: {
pkiSyncId: pkiSync.id,
name: pkiSync.name
}
}
});
return { pkiSync };
} catch (error) {
logger.error("Failed to update PKI sync");
logger.error(error);
throw error;
}
}
});
server.route({
method: "DELETE",
url: "/:pkiSyncId",
config: {
rateLimit: readLimit
},
schema: {
description: "Delete PKI sync",
security: [
{
bearerAuth: []
}
],
params: z.object({
pkiSyncId: z.string()
}),
querystring: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: {
description: "PKI sync deleted successfully",
content: {
"application/json": {
schema: z.object({
pkiSync: z.object({
id: z.string(),
name: z.string(),
destination: z.nativeEnum(PkiSync)
})
})
}
}
}
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
try {
const pkiSync = await server.services.pkiSync.deletePkiSync(
{
id: req.params.pkiSyncId,
projectId: req.query.projectId
},
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.projectId,
event: {
type: EventType.DELETE_PKI_SYNC,
metadata: {
pkiSyncId: pkiSync.id,
name: pkiSync.name,
destination: pkiSync.destination
}
}
});
return { pkiSync };
} catch (error) {
logger.error("Failed to delete PKI sync");
logger.error(error);
throw error;
}
}
});
server.route({
method: "POST",
url: "/:pkiSyncId/sync",
config: {
rateLimit: readLimit
},
schema: {
description: "Trigger PKI sync",
security: [
{
bearerAuth: []
}
],
params: z.object({
pkiSyncId: z.string()
}),
querystring: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: {
description: "PKI sync triggered successfully",
content: {
"application/json": {
schema: z.object({
message: z.string()
})
}
}
}
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
try {
const result = await server.services.pkiSync.triggerPkiSyncSyncCertificatesById(
{
id: req.params.pkiSyncId,
projectId: req.query.projectId
},
req.permission
);
return result;
} catch (error) {
logger.error("Failed to trigger PKI sync");
logger.error(error);
throw error;
}
}
});
server.route({
method: "POST",
url: "/:pkiSyncId/import",
config: {
rateLimit: readLimit
},
schema: {
description: "Import certificates from PKI sync destination",
security: [
{
bearerAuth: []
}
],
params: z.object({
pkiSyncId: z.string()
}),
querystring: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: {
description: "PKI sync import triggered successfully",
content: {
"application/json": {
schema: z.object({
message: z.string()
})
}
}
}
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
try {
const result = await server.services.pkiSync.triggerPkiSyncImportCertificatesById(
{
id: req.params.pkiSyncId,
projectId: req.query.projectId
},
req.permission
);
return result;
} catch (error) {
logger.error("Failed to trigger PKI sync import certificates");
logger.error(error);
throw error;
}
}
});
server.route({
method: "POST",
url: "/:pkiSyncId/remove",
config: {
rateLimit: readLimit
},
schema: {
description: "Remove certificates from PKI sync destination",
security: [
{
bearerAuth: []
}
],
params: z.object({
pkiSyncId: z.string()
}),
querystring: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: {
description: "PKI sync remove triggered successfully",
content: {
"application/json": {
schema: z.object({
message: z.string()
})
}
}
}
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
try {
const result = await server.services.pkiSync.triggerPkiSyncRemoveCertificatesById(
{
id: req.params.pkiSyncId,
projectId: req.query.projectId
},
req.permission
);
return result;
} catch (error) {
logger.error("Failed to trigger PKI sync remove certificates");
logger.error(error);
throw error;
}
}
});
};

View File

@@ -0,0 +1,17 @@
import {
AzureKeyVaultPkiSyncSchema,
CreateAzureKeyVaultPkiSyncSchema,
UpdateAzureKeyVaultPkiSyncSchema
} from "@app/services/pki-sync/azure-key-vault";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { registerSyncPkiEndpoints } from "./pki-sync-endpoints";
export const registerAzureKeyVaultPkiSyncRouter = async (server: FastifyZodProvider) =>
registerSyncPkiEndpoints({
destination: PkiSync.AzureKeyVault,
server,
responseSchema: AzureKeyVaultPkiSyncSchema,
createSchema: CreateAzureKeyVaultPkiSyncSchema,
updateSchema: UpdateAzureKeyVaultPkiSyncSchema
});

View File

@@ -0,0 +1,9 @@
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { registerAzureKeyVaultPkiSyncRouter } from "./azure-key-vault-pki-sync-router";
export * from "./pki-sync-router";
export const PKI_SYNC_REGISTER_ROUTER_MAP: Record<PkiSync, (server: FastifyZodProvider) => Promise<void>> = {
[PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter
};

View File

@@ -0,0 +1,363 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { PKI_SYNC_NAME_MAP } from "@app/services/pki-sync/pki-sync-maps";
export const registerSyncPkiEndpoints = ({
server,
destination,
createSchema,
updateSchema,
responseSchema
}: {
destination: PkiSync;
server: FastifyZodProvider;
createSchema: z.ZodType<{
name: string;
projectId: string;
connectionId: string;
destinationConfig: Record<string, unknown>;
syncOptions?: Record<string, unknown>;
description?: string;
isAutoSyncEnabled?: boolean;
subscriberId?: string;
}>;
updateSchema: z.ZodType<{
connectionId?: string;
name?: string;
destinationConfig?: Record<string, unknown>;
syncOptions?: Record<string, unknown>;
description?: string;
isAutoSyncEnabled?: boolean;
subscriberId?: string;
}>;
responseSchema: z.ZodTypeAny;
}) => {
const destinationName = PKI_SYNC_NAME_MAP[destination];
server.route({
method: "GET",
url: `/`,
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `List the ${destinationName} PKI Syncs for the specified project.`,
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required")
}),
response: {
200: z.object({ pkiSyncs: responseSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
const {
query: { projectId }
} = req;
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_PKI_SYNCS,
metadata: {
projectId
}
}
});
return { pkiSyncs };
}
});
server.route({
method: "GET",
url: "/:pkiSyncId",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `Get the specified ${destinationName} PKI Sync by ID.`,
params: z.object({
pkiSyncId: z.string()
}),
querystring: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: z.object({ pkiSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const { projectId } = req.query;
const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId, projectId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiSync.projectId,
event: {
type: EventType.GET_PKI_SYNC,
metadata: {
syncId: pkiSyncId,
destination
}
}
});
return { pkiSync };
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `Create a ${destinationName} PKI Sync for the specified project.`,
body: createSchema,
response: {
200: z.object({ pkiSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
const pkiSync = await server.services.pkiSync.createPkiSync({ ...req.body, destination }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiSync.projectId,
event: {
type: EventType.CREATE_PKI_SYNC,
metadata: {
pkiSyncId: pkiSync.id,
name: pkiSync.name,
destination
}
}
});
return { pkiSync };
}
});
server.route({
method: "PATCH",
url: "/:pkiSyncId",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `Update the specified ${destinationName} PKI Sync.`,
params: z.object({
pkiSyncId: z.string()
}),
querystring: z.object({
projectId: z.string().trim().min(1)
}),
body: updateSchema,
response: {
200: z.object({ pkiSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_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
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.UPDATE_PKI_SYNC,
metadata: {
pkiSyncId,
name: pkiSync.name
}
}
});
return { pkiSync };
}
});
server.route({
method: "DELETE",
url: `/:pkiSyncId`,
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `Delete the specified ${destinationName} PKI Sync.`,
params: z.object({
pkiSyncId: z.string()
}),
querystring: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: z.object({ pkiSync: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const { projectId } = req.query;
const pkiSync = await server.services.pkiSync.deletePkiSync({ id: pkiSyncId, projectId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.DELETE_PKI_SYNC,
metadata: {
pkiSyncId,
name: pkiSync.name,
destination: pkiSync.destination
}
}
});
return { pkiSync };
}
});
server.route({
method: "POST",
url: "/:pkiSyncId/sync",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `Trigger a sync for the specified ${destinationName} PKI Sync.`,
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]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const { projectId } = req.query;
const result = await server.services.pkiSync.triggerPkiSyncSyncCertificatesById(
{
id: pkiSyncId,
projectId
},
req.permission
);
return result;
}
});
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() })
}
},
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",
url: "/:pkiSyncId/remove",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `Remove 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() })
}
},
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.triggerPkiSyncRemoveCertificatesById(
{
id: pkiSyncId,
projectId
},
req.permission
);
return result;
}
});
};

View File

@@ -0,0 +1,174 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { AuthMode } from "@app/services/auth/auth-type";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
const PkiSyncSchema = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable().optional(),
destination: z.nativeEnum(PkiSync),
isAutoSyncEnabled: z.boolean(),
destinationConfig: z.record(z.unknown()),
syncOptions: z.record(z.unknown()),
projectId: z.string().uuid(),
subscriberId: z.string().uuid().nullable().optional(),
connectionId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
// Sync status fields
syncStatus: z.string().nullable().optional(),
lastSyncJobId: z.string().nullable().optional(),
lastSyncMessage: z.string().nullable().optional(),
lastSyncedAt: z.date().nullable().optional(),
// Import status fields
importStatus: z.string().nullable().optional(),
lastImportJobId: z.string().nullable().optional(),
lastImportMessage: z.string().nullable().optional(),
lastImportedAt: z.date().nullable().optional(),
// Remove status fields
removeStatus: z.string().nullable().optional(),
lastRemoveJobId: z.string().nullable().optional(),
lastRemoveMessage: z.string().nullable().optional(),
lastRemovedAt: z.date().nullable().optional(),
// App connection info
appConnectionName: z.string(),
appConnectionApp: z.string(),
connection: z.object({
id: z.string(),
name: z.string(),
app: z.string(),
encryptedCredentials: z.unknown().nullable(),
orgId: z.string().uuid(),
projectId: z.string().uuid().nullable().optional(),
method: z.string(),
description: z.string().nullable().optional(),
version: z.number(),
gatewayId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
isPlatformManagedCredentials: z.boolean().nullable().optional()
})
});
const PkiSyncOptionsSchema = z.object({
name: z.string(),
connection: z.nativeEnum(AppConnection),
destination: z.nativeEnum(PkiSync),
canImportCertificates: z.boolean(),
canRemoveCertificates: z.boolean()
});
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/options",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: "List the available PKI Sync Options.",
response: {
200: z.object({
pkiSyncOptions: PkiSyncOptionsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: () => {
const pkiSyncOptions = server.services.pkiSync.getPkiSyncOptions();
return { pkiSyncOptions };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: "List all the PKI Syncs for the specified project.",
querystring: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: z.object({ pkiSyncs: PkiSyncSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
const {
query: { projectId },
permission
} = req;
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_PKI_SYNCS,
metadata: {
projectId
}
}
});
return { pkiSyncs };
}
});
server.route({
method: "GET",
url: "/:pkiSyncId",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: "Get a PKI Sync by ID.",
params: z.object({
pkiSyncId: z.string()
}),
querystring: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: z.object({ pkiSync: PkiSyncSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const { projectId } = req.query;
const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId, projectId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiSync.projectId,
event: {
type: EventType.GET_PKI_SYNC,
metadata: {
syncId: pkiSyncId,
destination: pkiSync.destination
}
}
});
return { pkiSync };
}
});
};

View File

@@ -4,13 +4,23 @@ import { AxiosError } from "axios";
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 { TKmsServiceFactory } from "@app/services/kms/kms-service";
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";
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
};
type TAzureKeyVaultPkiSyncFactoryDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
@@ -56,7 +66,6 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
lastSlashIndex = getAzureKeyVaultCertificate.id.lastIndexOf("/");
}
// Get the certificate details
const azureKeyVaultCertificate = await request.get<GetAzureKeyVaultCertificate>(
`${getAzureKeyVaultCertificate.id}?api-version=7.4`,
{
@@ -66,7 +75,6 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
}
);
// Convert base64 certificate to PEM format if available
let certPem = "";
if (azureKeyVaultCertificate.data.cer) {
try {
@@ -75,7 +83,6 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
const base64Cert = azureKeyVaultCertificate.data.cer;
certPem = `-----BEGIN CERTIFICATE-----\n${base64Cert.match(/.{1,64}/g)?.join("\n")}\n-----END CERTIFICATE-----`;
} catch (error) {
// If conversion fails, assume it's already in PEM format
certPem = azureKeyVaultCertificate.data.cer;
}
}
@@ -259,7 +266,6 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
{ certificateKey: key, syncId: pkiSync.id },
"Certificate exists in deleted but recoverable state in Azure Key Vault - skipping upload"
);
// Return a successful result to avoid failing the entire sync
return { key, success: false, skipped: true, reason: "Certificate in deleted but recoverable state" };
}

View File

@@ -0,0 +1,41 @@
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 AzureKeyVaultPkiSyncSchema = PkiSyncSchema.extend({
destination: z.literal(PkiSync.AzureKeyVault),
destinationConfig: AzureKeyVaultPkiSyncConfigSchema
});
export const CreateAzureKeyVaultPkiSyncSchema = z.object({
name: z.string().trim().min(1).max(64),
description: z.string().optional(),
isAutoSyncEnabled: z.boolean().default(true),
destinationConfig: AzureKeyVaultPkiSyncConfigSchema,
syncOptions: z.record(z.unknown()).optional().default({}),
subscriberId: z.string().optional(),
connectionId: z.string(),
projectId: z.string().trim().min(1)
});
export const UpdateAzureKeyVaultPkiSyncSchema = z.object({
name: z.string().trim().min(1).max(64).optional(),
description: z.string().optional(),
isAutoSyncEnabled: z.boolean().optional(),
destinationConfig: AzureKeyVaultPkiSyncConfigSchema.optional(),
syncOptions: z.record(z.unknown()).optional(),
subscriberId: z.string().optional(),
connectionId: z.string().optional()
});
export const AzureKeyVaultPkiSyncListItemSchema = z.object({
name: z.literal("Azure Key Vault"),
connection: z.literal(AppConnection.AzureKeyVault),
destination: z.literal(PkiSync.AzureKeyVault),
canImportCertificates: z.literal(false),
canRemoveCertificates: z.literal(true)
});

View File

@@ -0,0 +1,3 @@
export * from "./azure-key-vault-pki-sync-fns";
export * from "./azure-key-vault-pki-sync-schemas";
export * from "./azure-key-vault-pki-sync-types";

View File

@@ -21,6 +21,7 @@ const basePkiSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: PkiSyncF
db.ref("app").withSchema(TableName.AppConnection).as("appConnectionApp"),
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("appConnectionEncryptedCredentials"),
db.ref("orgId").withSchema(TableName.AppConnection).as("appConnectionOrgId"),
db.ref("projectId").withSchema(TableName.AppConnection).as("appConnectionProjectId"),
db.ref("method").withSchema(TableName.AppConnection).as("appConnectionMethod"),
db.ref("description").withSchema(TableName.AppConnection).as("appConnectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("appConnectionVersion"),
@@ -47,6 +48,7 @@ const expandPkiSync = (pkiSync: Awaited<ReturnType<typeof basePkiSyncQuery>>[num
appConnectionApp,
appConnectionEncryptedCredentials,
appConnectionOrgId,
appConnectionProjectId,
appConnectionMethod,
appConnectionDescription,
appConnectionVersion,
@@ -70,6 +72,7 @@ const expandPkiSync = (pkiSync: Awaited<ReturnType<typeof basePkiSyncQuery>>[num
app: appConnectionApp,
encryptedCredentials: appConnectionEncryptedCredentials,
orgId: appConnectionOrgId,
projectId: appConnectionProjectId,
method: appConnectionMethod,
description: appConnectionDescription,
version: appConnectionVersion,

View File

@@ -5,11 +5,16 @@ import { BadRequestError } from "@app/lib/errors";
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 { PkiSync } from "./pki-sync-enums";
import { TCertificateMap, TPkiSyncWithCredentials } from "./pki-sync-types";
const ENTERPRISE_PKI_SYNCS: PkiSync[] = [];
const PKI_SYNC_LIST_OPTIONS = {
[PkiSync.AzureKeyVault]: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION
};
export const enterprisePkiSyncCheck = async (
licenseService: Pick<TLicenseServiceFactory, "getPlan">,
orgId: string,
@@ -26,7 +31,7 @@ export const enterprisePkiSyncCheck = async (
};
export const listPkiSyncOptions = () => {
return Object.values(PkiSync);
return Object.values(PKI_SYNC_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name));
};
export const matchesSchema = <T extends ZodSchema>(schema: T, data: unknown): data is z.infer<T> => {

View File

@@ -216,7 +216,6 @@ export const pkiSyncQueueFactory = ({
encryptedCertificate
});
// Create certificate secret record with encrypted private key (if available)
if (certData.privateKey) {
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(certData.privateKey)
@@ -231,7 +230,6 @@ export const pkiSyncQueueFactory = ({
logger.info(`Successfully created certificate ${certData.name} with ID ${createdCert.id}`);
} catch (error) {
logger.error(`Failed to create certificate ${certData.name}: ${String(error)}`);
// Continue with other certificates even if one fails
}
}
};
@@ -264,7 +262,6 @@ export const pkiSyncQueueFactory = ({
for (const certificate of certificates) {
try {
// Only sync certificates issued by Infisical (not imported ones)
// Imported certificates don't have caId and certificateTemplateId
if (!certificate.caId) {
logger.debug(
{ certificateId: certificate.id, subscriberId },
@@ -503,15 +500,14 @@ export const pkiSyncQueueFactory = ({
try {
const {
connection: { orgId, encryptedCredentials },
projectId
connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId }
} = pkiSync;
const credentials = await decryptAppConnectionCredentials({
orgId,
encryptedCredentials,
kmsService,
projectId
projectId: appConnectionProjectId
});
const pkiSyncWithCredentials = {
@@ -564,7 +560,6 @@ export const pkiSyncQueueFactory = ({
if (err instanceof PkiSyncError && !err.shouldRetry) {
isFinalAttempt = true;
} else {
// re-throw so job fails
throw err;
}
} finally {
@@ -630,15 +625,14 @@ export const pkiSyncQueueFactory = ({
try {
const {
connection: { orgId, encryptedCredentials },
projectId
connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId }
} = pkiSync;
const credentials = await decryptAppConnectionCredentials({
orgId,
encryptedCredentials,
kmsService,
projectId
projectId: appConnectionProjectId
});
await $importCertificates({
@@ -673,7 +667,6 @@ export const pkiSyncQueueFactory = ({
if (err instanceof PkiSyncError && !err.shouldRetry) {
isFinalAttempt = true;
} else {
// re-throw so job fails
throw err;
}
} finally {
@@ -746,14 +739,14 @@ export const pkiSyncQueueFactory = ({
try {
const {
connection: { orgId, encryptedCredentials }
connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId }
} = pkiSync;
const credentials = await decryptAppConnectionCredentials({
orgId,
encryptedCredentials,
kmsService,
projectId: pkiSync.projectId
projectId: appConnectionProjectId
});
const certificateMap = await $getInfisicalCertificates(pkiSync);
@@ -797,7 +790,6 @@ export const pkiSyncQueueFactory = ({
if (err instanceof PkiSyncError && !err.shouldRetry) {
isFinalAttempt = true;
} else {
// re-throw so job fails
throw err;
}
} finally {
@@ -880,7 +872,6 @@ export const pkiSyncQueueFactory = ({
errorMessage = lastRemoveMessage || null;
}
// Log notification for now - actual email sending would require SMTP configuration
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}`

View File

@@ -40,7 +40,6 @@ export const PkiSyncListItemSchema = PkiSyncSchema.extend({
appConnectionApp: z.string().max(255)
});
// Schema for PKI sync details (includes app connection info)
export const PkiSyncDetailsSchema = PkiSyncSchema.extend({
appConnectionName: z.string().max(255),
appConnectionApp: z.string().max(255)

View File

@@ -103,6 +103,12 @@ export const pkiSyncServiceFactory = ({
// Validates permission to connect and app is valid for sync destination
await appConnectionService.connectAppConnectionById(destinationApp, connectionId, actor);
const defaultSyncOptions = {
canImportCertificates: false,
canRemoveCertificates: true,
...syncOptions
};
try {
const pkiSync = await pkiSyncDAL.create({
name,
@@ -110,7 +116,7 @@ export const pkiSyncServiceFactory = ({
destination,
isAutoSyncEnabled,
destinationConfig,
syncOptions,
syncOptions: defaultSyncOptions,
subscriberId,
connectionId,
projectId,

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/pki-syncs"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/pki-syncs/{pkiSyncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/pki-syncs/{pkiSyncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Import Certificates"
openapi: "POST /api/v1/pki-syncs/{pkiSyncId}/import"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/pki-syncs"
---

View File

@@ -0,0 +1,4 @@
---
title: "Remove Certificates"
openapi: "POST /api/v1/pki-syncs/{pkiSyncId}/remove"
---

View File

@@ -0,0 +1,4 @@
---
title: "Sync Certificates"
openapi: "POST /api/v1/pki-syncs/{pkiSyncId}/sync"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/pki-syncs/{pkiSyncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List PKI Syncs"
openapi: "GET /api/v1/pki-syncs"
---

View File

@@ -0,0 +1,4 @@
---
title: "Options"
openapi: "GET /api/v1/pki-syncs/options"
---

View File

@@ -711,6 +711,19 @@
"documentation/platform/pki/pki-issuer",
"documentation/platform/pki/integration-guides/gloo-mesh"
]
},
{
"group": "Certificate Syncs",
"pages": [
"documentation/platform/pki/certificate-syncs",
"documentation/platform/pki/certificate-syncs/overview",
{
"group": "Syncs",
"pages": [
"documentation/platform/pki/certificate-syncs/azure-key-vault"
]
}
]
}
]
}
@@ -2475,6 +2488,26 @@
"api-reference/endpoints/pki-alerts/update",
"api-reference/endpoints/pki-alerts/delete"
]
},
{
"group": "Certificate Syncs",
"pages": [
"api-reference/endpoints/certificate-syncs/list",
"api-reference/endpoints/certificate-syncs/options",
{
"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"
]
}
]
}
]
},
@@ -2614,9 +2647,9 @@
"href": "https://infisical.com"
},
"api": {
"openapi": "https://app.infisical.com/api/docs/json",
"openapi": "https://5e8f77f30103.ngrok-free.app/api/docs/json",
"mdx": {
"server": ["https://app.infisical.com"]
"server": ["https://5e8f77f30103.ngrok-free.app"]
}
},
"appearance": {

View File

@@ -0,0 +1,8 @@
---
sidebarTitle: "Explore Options"
description: "Browse and search through all available certificate syncs for Infisical PKI."
---
import { CertificateSyncsBrowser } from "/snippets/CertificateSyncsBrowser.jsx";
<CertificateSyncsBrowser />

View File

@@ -0,0 +1,136 @@
---
title: "Azure Key Vault"
description: "Learn how to configure an Azure Key Vault Certificate Sync for Infisical PKI."
---
**Prerequisites:**
- Set up and configure a [Certificate Authority](/documentation/platform/pki/overview)
- Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault)
- Ensure your network security policies allow incoming requests from Infisical to this certificate sync provider, if network restrictions apply.
<Note>
The Azure Key Vault Certificate Sync requires the following certificate permissions to be set on the user / service principal
for Infisical to sync certificates to Azure Key Vault: `certificates/list`, `certificates/get`, `certificates/import`, `certificates/delete`.
Any role with these permissions would work such as the **Key Vault Certificates Officer** role.
</Note>
<Note>
Certificates synced to Azure Key Vault will be stored as certificate objects, preserving both the certificate and private key components.
</Note>
<Tabs>
<Tab title="Infisical UI">
1. Navigate to **Project** > **Integrations** and select the **Certificate Syncs** tab. Click on the **Add Sync** button.
![Certificate Syncs Tab](/images/certificate-syncs/general/certificate-sync-tab.png)
2. Select the **Azure Key Vault** option.
![Select Key Vault](/images/certificate-syncs/azure-key-vault/select-key-vault-option.png)
3. Configure the **Source** from where certificates should be retrieved, then click **Next**.
![Configure Source](/images/certificate-syncs/azure-key-vault/vault-source.png)
- **PKI Subscriber**: The PKI subscriber to retrieve certificates from.
4. Configure the **Destination** to where certificates should be deployed, then click **Next**.
![Configure Destination](/images/certificate-syncs/azure-key-vault/vault-destination.png)
- **Azure Connection**: The Azure Connection to authenticate with.
- **Vault Base URL**: The URL of your Azure Key Vault.
<p class="height:1px" />
5. Configure the **Sync Options** to specify how certificates should be synced, then click **Next**.
![Configure Options](/images/certificate-syncs/azure-key-vault/vault-options.png)
- **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.
6. Configure the **Details** of your Azure Key Vault Certificate Sync, then click **Next**.
![Configure Details](/images/certificate-syncs/azure-key-vault/vault-details.png)
- **Name**: The name of your sync. Must be slug-friendly.
- **Description**: An optional description for your sync.
7. Review your Azure Key Vault Certificate Sync configuration, then click **Create Sync**.
![Confirm Configuration](/images/certificate-syncs/azure-key-vault/vault-review.png)
8. If enabled, your Azure Key Vault Certificate Sync will begin syncing your certificates to the destination endpoint.
![Sync Certificates](/images/certificate-syncs/azure-key-vault/vault-synced.png)
</Tab>
<Tab title="API">
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.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/pki-syncs \
--header 'Content-Type: application/json' \
--data '{
"name": "my-key-vault-cert-sync",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "an example certificate sync",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"subscriberId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"destination": "azure-key-vault",
"isAutoSyncEnabled": true,
"syncOptions": {
"canRemoveCertificates": true
},
"destinationConfig": {
"vaultBaseUrl": "https://my-key-vault.vault.azure.net"
}
}'
```
### Sample response
```json Response
{
"pkiSync": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-key-vault-cert-sync",
"description": "an example certificate sync",
"destination": "azure-key-vault",
"isAutoSyncEnabled": true,
"destinationConfig": {
"vaultBaseUrl": "https://my-key-vault.vault.azure.net"
},
"syncOptions": {
"canRemoveCertificates": true
},
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"subscriberId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-01-01T00:00:00.000Z",
"updatedAt": "2023-01-01T00:00:00.000Z"
}
}
```
</Tab>
</Tabs>
## Certificate Management
Your Azure Key Vault Certificate Sync will:
- **Automatic Deployment**: Deploy new certificates issued by your PKI subscriber to Azure Key Vault
- **Certificate Updates**: Update certificates in Azure Key Vault when renewals occur
- **Expiration Handling**: Optionally remove expired certificates from Azure Key Vault (if enabled)
- **Format Preservation**: Maintain certificate format and metadata during sync operations
<Note>
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.
</Note>
## Manual Certificate Import
You can manually import existing certificates from your PKI subscriber to Azure Key Vault using the import certificates functionality. This is useful for:
- Initial setup when you have existing certificates to migrate
- One-time imports of specific certificates
- Testing certificate sync configurations
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.

View File

@@ -0,0 +1,118 @@
---
sidebarTitle: "Overview"
description: "Learn how to sync certificates from Infisical PKI to third-party services."
---
Certificate Syncs enable you to sync certificates from Infisical PKI to third-party services using [App Connections](/integrations/app-connections/overview).
<Note>
Certificate Syncs are designed to automatically deploy certificates issued by your Certificate Authority to external services, ensuring your certificates are always up-to-date across your infrastructure.
</Note>
## Concept
Certificate Syncs are a project-level resource used to sync certificates, via an [App Connection](/integrations/app-connections/overview), from a particular PKI subscriber (source)
to a third-party service (destination). When new certificates are issued or existing certificates are renewed, changes will automatically be propagated to the destination, ensuring
your certificates are always current.
<br />
<div align="center">
```mermaid
%%{init: {'flowchart': {'curve': 'linear'} } }%%
graph LR
A[App Connection]
B[Certificate Sync]
C[Certificate 1]
D[Certificate 2]
E[Certificate 3]
F[Third-Party Service]
G[Certificate 1]
H[Certificate 2]
I[Certificate 3]
J[PKI Subscriber]
B --> A
C --> J
D --> J
E --> J
A --> F
F --> G
F --> H
F --> I
J --> B
classDef default fill:#ffffff,stroke:#666,stroke-width:2px,rx:10px,color:black
classDef connection fill:#FFF2B2,stroke:#E6C34A,stroke-width:2px,color:black,rx:15px
classDef certificate fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px
classDef sync fill:#F4FFE6,stroke:#96D600,stroke-width:2px,color:black,rx:15px
classDef service fill:#E6E6FF,stroke:#6B4E96,stroke-width:2px,color:black,rx:15px
classDef subscriber fill:#FFE6E6,stroke:#D63F3F,stroke-width:2px,color:black,rx:15px
class A connection
class B sync
class C,D,E,G,H,I certificate
class F service
class J subscriber
```
</div>
## Workflow
Configuring a Certificate Sync requires three components: a <strong>source</strong> PKI subscriber to retrieve certificates from,
a <strong>destination</strong> endpoint to deploy certificates to, and <strong>configuration options</strong> to determine how your certificates
should be synced. Follow these steps to start syncing:
<Note>
For step-by-step guides on syncing to a particular third-party service, refer to the Certificate Syncs section in the Navigation Bar.
</Note>
1. <strong>Create App Connection:</strong> If you have not already done so, create an [App Connection](/integrations/app-connections/overview)
via the UI or API for the third-party service you intend to sync certificates to.
2. <strong>Create Certificate Sync:</strong> Configure a Certificate Sync in the desired project by specifying the following parameters via the UI or API:
- <strong>Source:</strong> The PKI subscriber you wish to retrieve certificates from.
- <strong>Destination:</strong> The App Connection to utilize and the destination endpoint to deploy certificates to. These can vary between services.
- <strong>Options:</strong> Customize how certificates should be synced, such as whether or not certificates should be removed from the destination when they expire.
<Note>
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.
</Note>
<Info>
Some third-party services do not support removing expired certificates automatically.
</Info>
3. <strong>Utilize Sync:</strong> Any new certificates issued or renewals from the source PKI subscriber will now automatically be propagated to the destination endpoint.
<Note>
Infisical is continuously expanding its Certificate Sync third-party service support. If the service you need isn't available,
contact us at team@infisical.com to make a request.
</Note>
## Certificate Management
Certificate Syncs handle the full lifecycle of certificate management:
- **Automatic Deployment**: New certificates are automatically deployed to configured destinations
- **Renewal Propagation**: Certificate renewals are seamlessly pushed to all connected services
- **Expiration Handling**: Expired certificates can be automatically removed from destinations (service-dependent)
- **Certificate Validation**: Certificates are validated before deployment to ensure integrity
<div align="center">
```mermaid
graph LR
A[Certificate Issued] -->|Deploy| B[Destination Service]
C[Certificate Renewed] -->|Update| B
D[Certificate Expired] -->|Remove| B
style B fill:#F4FFE6,stroke:#96D600,stroke-width:2px,color:black,rx:15px
style A fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px
style C fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px
style D fill:#FFE6E6,stroke:#D63F3F,stroke-width:2px,color:black,rx:15px
```
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 KiB

View File

@@ -23,7 +23,8 @@ export const DeletePkiSyncModal = ({ isOpen, onOpenChange, pkiSync, onComplete }
try {
await deleteSync.mutateAsync({
syncId,
projectId
projectId,
destination
});
createNotification({

View File

@@ -24,7 +24,8 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => {
try {
await triggerImportCertificates.mutateAsync({
syncId,
projectId
projectId,
destination
});
createNotification({

View File

@@ -24,7 +24,8 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => {
try {
await triggerRemoveCertificates.mutateAsync({
syncId,
projectId
projectId,
destination
});
createNotification({

View File

@@ -10,13 +10,7 @@ import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Switch } from "@app/components/v2";
import { useProject } from "@app/context";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import {
PkiSync,
TCreatePkiSyncDTO,
TPkiSync,
useCreatePkiSync,
usePkiSyncOption
} from "@app/hooks/api/pkiSyncs";
import { PkiSync, TPkiSync, useCreatePkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
@@ -69,10 +63,7 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props)
...formData,
connectionId: connection.id,
projectId: currentProject.id,
destinationConfig: {
destination,
config: destinationConfig
} as unknown as TCreatePkiSyncDTO["destinationConfig"]
destinationConfig
});
createNotification({

View File

@@ -43,7 +43,8 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => {
syncId: pkiSync.id,
...formData,
connectionId: connection.id,
projectId: pkiSync.projectId
projectId: pkiSync.projectId,
destination: pkiSync.destination
});
createNotification({

View File

@@ -12,6 +12,8 @@ export const PkiSyncOptionsFields = () => {
return (
<>
<p className="mb-4 text-sm text-bunker-300">Configure how certificates should be synced.</p>
{/*
TODO: Re-enable this when we have a way to import certificates
<Controller
control={control}
name="syncOptions.canImportCertificates"
@@ -48,6 +50,7 @@ export const PkiSyncOptionsFields = () => {
</FormControl>
)}
/>
*/}
<Controller
control={control}
name="syncOptions.canRemoveCertificates"

View File

@@ -70,6 +70,7 @@ export const PkiSyncReviewFields = () => {
<GenericFieldLabel label="Upload Certificates">
<Badge variant="success">Always Enabled</Badge>
</GenericFieldLabel>
{/* Hidden for now - Import certificates functionality disabled
{syncOptions?.canImportCertificates !== undefined && (
<GenericFieldLabel label="Import Certificates">
<Badge variant={syncOptions.canImportCertificates ? "success" : "danger"}>
@@ -77,6 +78,7 @@ export const PkiSyncReviewFields = () => {
</Badge>
</GenericFieldLabel>
)}
*/}
{syncOptions?.canRemoveCertificates !== undefined && (
<GenericFieldLabel label="Remove Certificates">
<Badge variant={syncOptions.canRemoveCertificates ? "success" : "danger"}>

View File

@@ -20,7 +20,7 @@ export const PkiSyncFormSchema = z.object({
vaultBaseUrl: z.string().url("Valid URL is required")
}),
syncOptions: z.object({
canImportCertificates: z.boolean().default(true),
canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(true)
})
});

View File

@@ -15,8 +15,11 @@ import {
export const useCreatePkiSync = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (params: TCreatePkiSyncDTO) => {
const { data } = await apiRequest.post<TPkiSyncResponse>("/api/v1/pki-syncs", params);
mutationFn: async ({ destination, ...params }: TCreatePkiSyncDTO) => {
const { data } = await apiRequest.post<TPkiSyncResponse>(
`/api/v1/pki-syncs/${destination}`,
params
);
return data.pkiSync;
},
@@ -28,9 +31,9 @@ export const useCreatePkiSync = () => {
export const useUpdatePkiSync = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, projectId, ...params }: TUpdatePkiSyncDTO) => {
mutationFn: async ({ syncId, projectId, destination, ...params }: TUpdatePkiSyncDTO) => {
const { data } = await apiRequest.patch<TPkiSyncResponse>(
`/api/v1/pki-syncs/${syncId}`,
`/api/v1/pki-syncs/${destination}/${syncId}`,
params,
{ params: { projectId } }
);
@@ -47,8 +50,8 @@ export const useUpdatePkiSync = () => {
export const useDeletePkiSync = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, projectId }: TDeletePkiSyncDTO) => {
const { data } = await apiRequest.delete(`/api/v1/pki-syncs/${syncId}`, {
mutationFn: async ({ syncId, projectId, destination }: TDeletePkiSyncDTO) => {
const { data } = await apiRequest.delete(`/api/v1/pki-syncs/${destination}/${syncId}`, {
params: { projectId }
});
@@ -64,9 +67,9 @@ export const useDeletePkiSync = () => {
export const useTriggerPkiSyncSyncCertificates = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, projectId }: TTriggerPkiSyncSyncCertificatesDTO) => {
mutationFn: async ({ syncId, projectId, destination }: TTriggerPkiSyncSyncCertificatesDTO) => {
const { data } = await apiRequest.post(
`/api/v1/pki-syncs/${syncId}/sync`,
`/api/v1/pki-syncs/${destination}/${syncId}/sync`,
{},
{
params: { projectId }
@@ -85,9 +88,13 @@ export const useTriggerPkiSyncSyncCertificates = () => {
export const useTriggerPkiSyncImportCertificates = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, projectId }: TTriggerPkiSyncImportCertificatesDTO) => {
mutationFn: async ({
syncId,
projectId,
destination
}: TTriggerPkiSyncImportCertificatesDTO) => {
const { data } = await apiRequest.post(
`/api/v1/pki-syncs/${syncId}/import`,
`/api/v1/pki-syncs/${destination}/${syncId}/import`,
{},
{
params: { projectId }
@@ -106,9 +113,13 @@ export const useTriggerPkiSyncImportCertificates = () => {
export const useTriggerPkiSyncRemoveCertificates = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, projectId }: TTriggerPkiSyncRemoveCertificatesDTO) => {
mutationFn: async ({
syncId,
projectId,
destination
}: TTriggerPkiSyncRemoveCertificatesDTO) => {
const { data } = await apiRequest.post(
`/api/v1/pki-syncs/${syncId}/remove`,
`/api/v1/pki-syncs/${destination}/${syncId}/remove`,
{},
{
params: { projectId }

View File

@@ -29,29 +29,34 @@ export type TCreatePkiSyncDTO = DiscriminativePick<
| "isAutoSyncEnabled"
> & { subscriberId?: string; projectId: string };
export type TUpdatePkiSyncDTO = Partial<Omit<TCreatePkiSyncDTO, "destination" | "projectId">> & {
export type TUpdatePkiSyncDTO = Partial<Omit<TCreatePkiSyncDTO, "projectId">> & {
syncId: string;
projectId: string;
destination: PkiSync;
};
export type TDeletePkiSyncDTO = {
syncId: string;
projectId: string;
destination: PkiSync;
};
export type TTriggerPkiSyncSyncCertificatesDTO = {
syncId: string;
projectId: string;
destination: PkiSync;
};
export type TTriggerPkiSyncImportCertificatesDTO = {
syncId: string;
projectId: string;
destination: PkiSync;
};
export type TTriggerPkiSyncRemoveCertificatesDTO = {
syncId: string;
projectId: string;
destination: PkiSync;
};
export * from "./common";

View File

@@ -44,7 +44,7 @@ import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { useToggle } from "@app/hooks";
import { PkiSyncData, PkiSyncStatus, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
import { PkiSyncData, PkiSyncStatus } from "@app/hooks/api/pkiSyncs";
import { PkiSyncDestinationCol } from "./PkiSyncDestinationCol";
import { PkiSyncTableCell } from "./PkiSyncTableCell";
@@ -80,8 +80,6 @@ export const PkiSyncRow = ({
projectId
} = pkiSync;
const { syncOption } = usePkiSyncOption(destination);
const destinationName = PKI_SYNC_MAP[destination].name;
const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -296,38 +294,36 @@ export const PkiSyncRow = ({
</DropdownMenuItem>
)}
</ProjectPermissionCan>
{syncOption?.canImportCertificates && (
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.ImportCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faDownload} />}
onClick={(e) => {
e.stopPropagation();
onTriggerImportCertificates(pkiSync);
}}
isDisabled={!isAllowed}
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.ImportCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faDownload} />}
onClick={(e) => {
e.stopPropagation();
onTriggerImportCertificates(pkiSync);
}}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Import certificates from this ${destinationName} destination into Infisical.`}
>
<Tooltip
position="left"
sideOffset={42}
content={`Import certificates from this ${destinationName} destination into Infisical.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Import Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Import Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.RemoveCertificates}
a={permissionSubject}

View File

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

View File

@@ -42,7 +42,6 @@ import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { usePopUp, useToggle } from "@app/hooks";
import {
TPkiSync,
usePkiSyncOption,
useTriggerPkiSyncSyncCertificates,
useUpdatePkiSync
} from "@app/hooks/api/pkiSyncs";
@@ -69,7 +68,6 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
const updatePkiSyncMutation = useUpdatePkiSync();
const destinationName = PKI_SYNC_MAP[destination].name;
const { syncOption } = usePkiSyncOption(destination);
const handleCopyId = useCallback(() => {
setIsIdCopied.on();
@@ -88,7 +86,8 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
try {
await triggerSyncMutation.mutateAsync({
syncId: id,
projectId
projectId,
destination
});
createNotification({
text: "PKI sync job queued successfully",
@@ -108,6 +107,7 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
await updatePkiSyncMutation.mutateAsync({
syncId: id,
projectId,
destination,
isAutoSyncEnabled: !pkiSync.isAutoSyncEnabled
});
createNotification({
@@ -192,65 +192,61 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
Copy Sync ID
</DropdownMenuItem>
{syncOption?.canImportCertificates && (
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.ImportCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faDownload} />}
onClick={() => handlePopUpOpen("importCertificates")}
isDisabled={!isAllowed}
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.ImportCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faDownload} />}
onClick={() => handlePopUpOpen("importCertificates")}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Import certificates from this ${destinationName} destination into Infisical.`}
>
<Tooltip
position="left"
sideOffset={42}
content={`Import certificates from this ${destinationName} destination into Infisical.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Import Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Import Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
{syncOption?.canRemoveCertificates && (
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.RemoveCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faEraser} />}
onClick={() => handlePopUpOpen("removeCertificates")}
isDisabled={!isAllowed}
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.RemoveCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faEraser} />}
onClick={() => handlePopUpOpen("removeCertificates")}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Remove certificates synced by Infisical from this ${destinationName} destination.`}
>
<Tooltip
position="left"
sideOffset={42}
content={`Remove certificates synced by Infisical from this ${destinationName} destination.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Remove Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Remove Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
{(isAllowed: boolean) => (

View File

@@ -16,7 +16,7 @@ type Props = {
export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
const {
syncOptions: { canImportCertificates, canRemoveCertificates }
syncOptions: { canRemoveCertificates }
} = pkiSync;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
@@ -44,11 +44,13 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
</div>
<div>
<div className="space-y-3">
{/* Hidden for now - Import certificates functionality disabled
<GenericFieldLabel label="Certificate Import">
<Badge variant={canImportCertificates ? "success" : "danger"}>
{canImportCertificates ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
*/}
<GenericFieldLabel label="Certificate Removal">
<Badge variant={canRemoveCertificates ? "success" : "danger"}>
{canRemoveCertificates ? "Enabled" : "Disabled"}