Improvements to cert-syncs

This commit is contained in:
Carlos Monastyrski
2025-09-22 11:45:45 -03:00
parent 27dbe5b013
commit 3587fbf98b
26 changed files with 180 additions and 190 deletions

View File

@@ -309,7 +309,7 @@ export const registerSyncPkiEndpoints = ({
server.route({
method: "POST",
url: "/:pkiSyncId/remove",
url: "/:pkiSyncId/remove-certificates",
config: {
rateLimit: writeLimit
},

View File

@@ -23,7 +23,9 @@ 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 { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
@@ -57,12 +59,8 @@ type TAcmeCertificateAuthorityFnsDeps = {
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
pkiSyncDAL: {
find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise<Array<{ id: string }>>;
};
pkiSyncQueue: {
queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise<void>;
};
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
};

View File

@@ -26,7 +26,9 @@ 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 { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
@@ -56,12 +58,8 @@ type TAzureAdCsCertificateAuthorityFnsDeps = {
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
pkiSyncDAL: {
find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise<Array<{ id: string }>>;
};
pkiSyncQueue: {
queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise<void>;
};
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
};

View File

@@ -20,6 +20,8 @@ import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
import { SubscriberOperationStatus } from "../pki-subscriber/pki-subscriber-types";
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns";
import { AzureAdCsCertificateAuthorityFns } from "./azure-ad-cs/azure-ad-cs-certificate-authority-fns";
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
@@ -50,12 +52,8 @@ type TCertificateAuthorityQueueFactoryDep = {
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
queueService: TQueueServiceFactory;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById" | "updateById">;
pkiSyncDAL: {
find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise<Array<{ id: string }>>;
};
pkiSyncQueue: {
queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise<void>;
};
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
};
export type TCertificateAuthorityQueueFactory = ReturnType<typeof certificateAuthorityQueueFactory>;

View File

@@ -13,6 +13,8 @@ import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
import { TProjectDALFactory } from "../project/project-dal";
import {
AcmeCertificateAuthorityFns,
@@ -68,12 +70,8 @@ type TCertificateAuthorityServiceFactoryDep = {
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
pkiSyncDAL: {
find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise<Array<{ id: string }>>;
};
pkiSyncQueue: {
queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise<void>;
};
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
};
export type TCertificateAuthorityServiceFactory = ReturnType<typeof certificateAuthorityServiceFactory>;

View File

@@ -19,7 +19,9 @@ 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 { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
@@ -52,12 +54,8 @@ type TInternalCertificateAuthorityFnsDeps = {
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
pkiSyncDAL: {
find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise<Array<{ id: string }>>;
};
pkiSyncQueue: {
queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise<void>;
};
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
};
export const InternalCertificateAuthorityFns = ({

View File

@@ -22,7 +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 { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
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";

View File

@@ -38,7 +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 { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
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";

View File

@@ -0,0 +1,52 @@
import RE2 from "re2";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
/**
* Azure Key Vault naming constraints for certificates
*/
export const AZURE_KEY_VAULT_CERTIFICATE_NAMING = {
/**
* Regular expression pattern for valid Azure Key Vault certificate names
* Must contain only alphanumeric characters and hyphens (a-z, A-Z, 0-9, -)
* Must be 1-127 characters long
*/
NAME_PATTERN: new RE2("^[a-zA-Z0-9-]{1,127}$"),
/**
* String of characters that are forbidden in Azure Key Vault certificate names
*/
FORBIDDEN_CHARACTERS: "!@#$%^&*()+=[]{}|\\:;\"'<>,.?/~` _",
/**
* Maximum length for certificate names in Azure Key Vault
*/
MAX_NAME_LENGTH: 127,
/**
* Minimum length for certificate names in Azure Key Vault
*/
MIN_NAME_LENGTH: 1,
/**
* String representation of the allowed character pattern (for UI display)
*/
ALLOWED_CHARACTER_PATTERN: "^[a-zA-Z0-9-]{1,127}$"
} as const;
/**
* Azure Key Vault PKI Sync list option configuration
*/
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,
defaultCertificateNameSchema: "Infisical-PKI-Sync-{{certificateId}}",
forbiddenCharacters: AZURE_KEY_VAULT_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS,
allowedCharacterPattern: AZURE_KEY_VAULT_CERTIFICATE_NAMING.ALLOWED_CHARACTER_PATTERN,
maxCertificateNameLength: AZURE_KEY_VAULT_CERTIFICATE_NAMING.MAX_NAME_LENGTH,
minCertificateNameLength: AZURE_KEY_VAULT_CERTIFICATE_NAMING.MIN_NAME_LENGTH
} as const;

View File

@@ -5,14 +5,12 @@ 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 { TPkiSyncWithCredentials } from "../pki-sync-types";
import { GetAzureKeyVaultCertificate, TAzureKeyVaultPkiSyncConfig } from "./azure-key-vault-pki-sync-types";
@@ -45,19 +43,6 @@ const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyn
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,
defaultCertificateNameSchema: "Infisical-PKI-Sync-{{certificateId}}",
forbiddenCharacters: "!@#$%^&*()+=[]{}|\\:;\"'<>,.?/~` _",
allowedCharacterPattern: "^[a-zA-Z0-9-]{1,127}$",
maxCertificateNameLength: 127,
minCertificateNameLength: 1
};
type TAzureKeyVaultPkiSyncFactoryDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;

View File

@@ -5,6 +5,8 @@ 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 { AZURE_KEY_VAULT_CERTIFICATE_NAMING } from "./azure-key-vault-pki-sync-constants";
export const AzureKeyVaultPkiSyncConfigSchema = z.object({
vaultBaseUrl: z.string().url()
});
@@ -22,12 +24,12 @@ const AzureKeyVaultPkiSyncOptionsSchema = z.object({
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));
const hasForbiddenChars = AZURE_KEY_VAULT_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some((char) =>
testName.includes(char)
);
return azureNamePattern.test(testName) && !hasForbiddenChars;
return AZURE_KEY_VAULT_CERTIFICATE_NAMING.NAME_PATTERN.test(testName) && !hasForbiddenChars;
},
{
message:

View File

@@ -1,3 +1,4 @@
export * from "./azure-key-vault-pki-sync-constants";
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

@@ -3,14 +3,11 @@ 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,
azureKeyVaultPkiSyncFactory
} from "./azure-key-vault/azure-key-vault-pki-sync-fns";
import { AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION } from "./azure-key-vault/azure-key-vault-pki-sync-constants";
import { azureKeyVaultPkiSyncFactory } from "./azure-key-vault/azure-key-vault-pki-sync-fns";
import { PkiSync } from "./pki-sync-enums";
import { TCertificateMap, TPkiSyncWithCredentials } from "./pki-sync-types";
@@ -151,36 +148,6 @@ 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<Array<{ id: string }>>;
};
pkiSyncQueue: {
queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise<void>;
};
}
) => {
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,

View File

@@ -14,10 +14,8 @@ export const PkiSyncOptionsSchema = z.object({
(val) => {
if (!val) return true;
const allowedOptionalPlaceholders = ["{{environment}}"];
const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders]
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters
const allowedPlaceholdersRegexPart = ["{{certificateId}}"]
.map((p) => p.replace(new RE2(/[-/\\^$*+?.()|[\]{}]/g), "\\$&")) // Escape regex special characters
.join("|");
const allowedContentRegex = new RE2(`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`);
@@ -33,7 +31,7 @@ export const PkiSyncOptionsSchema = z.object({
},
{
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."
"Certificate name schema must include exactly one {{certificateId}} placeholder. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders."
}
)
});

View File

@@ -153,8 +153,8 @@ export const pkiSyncServiceFactory = ({
}: Omit<TUpdatePkiSyncDTO, "auditLogInfo" | "projectId">,
actor: OrgServiceActor
): Promise<TPkiSync> => {
const existingSync = await pkiSyncDAL.findById(id);
if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" });
const pkiSync = await pkiSyncDAL.findById(id);
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
@@ -162,12 +162,9 @@ export const pkiSyncServiceFactory = ({
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager,
projectId: existingSync.projectId
projectId: pkiSync.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);
@@ -181,7 +178,7 @@ export const pkiSyncServiceFactory = ({
);
if (name && name !== pkiSync.name) {
const existingPkiSync = await pkiSyncDAL.findByNameAndProjectId(name, existingSync.projectId);
const existingPkiSync = await pkiSyncDAL.findByNameAndProjectId(name, pkiSync.projectId);
if (existingPkiSync) {
throw new BadRequestError({ message: "PKI sync with this name already exists" });
}
@@ -189,7 +186,7 @@ export const pkiSyncServiceFactory = ({
if (subscriberId) {
const subscriber = await pkiSubscriberDAL.findById(subscriberId);
if (!subscriber || subscriber.projectId !== existingSync.projectId) {
if (!subscriber || subscriber.projectId !== pkiSync.projectId) {
throw new NotFoundError({ message: "PKI subscriber not found" });
}
}
@@ -209,9 +206,9 @@ export const pkiSyncServiceFactory = ({
});
}
if (syncOptions.canRemoveCertificates === false && providerCapabilities.canRemoveCertificates) {
if (syncOptions.canRemoveCertificates && !providerCapabilities.canRemoveCertificates) {
throw new BadRequestError({
message: `Certificate removal cannot be disabled for ${PKI_SYNC_NAME_MAP[pkiSync.destination]} PKI sync destination`
message: `Certificate removal cannot be enabled for ${PKI_SYNC_NAME_MAP[pkiSync.destination]} PKI sync destination`
});
}
@@ -237,9 +234,9 @@ export const pkiSyncServiceFactory = ({
const deletePkiSync = async (
{ id }: Omit<TDeletePkiSyncDTO, "auditLogInfo" | "projectId">,
actor: OrgServiceActor
): Promise<TPkiSync> => {
const existingSync = await pkiSyncDAL.findById(id);
if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" });
) => {
const pkiSync = await pkiSyncDAL.findById(id);
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
@@ -247,12 +244,9 @@ export const pkiSyncServiceFactory = ({
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager,
projectId: existingSync.projectId
projectId: pkiSync.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);
@@ -265,31 +259,7 @@ export const pkiSyncServiceFactory = ({
: ProjectPermissionSub.PkiSyncs
);
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
}
};
return pkiSyncDAL.deleteById(id);
};
const listPkiSyncsByProjectId = async ({ projectId }: TListPkiSyncsByProjectId, actor: OrgServiceActor) => {
@@ -310,15 +280,10 @@ export const pkiSyncServiceFactory = ({
const findPkiSyncById = async ({ id, projectId }: TFindPkiSyncByIdDTO, actor: OrgServiceActor) => {
const pkiSync = await pkiSyncDAL.findById(id);
if (!pkiSync)
if (!pkiSync || (projectId && pkiSync.projectId !== projectId)) {
throw new NotFoundError({
message: `Could not find PKI Sync with ID "${id}"`
});
if (projectId && pkiSync.projectId !== projectId) {
throw new NotFoundError({
message: `Could not find PKI Sync with ID "${id}" in project "${projectId}"`
});
}
const { permission } = await permissionService.getProjectPermission({
@@ -349,8 +314,8 @@ export const pkiSyncServiceFactory = ({
{ id }: Omit<TTriggerPkiSyncSyncCertificatesByIdDTO, "auditLogInfo" | "projectId">,
actor: OrgServiceActor
) => {
const existingSync = await pkiSyncDAL.findById(id);
if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" });
const pkiSync = await pkiSyncDAL.findById(id);
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
@@ -358,12 +323,9 @@ export const pkiSyncServiceFactory = ({
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager,
projectId: existingSync.projectId
projectId: pkiSync.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);
@@ -385,8 +347,8 @@ export const pkiSyncServiceFactory = ({
{ id }: Omit<TTriggerPkiSyncImportCertificatesByIdDTO, "auditLogInfo" | "projectId">,
actor: OrgServiceActor
) => {
const existingSync = await pkiSyncDAL.findById(id);
if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" });
const pkiSync = await pkiSyncDAL.findById(id);
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
@@ -394,12 +356,9 @@ export const pkiSyncServiceFactory = ({
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager,
projectId: existingSync.projectId
projectId: pkiSync.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) {
@@ -429,8 +388,8 @@ export const pkiSyncServiceFactory = ({
{ id }: Omit<TTriggerPkiSyncRemoveCertificatesByIdDTO, "auditLogInfo" | "projectId">,
actor: OrgServiceActor
) => {
const existingSync = await pkiSyncDAL.findById(id);
if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" });
const pkiSync = await pkiSyncDAL.findById(id);
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
@@ -438,12 +397,9 @@ export const pkiSyncServiceFactory = ({
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager,
projectId: existingSync.projectId
projectId: pkiSync.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);

View File

@@ -0,0 +1,27 @@
import { logger } from "@app/lib/logger";
import { TPkiSyncDALFactory } from "./pki-sync-dal";
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
export const triggerAutoSyncForSubscriber = async (
subscriberId: string,
dependencies: {
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
}
) => {
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}:`);
}
};

View File

@@ -1,6 +1,6 @@
---
title: "Remove Certificates from Azure Key Vault"
openapi: "POST /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}/remove"
openapi: "POST /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}/remove-certificates"
---
<Warning>
@@ -29,7 +29,7 @@ This endpoint removes certificates from the specified Azure Key Vault that are n
<RequestExample>
```bash cURL
curl -X POST "https://app.infisical.com/api/v1/pki/syncs/azure-key-vault/ps_12345/remove" \
curl -X POST "https://app.infisical.com/api/v1/pki/syncs/azure-key-vault/ps_12345/remove-certificates" \
-H "Authorization: Bearer <your-api-key>"
```
</RequestExample>

View File

@@ -10,7 +10,7 @@ This endpoint lists all available PKI sync destination options and their capabil
## Request
<ParamField query="projectId" type="string" optional>
Project ID (for authorization purposes, but the options are global)
Project ID
</ParamField>
## Response

View File

@@ -2646,9 +2646,9 @@
"href": "https://infisical.com"
},
"api": {
"openapi": "https://api.infisical.com/api/docs/json",
"openapi": "https://app.infisical.com/api/docs/json",
"mdx": {
"server": ["https://api.infisical.com"]
"server": ["https://app.infisical.com"]
}
},
"appearance": {

View File

@@ -80,8 +80,7 @@ via the UI or API for the third-party service you intend to sync certificates to
- Certificate naming schema to control how certificate names are generated in the destination
<Note>
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
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.
</Note>
@@ -110,11 +109,9 @@ By default, certificates are named using the pattern `Infisical-{certificateId}`
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:**

View File

@@ -130,11 +130,10 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props)
Certificate Sync Behavior
</div>
<p className="mt-1 text-sm text-bunker-200">
Certificate Syncs manage certificates that are prefixed with &quot;Infisical-&quot; 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.
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.
</p>
</div>
<div className="mt-4 flex gap-4">

View File

@@ -104,8 +104,8 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
tooltipText={
<div className="flex flex-col gap-3">
<span>
When a certificate is synced, values will be injected into the certificate name
schema before it reaches the destination. This is useful for organization.
When a certificate is synced, the certificate name schema will be applied before
it reaches the destination.
</span>
<div className="flex flex-col">
@@ -114,10 +114,6 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
<li>
<code>{"{{certificateId}}"}</code> - The unique ID of the certificate
</li>
<li>
<code>{"{{environment}}"}</code> - The environment which the certificate is in
(e.g. dev, staging, prod)
</li>
</ul>
</div>
{syncOption?.forbiddenCharacters && syncOption.forbiddenCharacters.length > 0 && (
@@ -129,11 +125,6 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
The following characters are not allowed:{" "}
{syncOption.forbiddenCharacters.split("").join(" ")}
</div>
{syncOption.allowedCharacterPattern && (
<div className="mt-1 text-xs text-bunker-300">
Only alphanumeric characters and hyphens are allowed (a-z, A-Z, 0-9, -)
</div>
)}
</div>
)}
</div>
@@ -142,7 +133,7 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
isOptional
errorText={error?.message}
label="Certificate Name Schema"
helperText="Infisical strongly advises setting a Certificate Name Schema to ensure that Infisical only manages the specific certificates you intend, keeping everything else untouched."
helperText="Infisical strongly advises setting a Certificate Name Schema to ensure that Infisical only manages the specific certificates you intend to manage, keeping everything else untouched."
>
<Input
value={value || ""}

View File

@@ -97,7 +97,9 @@ export const useTriggerPkiSyncRemoveCertificates = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, destination }: TTriggerPkiSyncRemoveCertificatesDTO) => {
const { data } = await apiRequest.post(`/api/v1/pki/syncs/${destination}/${syncId}/remove`);
const { data } = await apiRequest.post(
`/api/v1/pki/syncs/${destination}/${syncId}/remove-certificates`
);
return data;
},

View File

@@ -8,8 +8,7 @@ type Props = {
type:
| ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports
| ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.PkiSyncs;
| ProjectPermissionSub.SecretRotation;
};
export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => {

View File

@@ -0,0 +1,19 @@
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { ConditionsFields } from "./ConditionsFields";
type Props = {
position?: number;
isDisabled?: boolean;
};
export const PkiSyncPermissionConditions = ({ position = 0, isDisabled }: Props) => {
return (
<ConditionsFields
isDisabled={isDisabled}
subject={ProjectPermissionSub.PkiSyncs}
position={position}
selectOptions={[{ value: "subscriberName", label: "Subscriber Name" }]}
/>
);
};

View File

@@ -23,6 +23,7 @@ import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
import { PermissionEmptyState } from "./PermissionEmptyState";
import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions";
import { PkiSyncPermissionConditions } from "./PkiSyncPermissionConditions";
import { PkiTemplatePermissionConditions } from "./PkiTemplatePermissionConditions";
import {
EXCLUDED_PERMISSION_SUBS,
@@ -74,6 +75,10 @@ export const renderConditionalComponents = (
return <SecretSyncPermissionConditions isDisabled={isDisabled} />;
}
if (subject === ProjectPermissionSub.PkiSyncs) {
return <PkiSyncPermissionConditions isDisabled={isDisabled} />;
}
if (subject === ProjectPermissionSub.SecretEvents) {
return <SecretEventPermissionConditions isDisabled={isDisabled} />;
}