Merge pull request #4596 from Infisical/ENG-3798
Add PKI sync AWS Certificate Manager
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION,
|
||||
AwsCertificateManagerPkiSyncSchema,
|
||||
CreateAwsCertificateManagerPkiSyncSchema,
|
||||
UpdateAwsCertificateManagerPkiSyncSchema
|
||||
} from "@app/services/pki-sync/aws-certificate-manager";
|
||||
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||
|
||||
import { registerSyncPkiEndpoints } from "./pki-sync-endpoints";
|
||||
|
||||
export const registerAwsCertificateManagerPkiSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncPkiEndpoints({
|
||||
destination: PkiSync.AwsCertificateManager,
|
||||
server,
|
||||
responseSchema: AwsCertificateManagerPkiSyncSchema,
|
||||
createSchema: CreateAwsCertificateManagerPkiSyncSchema,
|
||||
updateSchema: UpdateAwsCertificateManagerPkiSyncSchema,
|
||||
syncOptions: {
|
||||
canImportCertificates: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION.canImportCertificates,
|
||||
canRemoveCertificates: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION.canRemoveCertificates
|
||||
}
|
||||
});
|
||||
@@ -1,9 +1,11 @@
|
||||
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||
|
||||
import { registerAwsCertificateManagerPkiSyncRouter } from "./aws-certificate-manager-pki-sync-router";
|
||||
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
|
||||
[PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter,
|
||||
[PkiSync.AwsCertificateManager]: registerAwsCertificateManagerPkiSyncRouter
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
* AWS Certificate Manager naming constraints for certificates
|
||||
*/
|
||||
export const AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING = {
|
||||
/**
|
||||
* Regular expression pattern for valid AWS Certificate Manager certificate names
|
||||
* Must contain only alphanumeric characters, spaces, hyphens, and underscores
|
||||
* Must be 1-256 characters long
|
||||
*/
|
||||
NAME_PATTERN: new RE2("^[a-zA-Z0-9\\s\\-_]{1,256}$"),
|
||||
|
||||
/**
|
||||
* String of characters that are forbidden in AWS Certificate Manager certificate names
|
||||
*/
|
||||
FORBIDDEN_CHARACTERS: "!@#$%^&*()+={}[]|\\:;\"'<>,.?/~`",
|
||||
|
||||
/**
|
||||
* Maximum length for certificate names in AWS Certificate Manager
|
||||
*/
|
||||
MAX_LENGTH: 256,
|
||||
|
||||
/**
|
||||
* Minimum length for certificate names in AWS Certificate Manager
|
||||
*/
|
||||
MIN_LENGTH: 1,
|
||||
|
||||
/**
|
||||
* String representation of the allowed character pattern (for UI display)
|
||||
*/
|
||||
ALLOWED_CHARACTER_PATTERN: "^[a-zA-Z0-9\\s\\-_]{1,256}$"
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* AWS Certificate Manager PKI Sync list option configuration
|
||||
*/
|
||||
export const AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION = {
|
||||
name: "AWS Certificate Manager" as const,
|
||||
connection: AppConnection.AWS,
|
||||
destination: PkiSync.AwsCertificateManager,
|
||||
canImportCertificates: false,
|
||||
canRemoveCertificates: true,
|
||||
defaultCertificateNameSchema: "Infisical-{{certificateId}}",
|
||||
forbiddenCharacters: AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS,
|
||||
allowedCharacterPattern: AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.ALLOWED_CHARACTER_PATTERN,
|
||||
maxCertificateNameLength: AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.MAX_LENGTH,
|
||||
minCertificateNameLength: AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.MIN_LENGTH
|
||||
} as const;
|
||||
@@ -0,0 +1,634 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import * as AWS from "aws-sdk";
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
import { AwsConnectionMethod } from "@app/services/app-connection/aws/aws-connection-enums";
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import {
|
||||
AwsConnectionAccessTokenCredentialsSchema,
|
||||
AwsConnectionAssumeRoleCredentialsSchema
|
||||
} from "@app/services/app-connection/aws/aws-connection-schemas";
|
||||
import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types";
|
||||
|
||||
import { PkiSyncError } from "../pki-sync-errors";
|
||||
import { TPkiSyncWithCredentials } from "../pki-sync-types";
|
||||
import {
|
||||
ACMCertificateWithKey,
|
||||
CertificateImportRequest,
|
||||
RemoveCertificatesResult,
|
||||
SyncCertificatesResult,
|
||||
TAwsCertificateManagerPkiSyncConfig
|
||||
} from "./aws-certificate-manager-pki-sync-types";
|
||||
|
||||
const INFISICAL_CERTIFICATE_TAG = "InfisicalCertificate";
|
||||
const AWS_CERTIFICATE_ARN_PATTERN = new RE2("^arn:aws:acm:[a-z0-9-]+:\\d{12}:certificate/[a-f0-9-]{36}$");
|
||||
|
||||
type TAwsAssumeRoleCredentials = z.infer<typeof AwsConnectionAssumeRoleCredentialsSchema>;
|
||||
type TAwsAccessKeyCredentials = z.infer<typeof AwsConnectionAccessTokenCredentialsSchema>;
|
||||
|
||||
const AWS_RATE_LIMIT_CONFIG: RateLimitConfig = {
|
||||
MAX_CONCURRENT_REQUESTS: 10,
|
||||
BASE_DELAY: 1000,
|
||||
MAX_DELAY: 30000,
|
||||
MAX_RETRIES: 3,
|
||||
RATE_LIMIT_STATUS_CODES: [429, 503]
|
||||
};
|
||||
|
||||
const awsConnectionQueue = createConnectionQueue(AWS_RATE_LIMIT_CONFIG);
|
||||
|
||||
const { withRateLimitRetry, executeWithConcurrencyLimit } = awsConnectionQueue;
|
||||
|
||||
const validateCertificateArn = (arn: string): boolean => {
|
||||
return AWS_CERTIFICATE_ARN_PATTERN.test(arn);
|
||||
};
|
||||
|
||||
const extractCertificateNameFromArn = (certificateArn: string): string => {
|
||||
if (!validateCertificateArn(certificateArn)) {
|
||||
throw new Error(`Invalid AWS Certificate Manager ARN format: ${certificateArn}`);
|
||||
}
|
||||
const parts = certificateArn.split("/");
|
||||
return parts[parts.length - 1];
|
||||
};
|
||||
|
||||
const sanitizeInput = (input: string): string => {
|
||||
return input.trim().replace(new RE2("[^\\w\\s-]", "g"), "");
|
||||
};
|
||||
|
||||
const validateCertificateContent = (cert: string, privateKey: string): void => {
|
||||
if (!cert || cert.trim().length === 0) {
|
||||
throw new Error("Certificate content is empty or missing");
|
||||
}
|
||||
|
||||
if (!privateKey || privateKey.trim().length === 0) {
|
||||
throw new Error("Private key content is empty or missing");
|
||||
}
|
||||
|
||||
if (!cert.includes("-----BEGIN CERTIFICATE-----") || !cert.includes("-----END CERTIFICATE-----")) {
|
||||
throw new Error("Certificate is not in valid PEM format");
|
||||
}
|
||||
|
||||
if (!privateKey.includes("-----BEGIN") || !privateKey.includes("-----END")) {
|
||||
throw new Error("Private key is not in valid PEM format");
|
||||
}
|
||||
};
|
||||
|
||||
const isAwsIssuedCertificate = (certificate: AWS.ACM.CertificateSummary): boolean => {
|
||||
return certificate.Type === "AMAZON_ISSUED";
|
||||
};
|
||||
|
||||
const shouldSkipCertificateExport = (certificate: AWS.ACM.CertificateSummary): boolean => {
|
||||
return isAwsIssuedCertificate(certificate);
|
||||
};
|
||||
|
||||
const findTagByKey = (tags: AWS.ACM.TagList | undefined, key: string): AWS.ACM.Tag | undefined => {
|
||||
if (!tags || !Array.isArray(tags)) {
|
||||
return undefined;
|
||||
}
|
||||
return tags.find((tag: AWS.ACM.Tag) => tag.Key === key && tag.Value);
|
||||
};
|
||||
|
||||
const findInfisicalCertificateTag = (tags: AWS.ACM.TagList | undefined): AWS.ACM.Tag | undefined => {
|
||||
return findTagByKey(tags, INFISICAL_CERTIFICATE_TAG);
|
||||
};
|
||||
|
||||
const validateCertificateIdentification = (
|
||||
certName: string,
|
||||
existingCert: { arn?: string; Tags?: AWS.ACM.TagList; cert?: string; privateKey?: string; certificateChain?: string }
|
||||
): boolean => {
|
||||
if (!existingCert?.arn || !existingCert?.Tags) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const certNameTag = findInfisicalCertificateTag(existingCert.Tags);
|
||||
|
||||
if (!certNameTag || !certNameTag.Value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return certNameTag.Value === certName;
|
||||
};
|
||||
|
||||
type TAwsCertificateManagerPkiSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
const validateCertificateNameSchema = (schema: string): void => {
|
||||
if (!schema.includes("{{certificateId}}")) {
|
||||
throw new Error(
|
||||
"Certificate name schema must include {{certificateId}} placeholder for proper certificate identification"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const generateCertificateName = (certificateName: string, pkiSync: TPkiSyncWithCredentials): string => {
|
||||
if (!certificateName || typeof certificateName !== "string") {
|
||||
throw new Error("Certificate name must be a non-empty string");
|
||||
}
|
||||
|
||||
const sanitizedCertificateName = sanitizeInput(certificateName);
|
||||
const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined;
|
||||
const certificateNameSchema = syncOptions?.certificateNameSchema;
|
||||
|
||||
if (certificateNameSchema) {
|
||||
validateCertificateNameSchema(certificateNameSchema);
|
||||
|
||||
let certificateId: string;
|
||||
|
||||
if (sanitizedCertificateName.startsWith("Infisical-")) {
|
||||
certificateId = sanitizedCertificateName.substring("Infisical-".length);
|
||||
} else {
|
||||
certificateId = sanitizedCertificateName;
|
||||
}
|
||||
|
||||
if (!certificateId || certificateId.trim().length === 0) {
|
||||
throw new Error(`Certificate ID cannot be empty after processing certificate name: ${certificateName}`);
|
||||
}
|
||||
|
||||
const environment = "global";
|
||||
const generatedName = certificateNameSchema
|
||||
.replace(new RE2("\\{\\{certificateId\\}\\}", "g"), certificateId)
|
||||
.replace(new RE2("\\{\\{environment\\}\\}", "g"), environment);
|
||||
|
||||
if (generatedName.length > 256 || generatedName.length < 1) {
|
||||
throw new Error(
|
||||
`Generated certificate name length (${generatedName.length}) must be between 1 and 256 characters`
|
||||
);
|
||||
}
|
||||
|
||||
if (generatedName.includes("{{certificateId}}")) {
|
||||
throw new Error("Certificate name schema failed to properly replace {{certificateId}} placeholder");
|
||||
}
|
||||
|
||||
return generatedName;
|
||||
}
|
||||
|
||||
return sanitizedCertificateName;
|
||||
};
|
||||
|
||||
const getAwsAcmClient = async (
|
||||
connectionId: string,
|
||||
region: AWSRegion,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
): Promise<AWS.ACM> => {
|
||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||
|
||||
if (!appConnection) {
|
||||
throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` });
|
||||
}
|
||||
|
||||
if (appConnection.app !== AppConnection.AWS) {
|
||||
throw new BadRequestError({
|
||||
message: `Connection '${connectionId}' is not an AWS connection (found: ${appConnection.app})`
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedCredentials = await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials,
|
||||
projectId: appConnection.projectId
|
||||
});
|
||||
|
||||
let awsConnectionConfig: TAwsConnectionConfig;
|
||||
switch (appConnection.method) {
|
||||
case AwsConnectionMethod.AssumeRole:
|
||||
awsConnectionConfig = {
|
||||
app: AppConnection.AWS,
|
||||
method: AwsConnectionMethod.AssumeRole,
|
||||
credentials: decryptedCredentials as TAwsAssumeRoleCredentials,
|
||||
orgId: appConnection.orgId
|
||||
};
|
||||
break;
|
||||
case AwsConnectionMethod.AccessKey:
|
||||
awsConnectionConfig = {
|
||||
app: AppConnection.AWS,
|
||||
method: AwsConnectionMethod.AccessKey,
|
||||
credentials: decryptedCredentials as TAwsAccessKeyCredentials,
|
||||
orgId: appConnection.orgId
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported AWS connection method: ${appConnection.method}`
|
||||
});
|
||||
}
|
||||
|
||||
const awsConfig = await getAwsConnectionConfig(awsConnectionConfig, region);
|
||||
|
||||
return new AWS.ACM(awsConfig);
|
||||
};
|
||||
|
||||
export const awsCertificateManagerPkiSyncFactory = ({
|
||||
kmsService,
|
||||
appConnectionDAL
|
||||
}: TAwsCertificateManagerPkiSyncFactoryDeps) => {
|
||||
const deleteCertificateFromAcm = async (
|
||||
acm: AWS.ACM,
|
||||
certificateArn: string,
|
||||
operation: string,
|
||||
syncId: string,
|
||||
throwOnError = false
|
||||
): Promise<{ arn: string; success: boolean; error?: Error }> => {
|
||||
try {
|
||||
await withRateLimitRetry(() => acm.deleteCertificate({ CertificateArn: certificateArn }).promise(), {
|
||||
operation,
|
||||
syncId
|
||||
});
|
||||
return { arn: certificateArn, success: true };
|
||||
} catch (error) {
|
||||
const errorObj = error instanceof Error ? error : new Error("Unknown error");
|
||||
|
||||
if (throwOnError) {
|
||||
throw new PkiSyncError({
|
||||
message: `Failed to remove certificate from AWS Certificate Manager: ${errorObj.message}`,
|
||||
cause: errorObj,
|
||||
context: {
|
||||
certificateArn,
|
||||
operation
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
arn: certificateArn,
|
||||
success: false,
|
||||
error: errorObj
|
||||
};
|
||||
}
|
||||
};
|
||||
const $getAwsAcmCertificates = async (
|
||||
acm: AWS.ACM,
|
||||
syncId = "unknown"
|
||||
): Promise<{
|
||||
acmCertificates: Record<
|
||||
string,
|
||||
{ cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList }
|
||||
>;
|
||||
}> => {
|
||||
const paginateAwsAcmCertificates = async () => {
|
||||
const certificates: AWS.ACM.CertificateSummary[] = [];
|
||||
let nextToken: string | undefined;
|
||||
|
||||
do {
|
||||
const listParams: AWS.ACM.ListCertificatesRequest = {
|
||||
CertificateStatuses: ["ISSUED"],
|
||||
NextToken: nextToken,
|
||||
MaxItems: 100
|
||||
};
|
||||
|
||||
const response = await withRateLimitRetry(() => acm.listCertificates(listParams).promise(), {
|
||||
operation: "list-certificates",
|
||||
syncId
|
||||
});
|
||||
|
||||
if (response.CertificateSummaryList) {
|
||||
certificates.push(...response.CertificateSummaryList);
|
||||
}
|
||||
nextToken = response.NextToken;
|
||||
} while (nextToken);
|
||||
|
||||
return certificates;
|
||||
};
|
||||
|
||||
const certificateSummaries = await paginateAwsAcmCertificates();
|
||||
|
||||
const certificateResults = await executeWithConcurrencyLimit(
|
||||
certificateSummaries,
|
||||
async (certSummary) => {
|
||||
if (!certSummary.CertificateArn) {
|
||||
throw new Error("Certificate ARN is missing");
|
||||
}
|
||||
|
||||
const [certificateDetails, tagsResponse] = await Promise.all([
|
||||
acm.describeCertificate({ CertificateArn: certSummary.CertificateArn }).promise(),
|
||||
acm.listTagsForCertificate({ CertificateArn: certSummary.CertificateArn }).promise()
|
||||
]);
|
||||
|
||||
let certificateContent: AWS.ACM.GetCertificateResponse | undefined;
|
||||
if (!shouldSkipCertificateExport(certSummary)) {
|
||||
try {
|
||||
certificateContent = await acm.getCertificate({ CertificateArn: certSummary.CertificateArn }).promise();
|
||||
} catch (error) {
|
||||
// Certificate content cannot be imported
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...certificateDetails.Certificate,
|
||||
Tags: tagsResponse.Tags,
|
||||
key: extractCertificateNameFromArn(certSummary.CertificateArn),
|
||||
cert: certificateContent?.Certificate || "",
|
||||
certificateChain: certificateContent?.CertificateChain || "",
|
||||
privateKey: "", // Private keys cannot be exported from ACM
|
||||
arn: certSummary.CertificateArn
|
||||
};
|
||||
},
|
||||
{ operation: "fetch-certificate-details", syncId }
|
||||
);
|
||||
|
||||
const successfulCertificates: ACMCertificateWithKey[] = [];
|
||||
certificateResults.forEach((result) => {
|
||||
if (result.status === "fulfilled") {
|
||||
successfulCertificates.push(result.value as ACMCertificateWithKey);
|
||||
}
|
||||
});
|
||||
|
||||
const failedFetches = certificateResults.filter((result) => result.status === "rejected");
|
||||
if (failedFetches.length > 0) {
|
||||
throw new PkiSyncError({
|
||||
message: `Failed to fetch ${failedFetches.length} certificate details from AWS Certificate Manager`,
|
||||
shouldRetry: true,
|
||||
context: {
|
||||
failedCount: failedFetches.length,
|
||||
totalCount: certificateSummaries.length
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const res: Record<
|
||||
string,
|
||||
{ cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList }
|
||||
> = successfulCertificates.reduce(
|
||||
(obj, certificate) => ({
|
||||
...obj,
|
||||
[certificate.key]: {
|
||||
cert: certificate.cert,
|
||||
privateKey: certificate.privateKey,
|
||||
certificateChain: certificate.certificateChain,
|
||||
arn: certificate.CertificateArn,
|
||||
Tags: certificate.Tags
|
||||
}
|
||||
}),
|
||||
{} as Record<
|
||||
string,
|
||||
{ cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList }
|
||||
>
|
||||
);
|
||||
|
||||
return {
|
||||
acmCertificates: res
|
||||
};
|
||||
};
|
||||
|
||||
const syncCertificates = async (
|
||||
pkiSync: TPkiSyncWithCredentials,
|
||||
certificateMap: TCertificateMap
|
||||
): Promise<SyncCertificatesResult> => {
|
||||
const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig;
|
||||
const acm = await getAwsAcmClient(
|
||||
pkiSync.connection.id,
|
||||
destinationConfig.region as AWSRegion,
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
);
|
||||
|
||||
const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id);
|
||||
|
||||
const setCertificates: CertificateImportRequest[] = [];
|
||||
|
||||
const activeCertificateNames = Object.keys(certificateMap);
|
||||
|
||||
Object.entries(certificateMap).forEach(([certName, certData]) => {
|
||||
const { cert, privateKey, certificateChain } = certData;
|
||||
const certificateName = generateCertificateName(certName, pkiSync);
|
||||
|
||||
const existingCert = Object.values(acmCertificates).find((acmCert) =>
|
||||
validateCertificateIdentification(certName, acmCert)
|
||||
);
|
||||
|
||||
const shouldUpdateCert = !existingCert || existingCert.cert !== cert;
|
||||
|
||||
try {
|
||||
validateCertificateContent(cert, privateKey);
|
||||
} catch (validationError) {
|
||||
throw new PkiSyncError({
|
||||
message: `Certificate validation failed for ${certName}: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
|
||||
shouldRetry: false,
|
||||
context: {
|
||||
certificateName,
|
||||
certName
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (shouldUpdateCert) {
|
||||
setCertificates.push({
|
||||
key: certName,
|
||||
name: certificateName,
|
||||
cert,
|
||||
privateKey,
|
||||
certificateChain,
|
||||
existingArn: existingCert?.arn
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Identify expired/removed certificates that need to be cleaned up from ACM
|
||||
const certificatesToRemove = Object.values(acmCertificates)
|
||||
.filter((acmCert) => {
|
||||
if (!acmCert.arn || !acmCert.Tags) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const certNameTag = findInfisicalCertificateTag(acmCert.Tags);
|
||||
if (!certNameTag || !certNameTag.Value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isActive = activeCertificateNames.includes(certNameTag.Value);
|
||||
return !isActive;
|
||||
})
|
||||
.map((acmCert) => acmCert.arn!)
|
||||
.filter((arn) => arn);
|
||||
|
||||
const uploadResults = await executeWithConcurrencyLimit(
|
||||
setCertificates,
|
||||
async ({ key, name, cert, privateKey, certificateChain, existingArn }) => {
|
||||
try {
|
||||
const importParams: AWS.ACM.ImportCertificateRequest = {
|
||||
Certificate: cert,
|
||||
PrivateKey: privateKey,
|
||||
Tags: [
|
||||
{
|
||||
Key: INFISICAL_CERTIFICATE_TAG,
|
||||
Value: key
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (certificateChain && certificateChain.trim().length > 0) {
|
||||
importParams.CertificateChain = certificateChain;
|
||||
}
|
||||
if (existingArn) {
|
||||
importParams.CertificateArn = existingArn;
|
||||
}
|
||||
|
||||
const response = await withRateLimitRetry(() => acm.importCertificate(importParams).promise(), {
|
||||
operation: "import-certificate",
|
||||
syncId: pkiSync.id
|
||||
});
|
||||
|
||||
return { key, name, success: true, response };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
throw new PkiSyncError({
|
||||
message: `Failed to import certificate ${key} to AWS Certificate Manager: ${errorMessage}`,
|
||||
cause: error instanceof Error ? error : new Error(errorMessage),
|
||||
context: {
|
||||
certificateKey: key,
|
||||
certificateName: name,
|
||||
region: destinationConfig.region
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{ operation: "import-certificates", syncId: pkiSync.id }
|
||||
);
|
||||
|
||||
const results = uploadResults;
|
||||
const failedUploads = results.filter((result) => result.status === "rejected");
|
||||
const successfulUploads = results.filter((result) => result.status === "fulfilled");
|
||||
|
||||
let removedCertificates = 0;
|
||||
let failedRemovals = 0;
|
||||
let removeResults: PromiseSettledResult<{ arn: string; success: boolean; error?: Error }>[] = [];
|
||||
|
||||
if (certificatesToRemove.length > 0) {
|
||||
removeResults = await executeWithConcurrencyLimit(
|
||||
certificatesToRemove,
|
||||
async (certificateArn) => deleteCertificateFromAcm(acm, certificateArn, "delete-certificate", pkiSync.id),
|
||||
{ operation: "remove-certificates", syncId: pkiSync.id }
|
||||
);
|
||||
|
||||
const successfulRemovals = removeResults.filter(
|
||||
(result) => result.status === "fulfilled" && result.value.success
|
||||
);
|
||||
removedCertificates = successfulRemovals.length;
|
||||
failedRemovals = removeResults.length - removedCertificates;
|
||||
}
|
||||
|
||||
const details: {
|
||||
failedUploads?: Array<{ name: string; error: string }>;
|
||||
failedRemovals?: Array<{ name: string; error: string }>;
|
||||
} = {};
|
||||
|
||||
if (failedUploads.length > 0) {
|
||||
details.failedUploads = failedUploads.map((failure, index) => {
|
||||
const certificateName = setCertificates[index]?.name || "unknown";
|
||||
let errorMessage = "Unknown error";
|
||||
|
||||
if (failure.status === "rejected") {
|
||||
errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error";
|
||||
}
|
||||
|
||||
return {
|
||||
name: certificateName,
|
||||
error: errorMessage
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (failedRemovals > 0 && removeResults.length > 0) {
|
||||
const actualFailedRemovals = removeResults
|
||||
.map((result, index) => {
|
||||
if (result.status === "rejected") {
|
||||
const arn = certificatesToRemove[index] || "unknown";
|
||||
const errorMessage = result.reason instanceof Error ? result.reason.message : "Unknown error";
|
||||
return {
|
||||
name: arn.includes("certificate/") ? extractCertificateNameFromArn(arn) : arn,
|
||||
error: errorMessage
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((item): item is { name: string; error: string } => item !== null);
|
||||
|
||||
details.failedRemovals = actualFailedRemovals;
|
||||
}
|
||||
|
||||
return {
|
||||
uploaded: successfulUploads.length,
|
||||
removed: removedCertificates,
|
||||
failedRemovals,
|
||||
skipped: Object.keys(certificateMap).length - setCertificates.length,
|
||||
details: Object.keys(details).length > 0 ? details : undefined
|
||||
};
|
||||
};
|
||||
|
||||
const removeCertificates = async (
|
||||
pkiSync: TPkiSyncWithCredentials,
|
||||
certificateNames: string[]
|
||||
): Promise<RemoveCertificatesResult> => {
|
||||
const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig;
|
||||
const acm = await getAwsAcmClient(
|
||||
pkiSync.connection.id,
|
||||
destinationConfig.region as AWSRegion,
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
);
|
||||
|
||||
const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id);
|
||||
|
||||
const certificateArnsToRemove: string[] = [];
|
||||
|
||||
for (const certName of certificateNames) {
|
||||
const matchingCerts = Object.values(acmCertificates).filter((acmCert) =>
|
||||
validateCertificateIdentification(certName, acmCert)
|
||||
);
|
||||
|
||||
for (const acmCert of matchingCerts) {
|
||||
if (acmCert.arn) {
|
||||
certificateArnsToRemove.push(acmCert.arn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const results = await executeWithConcurrencyLimit(
|
||||
certificateArnsToRemove,
|
||||
async (certificateArn) =>
|
||||
deleteCertificateFromAcm(acm, certificateArn, "delete-specific-certificate", pkiSync.id, true),
|
||||
{ operation: "remove-specific-certificates", syncId: pkiSync.id }
|
||||
);
|
||||
|
||||
const failedRemovals = results.filter((result) => result.status === "rejected");
|
||||
|
||||
if (failedRemovals.length > 0) {
|
||||
const failedReasons = failedRemovals.map((failure) => {
|
||||
if (failure.status === "rejected") {
|
||||
return failure.reason instanceof Error ? failure.reason.message : "Unknown error";
|
||||
}
|
||||
return "Unknown error";
|
||||
});
|
||||
|
||||
throw new PkiSyncError({
|
||||
message: `Failed to remove ${failedRemovals.length} certificate(s) from AWS Certificate Manager`,
|
||||
context: {
|
||||
failedReasons,
|
||||
totalCertificates: certificateArnsToRemove.length,
|
||||
failedCount: failedRemovals.length
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
removed: certificateArnsToRemove.length - failedRemovals.length,
|
||||
failed: failedRemovals.length,
|
||||
skipped: certificateNames.length - certificateArnsToRemove.length
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
syncCertificates,
|
||||
removeCertificates
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnection, AWSRegion } 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 { AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING } from "./aws-certificate-manager-pki-sync-constants";
|
||||
|
||||
export const AwsCertificateManagerPkiSyncConfigSchema = z.object({
|
||||
region: z.nativeEnum(AWSRegion)
|
||||
});
|
||||
|
||||
const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(true),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(schema) => {
|
||||
if (!schema) return true;
|
||||
|
||||
// Validate that {{certificateId}} placeholder is present
|
||||
if (!schema.includes("{{certificateId}}")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const testName = schema
|
||||
.replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "test-cert-id")
|
||||
.replace(new RE2("\\{\\{environment\\}\\}", "g"), "test-env");
|
||||
|
||||
const hasForbiddenChars = AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some(
|
||||
(char) => testName.includes(char)
|
||||
);
|
||||
|
||||
return (
|
||||
AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.NAME_PATTERN.test(testName) &&
|
||||
!hasForbiddenChars &&
|
||||
testName.length >= AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.MIN_LENGTH &&
|
||||
testName.length <= AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.MAX_LENGTH
|
||||
);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Certificate name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, spaces, hyphens, and underscores and be 1-256 characters long when compiled for AWS Certificate Manager"
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
export const AwsCertificateManagerPkiSyncSchema = PkiSyncSchema.extend({
|
||||
destination: z.literal(PkiSync.AwsCertificateManager),
|
||||
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema,
|
||||
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema
|
||||
});
|
||||
|
||||
export const CreateAwsCertificateManagerPkiSyncSchema = z.object({
|
||||
name: z.string().trim().min(1).max(64),
|
||||
description: z.string().optional(),
|
||||
isAutoSyncEnabled: z.boolean().default(true),
|
||||
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema,
|
||||
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional().default({}),
|
||||
subscriberId: z.string().optional(),
|
||||
connectionId: z.string(),
|
||||
projectId: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({
|
||||
name: z.string().trim().min(1).max(64).optional(),
|
||||
description: z.string().optional(),
|
||||
isAutoSyncEnabled: z.boolean().optional(),
|
||||
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema.optional(),
|
||||
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional(),
|
||||
subscriberId: z.string().optional(),
|
||||
connectionId: z.string().optional()
|
||||
});
|
||||
|
||||
export const AwsCertificateManagerPkiSyncListItemSchema = z.object({
|
||||
name: z.literal("AWS Certificate Manager"),
|
||||
connection: z.literal(AppConnection.AWS),
|
||||
destination: z.literal(PkiSync.AwsCertificateManager),
|
||||
canImportCertificates: z.literal(false),
|
||||
canRemoveCertificates: z.literal(true)
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import * as AWS from "aws-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
|
||||
import {
|
||||
AwsCertificateManagerPkiSyncConfigSchema,
|
||||
AwsCertificateManagerPkiSyncSchema,
|
||||
CreateAwsCertificateManagerPkiSyncSchema,
|
||||
UpdateAwsCertificateManagerPkiSyncSchema
|
||||
} from "./aws-certificate-manager-pki-sync-schemas";
|
||||
|
||||
export type TAwsCertificateManagerPkiSyncConfig = z.infer<typeof AwsCertificateManagerPkiSyncConfigSchema>;
|
||||
|
||||
export type TAwsCertificateManagerPkiSync = z.infer<typeof AwsCertificateManagerPkiSyncSchema>;
|
||||
|
||||
export type TAwsCertificateManagerPkiSyncInput = z.infer<typeof CreateAwsCertificateManagerPkiSyncSchema>;
|
||||
|
||||
export type TAwsCertificateManagerPkiSyncUpdate = z.infer<typeof UpdateAwsCertificateManagerPkiSyncSchema>;
|
||||
|
||||
export type TAwsCertificateManagerPkiSyncWithCredentials = TAwsCertificateManagerPkiSync & {
|
||||
connection: TAwsConnection;
|
||||
};
|
||||
|
||||
export interface ACMCertificateWithKey extends AWS.ACM.CertificateDetail {
|
||||
Tags?: AWS.ACM.TagList;
|
||||
key: string;
|
||||
cert: string;
|
||||
certificateChain: string;
|
||||
privateKey: string;
|
||||
arn?: string;
|
||||
}
|
||||
|
||||
export interface SyncCertificatesResult {
|
||||
uploaded: number;
|
||||
removed: number;
|
||||
failedRemovals: number;
|
||||
skipped: number;
|
||||
details?: {
|
||||
failedUploads?: Array<{ name: string; error: string }>;
|
||||
failedRemovals?: Array<{ name: string; error: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RemoveCertificatesResult {
|
||||
removed: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
export interface CertificateImportRequest {
|
||||
key: string;
|
||||
name: string;
|
||||
cert: string;
|
||||
privateKey: string;
|
||||
certificateChain?: string;
|
||||
existingArn?: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./aws-certificate-manager-pki-sync-constants";
|
||||
export * from "./aws-certificate-manager-pki-sync-fns";
|
||||
export * from "./aws-certificate-manager-pki-sync-schemas";
|
||||
export * from "./aws-certificate-manager-pki-sync-types";
|
||||
@@ -1,5 +1,6 @@
|
||||
export enum PkiSync {
|
||||
AzureKeyVault = "azure-key-vault"
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AwsCertificateManager = "aws-certificate-manager"
|
||||
}
|
||||
|
||||
export enum PkiSyncStatus {
|
||||
|
||||
@@ -6,6 +6,8 @@ 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 { AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION } from "./aws-certificate-manager/aws-certificate-manager-pki-sync-constants";
|
||||
import { awsCertificateManagerPkiSyncFactory } from "./aws-certificate-manager/aws-certificate-manager-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";
|
||||
@@ -14,7 +16,8 @@ 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
|
||||
[PkiSync.AzureKeyVault]: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION,
|
||||
[PkiSync.AwsCertificateManager]: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const enterprisePkiSyncCheck = async (
|
||||
@@ -144,8 +147,10 @@ export const matchesCertificateNameSchema = (name: string, environment: string,
|
||||
return name.startsWith(prefix) && name.endsWith(suffix);
|
||||
};
|
||||
|
||||
const isAzureKeyVaultPkiSync = (pkiSync: TPkiSyncWithCredentials): boolean => {
|
||||
return pkiSync.destination === PkiSync.AzureKeyVault;
|
||||
const checkPkiSyncDestination = (pkiSync: TPkiSyncWithCredentials, destination: PkiSync): void => {
|
||||
if (pkiSync.destination !== destination) {
|
||||
throw new Error(`Invalid PKI sync destination: ${pkiSync.destination}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const PkiSyncFns = {
|
||||
@@ -163,6 +168,11 @@ export const PkiSyncFns = {
|
||||
"Azure Key Vault does not support importing certificates into Infisical (private keys cannot be extracted)"
|
||||
);
|
||||
}
|
||||
case PkiSync.AwsCertificateManager: {
|
||||
throw new Error(
|
||||
"AWS Certificate Manager does not support importing certificates into Infisical (private keys cannot be extracted)"
|
||||
);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`);
|
||||
}
|
||||
@@ -188,12 +198,15 @@ export const PkiSyncFns = {
|
||||
}> => {
|
||||
switch (pkiSync.destination) {
|
||||
case PkiSync.AzureKeyVault: {
|
||||
if (!isAzureKeyVaultPkiSync(pkiSync)) {
|
||||
throw new Error("Invalid Azure Key Vault PKI sync configuration");
|
||||
}
|
||||
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
|
||||
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies);
|
||||
return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap);
|
||||
}
|
||||
case PkiSync.AwsCertificateManager: {
|
||||
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
|
||||
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies);
|
||||
return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`);
|
||||
}
|
||||
@@ -209,13 +222,17 @@ export const PkiSyncFns = {
|
||||
): Promise<void> => {
|
||||
switch (pkiSync.destination) {
|
||||
case PkiSync.AzureKeyVault: {
|
||||
if (!isAzureKeyVaultPkiSync(pkiSync)) {
|
||||
throw new Error("Invalid Azure Key Vault PKI sync configuration");
|
||||
}
|
||||
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
|
||||
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies);
|
||||
await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames);
|
||||
break;
|
||||
}
|
||||
case PkiSync.AwsCertificateManager: {
|
||||
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
|
||||
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies);
|
||||
await awsCertificateManagerPkiSync.removeCertificates(pkiSync, certificateNames);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
||||
import { PkiSync } from "./pki-sync-enums";
|
||||
|
||||
export const PKI_SYNC_NAME_MAP: Record<PkiSync, string> = {
|
||||
[PkiSync.AzureKeyVault]: "Azure Key Vault"
|
||||
[PkiSync.AzureKeyVault]: "Azure Key Vault",
|
||||
[PkiSync.AwsCertificateManager]: "AWS Certificate Manager"
|
||||
};
|
||||
|
||||
export const PKI_SYNC_CONNECTION_MAP: Record<PkiSync, AppConnection> = {
|
||||
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault
|
||||
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
||||
[PkiSync.AwsCertificateManager]: AppConnection.AWS
|
||||
};
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create AWS Certificate Manager PKI Sync"
|
||||
openapi: "POST /api/v1/pki/syncs/aws-certificate-manager"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete AWS Certificate Manager PKI Sync"
|
||||
openapi: "DELETE /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get AWS Certificate Manager PKI Sync by ID"
|
||||
openapi: "GET /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List AWS Certificate Manager PKI Syncs"
|
||||
openapi: "GET /api/v1/pki/syncs/aws-certificate-manager"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Remove Certificates from AWS Certificate Manager"
|
||||
openapi: "POST /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}/remove-certificates"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sync Certificates to AWS Certificate Manager"
|
||||
openapi: "POST /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}/sync"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update AWS Certificate Manager PKI Sync"
|
||||
openapi: "PATCH /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}"
|
||||
---
|
||||
@@ -723,6 +723,7 @@
|
||||
{
|
||||
"group": "Syncs",
|
||||
"pages": [
|
||||
"documentation/platform/pki/certificate-syncs/aws-certificate-manager",
|
||||
"documentation/platform/pki/certificate-syncs/azure-key-vault"
|
||||
]
|
||||
}
|
||||
@@ -2523,6 +2524,18 @@
|
||||
"api-reference/endpoints/pki/syncs/list",
|
||||
"api-reference/endpoints/pki/syncs/get-by-id",
|
||||
"api-reference/endpoints/pki/syncs/options",
|
||||
{
|
||||
"group": "AWS Certificate Manager",
|
||||
"pages": [
|
||||
"api-reference/endpoints/pki/syncs/aws-certificate-manager/list",
|
||||
"api-reference/endpoints/pki/syncs/aws-certificate-manager/get-by-id",
|
||||
"api-reference/endpoints/pki/syncs/aws-certificate-manager/create",
|
||||
"api-reference/endpoints/pki/syncs/aws-certificate-manager/update",
|
||||
"api-reference/endpoints/pki/syncs/aws-certificate-manager/delete",
|
||||
"api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates",
|
||||
"api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Azure Key Vault",
|
||||
"pages": [
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
title: "AWS Certificate Manager"
|
||||
description: "Learn how to configure an AWS Certificate Manager Certificate Sync for Infisical PKI."
|
||||
---
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Set up and configure a [Certificate Authority](/documentation/platform/pki/overview)
|
||||
- Create an [AWS Connection](/integrations/app-connections/aws)
|
||||
|
||||
<Note>
|
||||
The AWS Certificate Manager Certificate Sync requires the following ACM permissions to be set on the IAM user/role
|
||||
for Infisical to sync certificates to AWS Certificate Manager: `acm:ListCertificates`, `acm:DescribeCertificate`, `acm:ImportCertificate`, `acm:DeleteCertificate`, and `acm:ListTagsForCertificate`.
|
||||
|
||||
These permissions allow Infisical to list, import, tag, and manage certificates in your AWS Certificate Manager service.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
Certificates synced to AWS Certificate Manager will be stored as imported certificates, 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.
|
||||

|
||||
|
||||
2. Select the **AWS Certificate Manager** option.
|
||||

|
||||
|
||||
3. Configure the **Source** from where certificates should be retrieved, then click **Next**.
|
||||

|
||||
|
||||
- **PKI Subscriber**: The PKI subscriber to retrieve certificates from.
|
||||
|
||||
4. Configure the **Destination** to where certificates should be deployed, then click **Next**.
|
||||

|
||||
|
||||
- **AWS Connection**: The AWS Connection to authenticate with.
|
||||
- **AWS Region**: The AWS region where certificates should be stored.
|
||||
|
||||
5. Configure the **Sync Options** to specify how certificates should be synced, then click **Next**.
|
||||

|
||||
|
||||
- **Auto-Sync Enabled**: If enabled, certificates will automatically be synced from the source PKI subscriber when changes occur. Disable to enforce manual syncing only.
|
||||
- **Enable Certificate Removal**: If enabled, Infisical will remove expired certificates from the destination during sync operations. Disable this option if you intend to manage certificate cleanup manually.
|
||||
- **Certificate Name Schema** (Optional): Customize how certificate tags are generated in AWS Certificate Manager. Must include `{{certificateId}}` as a placeholder for the certificate ID to ensure proper certificate identification and management. If not specified, defaults to `Infisical-{{certificateId}}`.
|
||||
|
||||
<Tip>
|
||||
**AWS Certificate Manager Certificate Limits**: AWS Certificate Manager has limits on the number of certificates per account and region. Refer to AWS documentation for current limits. Deleted certificates count toward your quota until they are permanently purged by AWS (typically after 30 days).
|
||||
</Tip>
|
||||
|
||||
6. Configure the **Details** of your AWS Certificate Manager Certificate Sync, then click **Next**.
|
||||

|
||||
|
||||
- **Name**: The name of your sync. Must be slug-friendly.
|
||||
- **Description**: An optional description for your sync.
|
||||
|
||||
7. Review your AWS Certificate Manager Certificate Sync configuration, then click **Create Sync**.
|
||||

|
||||
|
||||
8. If enabled, your AWS Certificate Manager Certificate Sync will begin syncing your certificates to the destination endpoint.
|
||||

|
||||
|
||||
</Tab>
|
||||
<Tab title="API">
|
||||
To create an **AWS Certificate Manager Certificate Sync**, make an API request to the [Create AWS Certificate Manager Certificate Sync](/api-reference/endpoints/pki/syncs/aws-certificate-manager/create) API endpoint.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://app.infisical.com/api/v1/pki/syncs/aws-certificate-manager \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "my-acm-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": "aws-certificate-manager",
|
||||
"isAutoSyncEnabled": true,
|
||||
"syncOptions": {
|
||||
"canRemoveCertificates": true,
|
||||
"certificateNameSchema": "myapp-{{certificateId}}"
|
||||
},
|
||||
"destinationConfig": {
|
||||
"region": "us-east-1"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Sample response
|
||||
|
||||
```json Response
|
||||
{
|
||||
"pkiSync": {
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"name": "my-acm-cert-sync",
|
||||
"description": "an example certificate sync",
|
||||
"destination": "aws-certificate-manager",
|
||||
"isAutoSyncEnabled": true,
|
||||
"destinationConfig": {
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"syncOptions": {
|
||||
"canRemoveCertificates": true,
|
||||
"certificateNameSchema": "myapp-{{certificateId}}"
|
||||
},
|
||||
"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 AWS Certificate Manager Certificate Sync will:
|
||||
|
||||
- **Automatic Deployment**: Deploy new certificates issued by your PKI subscriber to AWS Certificate Manager
|
||||
- **Certificate Updates**: Update certificates in AWS Certificate Manager when renewals occur
|
||||
- **Expiration Handling**: Optionally remove expired certificates from AWS Certificate Manager (if enabled)
|
||||
- **Tagging**: Automatically tag certificates with an InfisicalCertificate tag for easy identification and management
|
||||
|
||||
<Note>
|
||||
AWS Certificate Manager 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 Sync
|
||||
|
||||
You can manually trigger certificate synchronization from your PKI subscriber to AWS Certificate Manager using the sync certificates functionality. This is useful for:
|
||||
|
||||
- Initial setup when you have existing certificates to deploy
|
||||
- One-time sync of specific certificates
|
||||
- Testing certificate sync configurations
|
||||
- Force sync after making changes
|
||||
|
||||
To manually sync certificates, use the [Sync Certificates](/api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates) API endpoint or the manual sync option in the Infisical UI.
|
||||
|
||||
<Note>
|
||||
AWS Certificate Manager does not support importing certificates back into Infisical due to security limitations where private keys cannot be extracted from AWS Certificate Manager. Only certificates imported into ACM (not AWS-issued certificates) can be managed by the sync.
|
||||
</Note>
|
||||
|
After Width: | Height: | Size: 519 KiB |
|
After Width: | Height: | Size: 525 KiB |
|
After Width: | Height: | Size: 581 KiB |
|
After Width: | Height: | Size: 541 KiB |
|
After Width: | Height: | Size: 501 KiB |
|
After Width: | Height: | Size: 803 KiB |
|
After Width: | Height: | Size: 478 KiB |
@@ -177,6 +177,45 @@ Infisical supports two methods for connecting to AWS.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</Tab>
|
||||
<Tab title="PKI Sync">
|
||||
<AccordionGroup>
|
||||
<Accordion title="AWS Certificate Manager">
|
||||
Use the following custom policy to grant the minimum permissions required by Infisical to sync certificates to AWS Certificate Manager:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowCertificateManagerAccess",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"acm:ListCertificates",
|
||||
"acm:DescribeCertificate",
|
||||
"acm:GetCertificate",
|
||||
"acm:ImportCertificate",
|
||||
"acm:ExportCertificate",
|
||||
"acm:DeleteCertificate",
|
||||
"acm:AddTagsToCertificate",
|
||||
"acm:RemoveTagsFromCertificate",
|
||||
"acm:ListTagsForCertificate"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
<Note>
|
||||
- **ListCertificates**: Lists all certificates in the account
|
||||
- **ImportCertificate**: Imports certificates from Infisical into AWS Certificate Manager
|
||||
- **ExportCertificate**: Exports certificates for synchronization
|
||||
- **DeleteCertificate**: Removes certificates that are no longer managed by Infisical
|
||||
- **DescribeCertificate** and **GetCertificate**: Retrieves certificate details for comparison during sync
|
||||
- Tag-related permissions: Manages certificate tags for identification and organization
|
||||
</Note>
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
@@ -354,6 +393,45 @@ Infisical supports two methods for connecting to AWS.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</Tab>
|
||||
<Tab title="PKI Sync">
|
||||
<AccordionGroup>
|
||||
<Accordion title="AWS Certificate Manager">
|
||||
Use the following custom policy to grant the minimum permissions required by Infisical to sync certificates to AWS Certificate Manager:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowCertificateManagerAccess",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"acm:ListCertificates",
|
||||
"acm:DescribeCertificate",
|
||||
"acm:GetCertificate",
|
||||
"acm:ImportCertificate",
|
||||
"acm:ExportCertificate",
|
||||
"acm:DeleteCertificate",
|
||||
"acm:AddTagsToCertificate",
|
||||
"acm:RemoveTagsFromCertificate",
|
||||
"acm:ListTagsForCertificate"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
<Note>
|
||||
- **ListCertificates**: Lists all certificates in the account
|
||||
- **ImportCertificate**: Imports certificates from Infisical into AWS Certificate Manager
|
||||
- **ExportCertificate**: Exports certificates for synchronization
|
||||
- **DeleteCertificate**: Removes certificates that are no longer managed by Infisical
|
||||
- **DescribeCertificate** and **GetCertificate**: Retrieves certificate details for comparison during sync
|
||||
- Tag-related permissions: Manages certificate tags for identification and organization
|
||||
</Note>
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Obtain Access Key ID and Secret Access Key">
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
import { PkiSyncConnectionField } from "./PkiSyncConnectionField";
|
||||
|
||||
export const AwsCertificateManagerPkiSyncFields = () => {
|
||||
const { control, setValue } = useFormContext<
|
||||
TPkiSyncForm & { destination: PkiSync.AwsCertificateManager }
|
||||
>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PkiSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.region", "");
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.region"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="AWS Region"
|
||||
tooltipText="Select the AWS region where your Certificate Manager certificates should be stored."
|
||||
>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
placeholder="Select an AWS region"
|
||||
>
|
||||
{AWS_REGIONS.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -3,8 +3,8 @@ import { Controller, useFormContext } from "react-hook-form";
|
||||
import { FormControl, Input } from "@app/components/v2";
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
import { PkiSyncConnectionField } from "./PkiSyncConnectionField";
|
||||
import { TPkiSyncForm } from "./schemas";
|
||||
|
||||
export const AzureKeyVaultPkiSyncFields = () => {
|
||||
const { control, setValue } = useFormContext<
|
||||
|
||||
@@ -12,12 +12,12 @@ import { useProject } from "@app/context";
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { PkiSync, TPkiSync, useCreatePkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
|
||||
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
|
||||
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
|
||||
import { PkiSyncReviewFields } from "./PkiSyncReviewFields";
|
||||
import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
|
||||
import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas";
|
||||
|
||||
type Props = {
|
||||
onComplete: (pkiSync: TPkiSync) => void;
|
||||
|
||||
@@ -8,11 +8,11 @@ import { Button, ModalClose } from "@app/components/v2";
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { TPkiSync, useUpdatePkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { TUpdatePkiSyncForm, UpdatePkiSyncFormSchema } from "./schemas/pki-sync-schema";
|
||||
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
|
||||
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
|
||||
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
|
||||
import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
|
||||
import { TPkiSyncForm, UpdatePkiSyncFormSchema } from "./schemas";
|
||||
|
||||
type Props = {
|
||||
onComplete: (pkiSync: TPkiSync) => void;
|
||||
@@ -24,7 +24,7 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => {
|
||||
const updatePkiSync = useUpdatePkiSync();
|
||||
const { name: destinationName } = PKI_SYNC_MAP[pkiSync.destination];
|
||||
|
||||
const formMethods = useForm<TPkiSyncForm>({
|
||||
const formMethods = useForm<TUpdatePkiSyncForm>({
|
||||
resolver: zodResolver(UpdatePkiSyncFormSchema),
|
||||
defaultValues: {
|
||||
...pkiSync,
|
||||
@@ -33,11 +33,11 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => {
|
||||
id: pkiSync.connectionId,
|
||||
name: pkiSync.appConnectionName
|
||||
}
|
||||
} as Partial<TPkiSyncForm>,
|
||||
} as Partial<TUpdatePkiSyncForm>,
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
const onSubmit = async ({ connection, ...formData }: TPkiSyncForm) => {
|
||||
const onSubmit = async ({ connection, ...formData }: TUpdatePkiSyncForm) => {
|
||||
try {
|
||||
const updatedPkiSync = await updatePkiSync.mutateAsync({
|
||||
syncId: pkiSync.id,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import { PKI_SYNC_CONNECTION_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas";
|
||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
|
||||
type Props = {
|
||||
onChange?: VoidFunction;
|
||||
|
||||
@@ -2,8 +2,9 @@ import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
import { AwsCertificateManagerPkiSyncFields } from "./AwsCertificateManagerPkiSyncFields";
|
||||
import { AzureKeyVaultPkiSyncFields } from "./AzureKeyVaultPkiSyncFields";
|
||||
import { TPkiSyncForm } from "./schemas";
|
||||
|
||||
export const PkiSyncDestinationFields = () => {
|
||||
const { watch } = useFormContext<TPkiSyncForm>();
|
||||
@@ -13,6 +14,8 @@ export const PkiSyncDestinationFields = () => {
|
||||
switch (destination) {
|
||||
case PkiSync.AzureKeyVault:
|
||||
return <AzureKeyVaultPkiSyncFields />;
|
||||
case PkiSync.AwsCertificateManager:
|
||||
return <AwsCertificateManagerPkiSyncFields />;
|
||||
default:
|
||||
return (
|
||||
<div className="flex items-center justify-center rounded-md border border-red-500 bg-red-100 p-4 text-red-700">
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input, TextArea } from "@app/components/v2";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas";
|
||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
|
||||
export const PkiSyncDetailsFields = () => {
|
||||
const { control } = useFormContext<TPkiSyncForm>();
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { FormControl, Input, Switch, Tooltip } from "@app/components/v2";
|
||||
import { PkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { TPkiSyncForm } from "../schemas";
|
||||
import { TPkiSyncForm } from "../schemas/pki-sync-schema";
|
||||
|
||||
type Props = {
|
||||
destination?: PkiSync;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useProject } from "@app/context";
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { useListWorkspacePkiSubscribers } from "@app/hooks/api";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas";
|
||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
|
||||
export const PkiSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TPkiSyncForm>();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useListWorkspacePkiSubscribers } from "@app/hooks/api";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas";
|
||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
|
||||
export const PkiSyncSourceFields = () => {
|
||||
const { control } = useFormContext<TPkiSyncForm>();
|
||||
|
||||
@@ -5,4 +5,4 @@ export { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
|
||||
export { PkiSyncOptionsFields } from "./PkiSyncOptionsFields/PkiSyncOptionsFields";
|
||||
export { PkiSyncReviewFields } from "./PkiSyncReviewFields";
|
||||
export { PkiSyncSourceFields } from "./PkiSyncSourceFields";
|
||||
export { PkiSyncFormSchema, type TPkiSyncForm } from "./schemas";
|
||||
export { PkiSyncFormSchema, type TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
|
||||
|
||||
const AwsCertificateManagerSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(false),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => {
|
||||
// For AWS Certificate Manager, {{certificateId}} is always required if certificateNameSchema is provided
|
||||
if (!val) return true;
|
||||
|
||||
if (!val.includes("{{certificateId}}")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const allowedOptionalPlaceholders = ["{{environment}}"];
|
||||
const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders]
|
||||
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
|
||||
.join("|");
|
||||
|
||||
const allowedContentRegex = new RegExp(
|
||||
`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`
|
||||
);
|
||||
|
||||
return allowedContentRegex.test(val);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Certificate name schema must include {{certificateId}} placeholder for AWS Certificate Manager."
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
export const AwsCertificateManagerPkiSyncDestinationSchema = BasePkiSyncSchema(
|
||||
AwsCertificateManagerSyncOptionsSchema
|
||||
).merge(
|
||||
z.object({
|
||||
destination: z.literal(PkiSync.AwsCertificateManager),
|
||||
destinationConfig: z.object({
|
||||
region: z.string().min(1, "AWS region is required")
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
export const UpdateAwsCertificateManagerPkiSyncDestinationSchema =
|
||||
AwsCertificateManagerPkiSyncDestinationSchema.partial().merge(
|
||||
z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Name is required")
|
||||
.max(255, "Name must be less than 255 characters"),
|
||||
destination: z.literal(PkiSync.AwsCertificateManager),
|
||||
connection: z.object({
|
||||
id: z.string().uuid("Invalid connection ID format"),
|
||||
name: z.string().max(255, "Connection name must be less than 255 characters")
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
|
||||
|
||||
export const AzureKeyVaultPkiSyncDestinationSchema = BasePkiSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(PkiSync.AzureKeyVault),
|
||||
destinationConfig: z.object({
|
||||
vaultBaseUrl: z.string().min(1, "Vault base URL is required").url("Valid URL is required")
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
export const UpdateAzureKeyVaultPkiSyncDestinationSchema =
|
||||
AzureKeyVaultPkiSyncDestinationSchema.partial().merge(
|
||||
z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Name is required")
|
||||
.max(255, "Name must be less than 255 characters"),
|
||||
destination: z.literal(PkiSync.AzureKeyVault),
|
||||
connection: z.object({
|
||||
id: z.string().uuid("Invalid connection ID format"),
|
||||
name: z
|
||||
.string()
|
||||
.min(1, "Connection name is required")
|
||||
.max(255, "Connection name must be less than 255 characters")
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -1,25 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { AnyZodObject, z } from "zod";
|
||||
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
export const PkiSyncFormSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Name is required")
|
||||
.max(255, "Name must be less than 255 characters"),
|
||||
description: z.string().optional(),
|
||||
destination: z.nativeEnum(PkiSync),
|
||||
isAutoSyncEnabled: z.boolean().default(true),
|
||||
subscriberId: z.string().min(1, "PKI Subscriber is required"),
|
||||
connection: z.object({
|
||||
id: z.string().uuid("Invalid connection ID format"),
|
||||
name: z.string().max(255, "Connection name must be less than 255 characters")
|
||||
}),
|
||||
destinationConfig: z.object({
|
||||
vaultBaseUrl: z.string().url("Valid URL is required")
|
||||
}),
|
||||
syncOptions: z.object({
|
||||
export const BasePkiSyncSchema = <T extends AnyZodObject | undefined = undefined>(
|
||||
additionalSyncOptions?: T
|
||||
) => {
|
||||
const baseSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(false),
|
||||
certificateNameSchema: z
|
||||
@@ -53,22 +37,27 @@ export const PkiSyncFormSchema = z.object({
|
||||
"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."
|
||||
}
|
||||
)
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
export type TPkiSyncForm = z.infer<typeof PkiSyncFormSchema>;
|
||||
const syncOptionsSchema = additionalSyncOptions
|
||||
? baseSyncOptionsSchema.merge(additionalSyncOptions)
|
||||
: (baseSyncOptionsSchema as T extends AnyZodObject
|
||||
? z.ZodObject<z.objectUtil.MergeShapes<typeof baseSyncOptionsSchema.shape, T["shape"]>>
|
||||
: typeof baseSyncOptionsSchema);
|
||||
|
||||
export const UpdatePkiSyncFormSchema = PkiSyncFormSchema.partial().merge(
|
||||
z.object({
|
||||
return z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Name is required")
|
||||
.max(255, "Name must be less than 255 characters"),
|
||||
destination: z.nativeEnum(PkiSync),
|
||||
description: z.string().optional(),
|
||||
isAutoSyncEnabled: z.boolean().default(true),
|
||||
subscriberId: z.string().min(1, "PKI Subscriber is required"),
|
||||
connection: z.object({
|
||||
id: z.string().uuid("Invalid connection ID format"),
|
||||
name: z.string().max(255, "Connection name must be less than 255 characters")
|
||||
})
|
||||
})
|
||||
);
|
||||
}),
|
||||
syncOptions: syncOptionsSchema
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
AwsCertificateManagerPkiSyncDestinationSchema,
|
||||
UpdateAwsCertificateManagerPkiSyncDestinationSchema
|
||||
} from "./aws-certificate-manager-pki-sync-destination-schema";
|
||||
import {
|
||||
AzureKeyVaultPkiSyncDestinationSchema,
|
||||
UpdateAzureKeyVaultPkiSyncDestinationSchema
|
||||
} from "./azure-key-vault-pki-sync-destination-schema";
|
||||
|
||||
const PkiSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
AzureKeyVaultPkiSyncDestinationSchema,
|
||||
AwsCertificateManagerPkiSyncDestinationSchema
|
||||
]);
|
||||
|
||||
const UpdatePkiSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
UpdateAzureKeyVaultPkiSyncDestinationSchema,
|
||||
UpdateAwsCertificateManagerPkiSyncDestinationSchema
|
||||
]);
|
||||
|
||||
export const PkiSyncFormSchema = PkiSyncUnionSchema;
|
||||
|
||||
export const UpdatePkiSyncFormSchema = UpdatePkiSyncUnionSchema;
|
||||
|
||||
export type TPkiSyncForm = z.infer<typeof PkiSyncFormSchema>;
|
||||
|
||||
export type TUpdatePkiSyncForm = z.infer<typeof UpdatePkiSyncFormSchema>;
|
||||
@@ -11,9 +11,14 @@ export const PKI_SYNC_MAP: Record<
|
||||
[PkiSync.AzureKeyVault]: {
|
||||
name: "Azure Key Vault",
|
||||
image: "Microsoft Azure.png"
|
||||
},
|
||||
[PkiSync.AwsCertificateManager]: {
|
||||
name: "AWS Certificate Manager",
|
||||
image: "Amazon Web Services.png"
|
||||
}
|
||||
};
|
||||
|
||||
export const PKI_SYNC_CONNECTION_MAP: Record<PkiSync, AppConnection> = {
|
||||
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault
|
||||
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
||||
[PkiSync.AwsCertificateManager]: AppConnection.AWS
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export enum PkiSync {
|
||||
AzureKeyVault = "azure-key-vault"
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AwsCertificateManager = "aws-certificate-manager"
|
||||
}
|
||||
|
||||
export enum PkiSyncStatus {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import { PkiSync } from "../enums";
|
||||
import { TRootPkiSync } from "./common";
|
||||
|
||||
export type TAwsCertificateManagerPkiSync = TRootPkiSync & {
|
||||
destination: PkiSync.AwsCertificateManager;
|
||||
destinationConfig: {
|
||||
region: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.AWS;
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
import { DiscriminativePick } from "@app/types";
|
||||
|
||||
import { TAwsCertificateManagerPkiSync } from "./aws-certificate-manager-sync";
|
||||
import { TAzureKeyVaultPkiSync } from "./azure-key-vault-sync";
|
||||
|
||||
export type TPkiSyncOption = {
|
||||
@@ -16,27 +16,38 @@ export type TPkiSyncOption = {
|
||||
minCertificateNameLength?: number;
|
||||
};
|
||||
|
||||
export type TPkiSync = TAzureKeyVaultPkiSync;
|
||||
export type TPkiSync = TAzureKeyVaultPkiSync | TAwsCertificateManagerPkiSync;
|
||||
|
||||
export type TListPkiSyncs = { pkiSyncs: TPkiSync[] };
|
||||
|
||||
export type TListPkiSyncOptions = { pkiSyncOptions: TPkiSyncOption[] };
|
||||
|
||||
export type TCreatePkiSyncDTO = DiscriminativePick<
|
||||
TPkiSync,
|
||||
| "name"
|
||||
| "destinationConfig"
|
||||
| "description"
|
||||
| "connectionId"
|
||||
| "syncOptions"
|
||||
| "destination"
|
||||
| "isAutoSyncEnabled"
|
||||
> & { subscriberId?: string; projectId: string };
|
||||
type TCreatePkiSyncDTOBase = {
|
||||
name: string;
|
||||
description?: string;
|
||||
connectionId: string;
|
||||
syncOptions: {
|
||||
canImportCertificates: boolean;
|
||||
canRemoveCertificates: boolean;
|
||||
certificateNamePrefix?: string;
|
||||
certificateNameSchema?: string;
|
||||
};
|
||||
isAutoSyncEnabled: boolean;
|
||||
subscriberId?: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TCreatePkiSyncDTO = TCreatePkiSyncDTOBase & {
|
||||
destination: PkiSync;
|
||||
destinationConfig: {
|
||||
vaultBaseUrl?: string;
|
||||
region?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TUpdatePkiSyncDTO = Partial<Omit<TCreatePkiSyncDTO, "projectId">> & {
|
||||
syncId: string;
|
||||
projectId: string;
|
||||
destination: PkiSync;
|
||||
};
|
||||
|
||||
export type TDeletePkiSyncDTO = {
|
||||
@@ -63,4 +74,6 @@ export type TTriggerPkiSyncRemoveCertificatesDTO = {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export * from "./aws-certificate-manager-sync";
|
||||
export * from "./azure-key-vault-sync";
|
||||
export * from "./common";
|
||||
|
||||