diff --git a/.infisicalignore b/.infisicalignore index ec1cbfe167..f86a9b5601 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -54,4 +54,5 @@ k8-operator/config/samples/universalAuthIdentitySecret.yaml:generic-api-key:8 docs/integrations/app-connections/redis.mdx:generic-api-key:80 backend/src/ee/services/app-connections/chef/chef-connection-fns.ts:private-key:42 docs/documentation/platform/pki/enrollment-methods/api.mdx:generic-api-key:93 -docs/documentation/platform/pki/enrollment-methods/api.mdx:private-key:139 \ No newline at end of file +docs/documentation/platform/pki/enrollment-methods/api.mdx:private-key:139 +docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx:private-key:62 \ No newline at end of file diff --git a/backend/src/server/routes/v1/pki-sync-routers/aws-secrets-manager-pki-sync-router.ts b/backend/src/server/routes/v1/pki-sync-routers/aws-secrets-manager-pki-sync-router.ts new file mode 100644 index 0000000000..ca40c4b4f4 --- /dev/null +++ b/backend/src/server/routes/v1/pki-sync-routers/aws-secrets-manager-pki-sync-router.ts @@ -0,0 +1,22 @@ +import { + AWS_SECRETS_MANAGER_PKI_SYNC_LIST_OPTION, + AwsSecretsManagerPkiSyncSchema, + CreateAwsSecretsManagerPkiSyncSchema, + UpdateAwsSecretsManagerPkiSyncSchema +} from "@app/services/pki-sync/aws-secrets-manager"; +import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; + +import { registerSyncPkiEndpoints } from "./pki-sync-endpoints"; + +export const registerAwsSecretsManagerPkiSyncRouter = async (server: FastifyZodProvider) => + registerSyncPkiEndpoints({ + destination: PkiSync.AwsSecretsManager, + server, + responseSchema: AwsSecretsManagerPkiSyncSchema, + createSchema: CreateAwsSecretsManagerPkiSyncSchema, + updateSchema: UpdateAwsSecretsManagerPkiSyncSchema, + syncOptions: { + canImportCertificates: AWS_SECRETS_MANAGER_PKI_SYNC_LIST_OPTION.canImportCertificates, + canRemoveCertificates: AWS_SECRETS_MANAGER_PKI_SYNC_LIST_OPTION.canRemoveCertificates + } + }); diff --git a/backend/src/server/routes/v1/pki-sync-routers/index.ts b/backend/src/server/routes/v1/pki-sync-routers/index.ts index 39108433f1..e961d370cd 100644 --- a/backend/src/server/routes/v1/pki-sync-routers/index.ts +++ b/backend/src/server/routes/v1/pki-sync-routers/index.ts @@ -1,6 +1,7 @@ import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; import { registerAwsCertificateManagerPkiSyncRouter } from "./aws-certificate-manager-pki-sync-router"; +import { registerAwsSecretsManagerPkiSyncRouter } from "./aws-secrets-manager-pki-sync-router"; import { registerAzureKeyVaultPkiSyncRouter } from "./azure-key-vault-pki-sync-router"; import { registerChefPkiSyncRouter } from "./chef-pki-sync-router"; @@ -9,5 +10,6 @@ export * from "./pki-sync-router"; export const PKI_SYNC_REGISTER_ROUTER_MAP: Record Promise> = { [PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter, [PkiSync.AwsCertificateManager]: registerAwsCertificateManagerPkiSyncRouter, + [PkiSync.AwsSecretsManager]: registerAwsSecretsManagerPkiSyncRouter, [PkiSync.Chef]: registerChefPkiSyncRouter }; diff --git a/backend/src/services/pki-sync/aws-secrets-manager/aws-secrets-manager-pki-sync-constants.ts b/backend/src/services/pki-sync/aws-secrets-manager/aws-secrets-manager-pki-sync-constants.ts new file mode 100644 index 0000000000..fd325a3f5f --- /dev/null +++ b/backend/src/services/pki-sync/aws-secrets-manager/aws-secrets-manager-pki-sync-constants.ts @@ -0,0 +1,71 @@ +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 Secrets Manager naming constraints for secrets + */ +export const AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING = { + /** + * Regular expression pattern for valid AWS Secrets Manager secret names + * Must contain only alphanumeric characters, hyphens, and underscores + * Must be 1-512 characters long + */ + NAME_PATTERN: new RE2("^[\\w-]+$"), + + /** + * String of characters that are forbidden in AWS Secrets Manager secret names + */ + FORBIDDEN_CHARACTERS: " @#$%^&*()+=[]{}|;':\"<>?,./", + + /** + * Minimum length for secret names in AWS Secrets Manager + */ + MIN_LENGTH: 1, + + /** + * Maximum length for secret names in AWS Secrets Manager + */ + MAX_LENGTH: 512, + + /** + * String representation of the allowed character pattern (for UI display) + */ + ALLOWED_CHARACTER_PATTERN: "^[\\w-]+$" +} as const; + +export const AWS_SECRETS_MANAGER_PKI_SYNC_DEFAULTS = { + INFISICAL_PREFIX: "infisical-", + DEFAULT_ENVIRONMENT: "production", + DEFAULT_CERTIFICATE_NAME_SCHEMA: "infisical-{{certificateId}}", + DEFAULT_FIELD_MAPPINGS: { + certificate: "certificate", + privateKey: "private_key", + certificateChain: "certificate_chain", + caCertificate: "ca_certificate" + } +}; + +export const AWS_SECRETS_MANAGER_PKI_SYNC_OPTIONS = { + DEFAULT_CAN_REMOVE_CERTIFICATES: true, + DEFAULT_PRESERVE_SECRET_ON_RENEWAL: true, + DEFAULT_UPDATE_EXISTING_CERTIFICATES: true, + DEFAULT_CAN_IMPORT_CERTIFICATES: false +}; + +/** + * AWS Secrets Manager PKI Sync list option configuration + */ +export const AWS_SECRETS_MANAGER_PKI_SYNC_LIST_OPTION = { + name: "AWS Secrets Manager" as const, + connection: AppConnection.AWS, + destination: PkiSync.AwsSecretsManager, + canImportCertificates: false, + canRemoveCertificates: true, + defaultCertificateNameSchema: "infisical-{{certificateId}}", + forbiddenCharacters: AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS, + allowedCharacterPattern: AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.ALLOWED_CHARACTER_PATTERN, + maxCertificateNameLength: AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.MAX_LENGTH, + minCertificateNameLength: AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.MIN_LENGTH +} as const; diff --git a/backend/src/services/pki-sync/aws-secrets-manager/aws-secrets-manager-pki-sync-fns.ts b/backend/src/services/pki-sync/aws-secrets-manager/aws-secrets-manager-pki-sync-fns.ts new file mode 100644 index 0000000000..cb4227ad12 --- /dev/null +++ b/backend/src/services/pki-sync/aws-secrets-manager/aws-secrets-manager-pki-sync-fns.ts @@ -0,0 +1,635 @@ +/* eslint-disable no-continue */ +/* eslint-disable no-await-in-loop */ +import { + CreateSecretCommand, + DeleteSecretCommand, + GetSecretValueCommand, + ListSecretsCommand, + SecretsManagerClient, + UpdateSecretCommand +} from "@aws-sdk/client-secrets-manager"; +import RE2 from "re2"; + +import { TCertificateSyncs } from "@app/db/schemas"; +import { CustomAWSHasher } from "@app/lib/aws/hashing"; +import { crypto } from "@app/lib/crypto"; +import { logger } from "@app/lib/logger"; +import { AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; +import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types"; +import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal"; +import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums"; +import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue"; +import { matchesCertificateNameSchema } from "@app/services/pki-sync/pki-sync-fns"; +import { TCertificateMap, TPkiSyncWithCredentials } from "@app/services/pki-sync/pki-sync-types"; + +import { AWS_SECRETS_MANAGER_PKI_SYNC_DEFAULTS } from "./aws-secrets-manager-pki-sync-constants"; +import { + AwsSecretsManagerCertificateSecret, + SyncCertificatesResult, + TAwsSecretsManagerPkiSyncWithCredentials +} from "./aws-secrets-manager-pki-sync-types"; + +const AWS_SECRETS_MANAGER_RATE_LIMIT_CONFIG: RateLimitConfig = { + MAX_CONCURRENT_REQUESTS: 10, + BASE_DELAY: 1000, + MAX_DELAY: 30000, + MAX_RETRIES: 3, + RATE_LIMIT_STATUS_CODES: [429, 503] +}; + +const awsSecretsManagerConnectionQueue = createConnectionQueue(AWS_SECRETS_MANAGER_RATE_LIMIT_CONFIG); +const { withRateLimitRetry } = awsSecretsManagerConnectionQueue; + +const MAX_RETRIES = 10; + +const sleep = async () => + new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + +const isInfisicalManagedCertificate = (secretName: string, pkiSync: TPkiSyncWithCredentials): boolean => { + const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined; + const certificateNameSchema = syncOptions?.certificateNameSchema; + + if (certificateNameSchema) { + const environment = AWS_SECRETS_MANAGER_PKI_SYNC_DEFAULTS.DEFAULT_ENVIRONMENT; + return matchesCertificateNameSchema(secretName, environment, certificateNameSchema); + } + + return secretName.startsWith(AWS_SECRETS_MANAGER_PKI_SYNC_DEFAULTS.INFISICAL_PREFIX); +}; + +const parseErrorMessage = (error: unknown): string => { + if (error instanceof Error) { + return error.message; + } + + if (typeof error === "string") { + return error; + } + + if (error && typeof error === "object" && "message" in error) { + const { message } = error as { message: unknown }; + if (typeof message === "string") { + return message; + } + } + + return "Unknown error occurred"; +}; + +const getSecretsManagerClient = async (pkiSync: TAwsSecretsManagerPkiSyncWithCredentials) => { + const { destinationConfig, connection } = pkiSync; + + const config = await getAwsConnectionConfig( + connection as TAwsConnectionConfig, + destinationConfig.region as AWSRegion + ); + + if (!config.credentials) { + throw new Error("AWS credentials not found in connection configuration"); + } + + const secretsManagerClient = new SecretsManagerClient({ + region: config.region, + useFipsEndpoint: crypto.isFipsModeEnabled(), + sha256: CustomAWSHasher, + credentials: config.credentials + }); + + return secretsManagerClient; +}; + +type TAwsSecretsManagerPkiSyncFactoryDeps = { + certificateDAL: Pick; + certificateSyncDAL: Pick< + TCertificateSyncDALFactory, + | "removeCertificates" + | "addCertificates" + | "findByPkiSyncAndCertificate" + | "updateById" + | "findByPkiSyncId" + | "updateSyncStatus" + >; +}; + +export const awsSecretsManagerPkiSyncFactory = ({ + certificateDAL, + certificateSyncDAL +}: TAwsSecretsManagerPkiSyncFactoryDeps) => { + const $getSecretsManagerSecrets = async ( + pkiSync: TAwsSecretsManagerPkiSyncWithCredentials, + syncId = "unknown" + ): Promise> => { + const client = await getSecretsManagerClient(pkiSync); + const secrets: Record = {}; + let hasNext = true; + let nextToken: string | undefined; + let attempt = 0; + + while (hasNext) { + try { + const currentToken = nextToken; + const output = await withRateLimitRetry( + () => client.send(new ListSecretsCommand({ NextToken: currentToken })), + { + operation: "list-secrets-manager-secrets", + syncId + } + ); + + attempt = 0; + + if (output.SecretList) { + output.SecretList.forEach((secretEntry) => { + if ( + secretEntry.Name && + isInfisicalManagedCertificate(secretEntry.Name, pkiSync as unknown as TPkiSyncWithCredentials) + ) { + secrets[secretEntry.Name] = secretEntry.ARN || secretEntry.Name; + } + }); + } + + hasNext = Boolean(output.NextToken); + nextToken = output.NextToken; + } catch (e) { + if ( + e && + typeof e === "object" && + "name" in e && + (e as { name: string }).name === "ThrottlingException" && + attempt < MAX_RETRIES + ) { + attempt += 1; + await sleep(); + continue; + } + throw e; + } + } + + return secrets; + }; + + const syncCertificates = async ( + pkiSync: TPkiSyncWithCredentials, + certificateMap: TCertificateMap + ): Promise => { + const awsPkiSync = pkiSync as unknown as TAwsSecretsManagerPkiSyncWithCredentials; + const client = await getSecretsManagerClient(awsPkiSync); + + const existingSecrets = await $getSecretsManagerSecrets(awsPkiSync, pkiSync.id); + + const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id); + const syncRecordsByCertId = new Map(); + const syncRecordsByExternalId = new Map(); + + existingSyncRecords.forEach((record: TCertificateSyncs) => { + if (record.certificateId) { + syncRecordsByCertId.set(record.certificateId, record); + } + if (record.externalIdentifier) { + syncRecordsByExternalId.set(record.externalIdentifier, record); + } + }); + + type CertificateUploadData = { + secretName: string; + certificateData: AwsSecretsManagerCertificateSecret; + certificateId: string; + isUpdate: boolean; + targetSecretName: string; + oldCertificateIdToRemove?: string; + }; + + const setCertificates: CertificateUploadData[] = []; + const validationErrors: Array<{ name: string; error: string }> = []; + + const syncOptions = pkiSync.syncOptions as + | { + canRemoveCertificates?: boolean; + preserveSecretOnRenewal?: boolean; + fieldMappings?: { + certificate?: string; + privateKey?: string; + certificateChain?: string; + caCertificate?: string; + }; + certificateNameSchema?: string; + } + | undefined; + + const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true; + const preserveSecretOnRenewal = syncOptions?.preserveSecretOnRenewal ?? true; + + const fieldMappings = { + certificate: syncOptions?.fieldMappings?.certificate ?? "certificate", + privateKey: syncOptions?.fieldMappings?.privateKey ?? "private_key", + certificateChain: syncOptions?.fieldMappings?.certificateChain ?? "certificate_chain", + caCertificate: syncOptions?.fieldMappings?.caCertificate ?? "ca_certificate" + }; + + const activeExternalIdentifiers = new Set(); + + for (const [certName, certData] of Object.entries(certificateMap)) { + const { cert, privateKey: certPrivateKey, certificateChain, caCertificate, certificateId } = certData; + + if (!cert || cert.trim().length === 0) { + validationErrors.push({ + name: certName, + error: "Certificate content is empty or missing" + }); + continue; + } + + if (!certPrivateKey || certPrivateKey.trim().length === 0) { + validationErrors.push({ + name: certName, + error: "Private key content is empty or missing" + }); + continue; + } + + if (!certificateId || typeof certificateId !== "string") { + continue; + } + + const certificateData: AwsSecretsManagerCertificateSecret = { + [fieldMappings.certificate]: cert, + [fieldMappings.privateKey]: certPrivateKey + }; + + if (certificateChain && certificateChain.trim().length > 0) { + certificateData[fieldMappings.certificateChain] = certificateChain; + } + + if (caCertificate && typeof caCertificate === "string" && caCertificate.trim().length > 0) { + certificateData[fieldMappings.caCertificate] = caCertificate; + } + + let targetSecretName = certName; + if (syncOptions?.certificateNameSchema) { + const extendedCertData = certData as Record; + const safeCommonName = typeof extendedCertData.commonName === "string" ? extendedCertData.commonName : ""; + + targetSecretName = syncOptions.certificateNameSchema + .replace(new RE2("\\{\\{certificateId\\}\\}", "g"), certificateId) + .replace(new RE2("\\{\\{commonName\\}\\}", "g"), safeCommonName); + } else { + targetSecretName = `${AWS_SECRETS_MANAGER_PKI_SYNC_DEFAULTS.INFISICAL_PREFIX}${certificateId}`; + } + + const certificate = await certificateDAL.findById(certificateId); + + if (certificate?.renewedByCertificateId) { + continue; + } + + const syncRecordLookupId = certificate?.renewedFromCertificateId || certificateId; + const existingRecord = syncRecordsByCertId.get(syncRecordLookupId); + + let shouldProcess = true; + let isUpdate = false; + + if (existingRecord?.externalIdentifier) { + const existingSecret = existingSecrets[existingRecord.externalIdentifier]; + + if (existingSecret) { + if (certificate?.renewedFromCertificateId && preserveSecretOnRenewal) { + targetSecretName = existingRecord.externalIdentifier; + isUpdate = true; + } else if (certificate?.renewedFromCertificateId && !preserveSecretOnRenewal) { + activeExternalIdentifiers.add(existingRecord.externalIdentifier); + } else if (!certificate?.renewedFromCertificateId) { + shouldProcess = false; + } + } + } + + if (!shouldProcess) { + continue; + } + + if (existingSecrets[targetSecretName]) { + isUpdate = true; + } + + activeExternalIdentifiers.add(targetSecretName); + + setCertificates.push({ + secretName: certName, + certificateData, + certificateId, + isUpdate, + targetSecretName, + oldCertificateIdToRemove: + certificate?.renewedFromCertificateId && preserveSecretOnRenewal + ? certificate.renewedFromCertificateId + : undefined + }); + } + + const result: SyncCertificatesResult = { + uploaded: 0, + updated: 0, + removed: 0, + failedRemovals: 0, + skipped: 0, + details: { + failedUploads: [], + failedRemovals: [], + validationErrors + } + }; + + for (const certData of setCertificates) { + const { secretName, certificateData, certificateId, isUpdate, targetSecretName, oldCertificateIdToRemove } = + certData; + + try { + const secretValue = JSON.stringify(certificateData); + const configKeyId: unknown = awsPkiSync.destinationConfig.keyId; + const keyId: string = typeof configKeyId === "string" ? configKeyId : "alias/aws/secretsmanager"; + + if (isUpdate) { + await withRateLimitRetry( + () => + client.send( + new UpdateSecretCommand({ + SecretId: targetSecretName, + SecretString: secretValue, + KmsKeyId: keyId + }) + ), + { + operation: "update-secret", + syncId: pkiSync.id + } + ); + result.updated += 1; + } else { + await withRateLimitRetry( + () => + client.send( + new CreateSecretCommand({ + Name: targetSecretName, + SecretString: secretValue, + KmsKeyId: keyId, + Description: `Certificate managed by Infisical` + }) + ), + { + operation: "create-secret", + syncId: pkiSync.id + } + ); + result.uploaded += 1; + } + + const existingRecord = syncRecordsByCertId.get(certificateId); + if (existingRecord?.id) { + await certificateSyncDAL.updateById(existingRecord.id, { + externalIdentifier: targetSecretName, + syncStatus: CertificateSyncStatus.Succeeded, + lastSyncedAt: new Date(), + lastSyncMessage: "Certificate successfully synced to AWS Secrets Manager" + }); + + if (oldCertificateIdToRemove && oldCertificateIdToRemove !== certificateId) { + await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateIdToRemove]); + } + } else { + await certificateSyncDAL.addCertificates(pkiSync.id, [ + { + certificateId, + externalIdentifier: targetSecretName + } + ]); + + const newCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId); + if (newCertSync?.id) { + await certificateSyncDAL.updateById(newCertSync.id, { + syncStatus: CertificateSyncStatus.Succeeded, + lastSyncedAt: new Date(), + lastSyncMessage: "Certificate successfully synced to AWS Secrets Manager" + }); + } + } + } catch (error) { + result.details?.failedUploads?.push({ + name: secretName, + error: parseErrorMessage(error) + }); + logger.error( + { + secretName, + certificateId, + error: parseErrorMessage(error), + pkiSyncId: pkiSync.id + }, + "Failed to sync certificate" + ); + + const existingRecord = syncRecordsByCertId.get(certificateId); + if (existingRecord?.id) { + await certificateSyncDAL.updateById(existingRecord.id, { + syncStatus: CertificateSyncStatus.Failed, + lastSyncMessage: parseErrorMessage(error) + }); + } + } + } + + if (canRemoveCertificates) { + for (const [secretName] of Object.entries(existingSecrets)) { + if (!activeExternalIdentifiers.has(secretName)) { + try { + await withRateLimitRetry( + () => + client.send( + new DeleteSecretCommand({ + SecretId: secretName, + ForceDeleteWithoutRecovery: true + }) + ), + { + operation: "delete-secret", + syncId: pkiSync.id + } + ); + + result.removed += 1; + + const recordToRemove = syncRecordsByExternalId.get(secretName); + if (recordToRemove?.id) { + await certificateSyncDAL.updateById(recordToRemove.id, { + syncStatus: CertificateSyncStatus.Failed + }); + } + } catch (error) { + result.failedRemovals += 1; + result.details?.failedRemovals?.push({ + name: secretName, + error: parseErrorMessage(error) + }); + logger.error( + { + secretName, + error: parseErrorMessage(error), + pkiSyncId: pkiSync.id + }, + "Failed to remove certificate secret" + ); + } + } + } + } + + return result; + }; + + const removeCertificates = async ( + pkiSync: TPkiSyncWithCredentials, + certificateMap: TCertificateMap + ): Promise<{ removed: number; failed: number }> => { + const awsPkiSync = pkiSync as unknown as TAwsSecretsManagerPkiSyncWithCredentials; + const client = await getSecretsManagerClient(awsPkiSync); + + const existingSecrets = await $getSecretsManagerSecrets(awsPkiSync, pkiSync.id); + const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id); + + let removed = 0; + let failed = 0; + + for (const [, certData] of Object.entries(certificateMap)) { + if (!certData.certificateId) continue; + + const syncRecord = existingSyncRecords.find((record) => record.certificateId === certData.certificateId); + if (!syncRecord?.externalIdentifier) continue; + + const secretName = syncRecord.externalIdentifier; + + if (existingSecrets[secretName]) { + try { + await withRateLimitRetry( + () => + client.send( + new DeleteSecretCommand({ + SecretId: secretName, + ForceDeleteWithoutRecovery: true + }) + ), + { + operation: "delete-secret", + syncId: pkiSync.id + } + ); + + if (syncRecord.id) { + await certificateSyncDAL.updateById(syncRecord.id, { + syncStatus: CertificateSyncStatus.Failed + }); + } + + removed += 1; + } catch (error) { + failed += 1; + logger.error( + { + secretName, + certificateId: certData.certificateId, + error: parseErrorMessage(error), + pkiSyncId: pkiSync.id + }, + "Failed to remove certificate secret" + ); + } + } + } + + return { removed, failed }; + }; + + const importCertificates = async (pkiSync: TPkiSyncWithCredentials): Promise => { + const awsPkiSync = pkiSync as unknown as TAwsSecretsManagerPkiSyncWithCredentials; + const client = await getSecretsManagerClient(awsPkiSync); + + const existingSecrets = await $getSecretsManagerSecrets(awsPkiSync, pkiSync.id); + const certificateMap: TCertificateMap = {}; + + const syncOptions = pkiSync.syncOptions as + | { + fieldMappings?: { + certificate?: string; + privateKey?: string; + certificateChain?: string; + caCertificate?: string; + }; + } + | undefined; + const fieldMappings = { + certificate: syncOptions?.fieldMappings?.certificate ?? "certificate", + privateKey: syncOptions?.fieldMappings?.privateKey ?? "private_key", + certificateChain: syncOptions?.fieldMappings?.certificateChain ?? "certificate_chain", + caCertificate: syncOptions?.fieldMappings?.caCertificate ?? "ca_certificate" + }; + + for (const [secretName] of Object.entries(existingSecrets)) { + try { + const secretValueResult = await withRateLimitRetry( + () => + client.send( + new GetSecretValueCommand({ + SecretId: secretName + }) + ), + { + operation: "get-secret-value", + syncId: pkiSync.id + } + ); + + if (secretValueResult.SecretString) { + const secretData = JSON.parse(secretValueResult.SecretString) as AwsSecretsManagerCertificateSecret; + + const cert = secretData[fieldMappings.certificate]; + const privateKey = secretData[fieldMappings.privateKey]; + const certificateChain = secretData[fieldMappings.certificateChain]; + const caCertificate = secretData[fieldMappings.caCertificate]; + + if (typeof cert === "string" && typeof privateKey === "string") { + certificateMap[secretName] = { + cert, + privateKey, + certificateChain: typeof certificateChain === "string" ? certificateChain : undefined, + caCertificate: typeof caCertificate === "string" ? caCertificate : undefined, + certificateId: secretName + }; + } + } + } catch (error) { + logger.error( + { + secretName, + error: parseErrorMessage(error), + pkiSyncId: pkiSync.id + }, + "Failed to import certificate from secret" + ); + } + } + + return certificateMap; + }; + + return { + syncCertificates, + removeCertificates, + importCertificates + }; +}; + +export type TAwsSecretsManagerPkiSyncFactory = ReturnType; diff --git a/backend/src/services/pki-sync/aws-secrets-manager/aws-secrets-manager-pki-sync-schemas.ts b/backend/src/services/pki-sync/aws-secrets-manager/aws-secrets-manager-pki-sync-schemas.ts new file mode 100644 index 0000000000..063e42683e --- /dev/null +++ b/backend/src/services/pki-sync/aws-secrets-manager/aws-secrets-manager-pki-sync-schemas.ts @@ -0,0 +1,103 @@ +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_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING } from "./aws-secrets-manager-pki-sync-constants"; + +export const AwsSecretsManagerPkiSyncConfigSchema = z.object({ + region: z.nativeEnum(AWSRegion), + keyId: z.string().trim().optional() +}); + +export const AwsSecretsManagerFieldMappingsSchema = z.object({ + certificate: z.string().min(1, "Certificate field name is required").default("certificate"), + privateKey: z.string().min(1, "Private key field name is required").default("private_key"), + certificateChain: z.string().min(1, "Certificate chain field name is required").default("certificate_chain"), + caCertificate: z.string().min(1, "CA certificate field name is required").default("ca_certificate") +}); + +const AwsSecretsManagerPkiSyncOptionsSchema = z.object({ + canImportCertificates: z.boolean().default(false), + canRemoveCertificates: z.boolean().default(true), + preserveSecretOnRenewal: z.boolean().default(true), + updateExistingCertificates: z.boolean().default(true), + certificateNameSchema: z + .string() + .optional() + .refine( + (schema) => { + if (!schema) return true; + + if (!schema.includes("{{certificateId}}")) { + return false; + } + + const testName = schema + .replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "test-cert-id") + .replace(new RE2("\\{\\{profileId\\}\\}", "g"), "test-profile-id") + .replace(new RE2("\\{\\{commonName\\}\\}", "g"), "test-common-name") + .replace(new RE2("\\{\\{friendlyName\\}\\}", "g"), "test-friendly-name") + .replace(new RE2("\\{\\{environment\\}\\}", "g"), "test-env"); + + const hasForbiddenChars = AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some( + (char) => testName.includes(char) + ); + + return ( + AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.NAME_PATTERN.test(testName) && + !hasForbiddenChars && + testName.length >= AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.MIN_LENGTH && + testName.length <= AWS_SECRETS_MANAGER_PKI_SYNC_CERTIFICATE_NAMING.MAX_LENGTH + ); + }, + { + message: + "Certificate name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, underscores, and hyphens and be 1-512 characters long for AWS Secrets Manager." + } + ), + fieldMappings: AwsSecretsManagerFieldMappingsSchema.optional().default({ + certificate: "certificate", + privateKey: "private_key", + certificateChain: "certificate_chain", + caCertificate: "ca_certificate" + }) +}); + +export const AwsSecretsManagerPkiSyncSchema = PkiSyncSchema.extend({ + destination: z.literal(PkiSync.AwsSecretsManager), + destinationConfig: AwsSecretsManagerPkiSyncConfigSchema, + syncOptions: AwsSecretsManagerPkiSyncOptionsSchema +}); + +export const CreateAwsSecretsManagerPkiSyncSchema = z.object({ + name: z.string().trim().min(1).max(64), + description: z.string().optional(), + isAutoSyncEnabled: z.boolean().default(true), + destinationConfig: AwsSecretsManagerPkiSyncConfigSchema, + syncOptions: AwsSecretsManagerPkiSyncOptionsSchema.optional().default({}), + subscriberId: z.string().nullish(), + connectionId: z.string(), + projectId: z.string().trim().min(1), + certificateIds: z.array(z.string().uuid()).optional() +}); + +export const UpdateAwsSecretsManagerPkiSyncSchema = z.object({ + name: z.string().trim().min(1).max(64).optional(), + description: z.string().optional(), + isAutoSyncEnabled: z.boolean().optional(), + destinationConfig: AwsSecretsManagerPkiSyncConfigSchema.optional(), + syncOptions: AwsSecretsManagerPkiSyncOptionsSchema.optional(), + subscriberId: z.string().nullish(), + connectionId: z.string().optional() +}); + +export const AwsSecretsManagerPkiSyncListItemSchema = z.object({ + name: z.literal("AWS Secrets Manager"), + connection: z.literal(AppConnection.AWS), + destination: z.literal(PkiSync.AwsSecretsManager), + canImportCertificates: z.literal(false), + canRemoveCertificates: z.literal(true) +}); diff --git a/backend/src/services/pki-sync/aws-secrets-manager/aws-secrets-manager-pki-sync-types.ts b/backend/src/services/pki-sync/aws-secrets-manager/aws-secrets-manager-pki-sync-types.ts new file mode 100644 index 0000000000..7c4a5f8a14 --- /dev/null +++ b/backend/src/services/pki-sync/aws-secrets-manager/aws-secrets-manager-pki-sync-types.ts @@ -0,0 +1,59 @@ +import { z } from "zod"; + +import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types"; + +import { + AwsSecretsManagerFieldMappingsSchema, + AwsSecretsManagerPkiSyncConfigSchema, + AwsSecretsManagerPkiSyncSchema, + CreateAwsSecretsManagerPkiSyncSchema, + UpdateAwsSecretsManagerPkiSyncSchema +} from "./aws-secrets-manager-pki-sync-schemas"; + +export type TAwsSecretsManagerPkiSyncConfig = z.infer; + +export type TAwsSecretsManagerFieldMappings = z.infer; + +export type TAwsSecretsManagerPkiSync = z.infer; + +export type TAwsSecretsManagerPkiSyncInput = z.infer; + +export type TAwsSecretsManagerPkiSyncUpdate = z.infer; + +export type TAwsSecretsManagerPkiSyncWithCredentials = TAwsSecretsManagerPkiSync & { + connection: TAwsConnection; + appConnectionName: string; + appConnectionApp: string; +}; + +export interface AwsSecretsManagerCertificateSecret { + [key: string]: string; +} + +export interface SyncCertificatesResult { + uploaded: number; + updated: number; + removed: number; + failedRemovals: number; + skipped: number; + details?: { + failedUploads?: Array<{ name: string; error: string }>; + failedRemovals?: Array<{ name: string; error: string }>; + validationErrors?: Array<{ name: string; error: string }>; + }; +} + +export interface RemoveCertificatesResult { + removed: number; + failed: number; + skipped: number; +} + +export interface CertificateImportRequest { + name: string; + certificate: string; + privateKey: string; + certificateChain?: string; + caCertificate?: string; + certificateId?: string; +} diff --git a/backend/src/services/pki-sync/aws-secrets-manager/index.ts b/backend/src/services/pki-sync/aws-secrets-manager/index.ts new file mode 100644 index 0000000000..7f6e116649 --- /dev/null +++ b/backend/src/services/pki-sync/aws-secrets-manager/index.ts @@ -0,0 +1,4 @@ +export * from "./aws-secrets-manager-pki-sync-constants"; +export * from "./aws-secrets-manager-pki-sync-fns"; +export * from "./aws-secrets-manager-pki-sync-schemas"; +export * from "./aws-secrets-manager-pki-sync-types"; diff --git a/backend/src/services/pki-sync/pki-sync-enums.ts b/backend/src/services/pki-sync/pki-sync-enums.ts index 0a8c3a4ee0..bea444372a 100644 --- a/backend/src/services/pki-sync/pki-sync-enums.ts +++ b/backend/src/services/pki-sync/pki-sync-enums.ts @@ -1,6 +1,7 @@ export enum PkiSync { AzureKeyVault = "azure-key-vault", AwsCertificateManager = "aws-certificate-manager", + AwsSecretsManager = "aws-secrets-manager", Chef = "chef" } diff --git a/backend/src/services/pki-sync/pki-sync-fns.ts b/backend/src/services/pki-sync/pki-sync-fns.ts index c44c66e075..1d91a582dc 100644 --- a/backend/src/services/pki-sync/pki-sync-fns.ts +++ b/backend/src/services/pki-sync/pki-sync-fns.ts @@ -10,6 +10,8 @@ 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 { AWS_SECRETS_MANAGER_PKI_SYNC_LIST_OPTION } from "./aws-secrets-manager/aws-secrets-manager-pki-sync-constants"; +import { awsSecretsManagerPkiSyncFactory } from "./aws-secrets-manager/aws-secrets-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 { chefPkiSyncFactory } from "./chef/chef-pki-sync-fns"; @@ -22,6 +24,7 @@ const ENTERPRISE_PKI_SYNCS: PkiSync[] = []; const PKI_SYNC_LIST_OPTIONS = { [PkiSync.AzureKeyVault]: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION, [PkiSync.AwsCertificateManager]: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION, + [PkiSync.AwsSecretsManager]: AWS_SECRETS_MANAGER_PKI_SYNC_LIST_OPTION, [PkiSync.Chef]: CHEF_PKI_SYNC_LIST_OPTION }; @@ -165,6 +168,8 @@ export const PkiSyncFns = { dependencies: { appConnectionDAL: Pick; kmsService: Pick; + certificateDAL: TCertificateDALFactory; + certificateSyncDAL: TCertificateSyncDALFactory; } ): Promise => { switch (pkiSync.destination) { @@ -178,6 +183,14 @@ export const PkiSyncFns = { "AWS Certificate Manager does not support importing certificates into Infisical (private keys cannot be extracted)" ); } + case PkiSync.AwsSecretsManager: { + checkPkiSyncDestination(pkiSync, PkiSync.AwsSecretsManager as PkiSync); + const awsSecretsManagerPkiSync = awsSecretsManagerPkiSyncFactory({ + certificateDAL: dependencies.certificateDAL, + certificateSyncDAL: dependencies.certificateSyncDAL + }); + return awsSecretsManagerPkiSync.importCertificates(pkiSync); + } case PkiSync.Chef: { throw new Error( "Chef does not support importing certificates into Infisical (private keys cannot be extracted securely)" @@ -211,7 +224,7 @@ export const PkiSyncFns = { }> => { switch (pkiSync.destination) { case PkiSync.AzureKeyVault: { - checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault); + checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault as PkiSync); const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({ appConnectionDAL: dependencies.appConnectionDAL, kmsService: dependencies.kmsService, @@ -221,7 +234,7 @@ export const PkiSyncFns = { return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap); } case PkiSync.AwsCertificateManager: { - checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager); + checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager as PkiSync); const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({ appConnectionDAL: dependencies.appConnectionDAL, kmsService: dependencies.kmsService, @@ -230,8 +243,16 @@ export const PkiSyncFns = { }); return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap); } + case PkiSync.AwsSecretsManager: { + checkPkiSyncDestination(pkiSync, PkiSync.AwsSecretsManager as PkiSync); + const awsSecretsManagerPkiSync = awsSecretsManagerPkiSyncFactory({ + certificateDAL: dependencies.certificateDAL, + certificateSyncDAL: dependencies.certificateSyncDAL + }); + return awsSecretsManagerPkiSync.syncCertificates(pkiSync, certificateMap); + } case PkiSync.Chef: { - checkPkiSyncDestination(pkiSync, PkiSync.Chef); + checkPkiSyncDestination(pkiSync, PkiSync.Chef as PkiSync); const chefPkiSync = chefPkiSyncFactory({ certificateDAL: dependencies.certificateDAL, certificateSyncDAL: dependencies.certificateSyncDAL @@ -256,7 +277,7 @@ export const PkiSyncFns = { ): Promise => { switch (pkiSync.destination) { case PkiSync.AzureKeyVault: { - checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault); + checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault as PkiSync); const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({ appConnectionDAL: dependencies.appConnectionDAL, kmsService: dependencies.kmsService, @@ -270,7 +291,7 @@ export const PkiSyncFns = { break; } case PkiSync.AwsCertificateManager: { - checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager); + checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager as PkiSync); const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({ appConnectionDAL: dependencies.appConnectionDAL, kmsService: dependencies.kmsService, @@ -283,8 +304,17 @@ export const PkiSyncFns = { }); break; } + case PkiSync.AwsSecretsManager: { + checkPkiSyncDestination(pkiSync, PkiSync.AwsSecretsManager as PkiSync); + const awsSecretsManagerPkiSync = awsSecretsManagerPkiSyncFactory({ + certificateDAL: dependencies.certificateDAL, + certificateSyncDAL: dependencies.certificateSyncDAL + }); + await awsSecretsManagerPkiSync.removeCertificates(pkiSync, dependencies.certificateMap); + break; + } case PkiSync.Chef: { - checkPkiSyncDestination(pkiSync, PkiSync.Chef); + checkPkiSyncDestination(pkiSync, PkiSync.Chef as PkiSync); const chefPkiSync = chefPkiSyncFactory({ certificateDAL: dependencies.certificateDAL, certificateSyncDAL: dependencies.certificateSyncDAL diff --git a/backend/src/services/pki-sync/pki-sync-maps.ts b/backend/src/services/pki-sync/pki-sync-maps.ts index e5a3abcd33..a7edcbc113 100644 --- a/backend/src/services/pki-sync/pki-sync-maps.ts +++ b/backend/src/services/pki-sync/pki-sync-maps.ts @@ -5,11 +5,13 @@ import { PkiSync } from "./pki-sync-enums"; export const PKI_SYNC_NAME_MAP: Record = { [PkiSync.AzureKeyVault]: "Azure Key Vault", [PkiSync.AwsCertificateManager]: "AWS Certificate Manager", + [PkiSync.AwsSecretsManager]: "AWS Secrets Manager", [PkiSync.Chef]: "Chef" }; export const PKI_SYNC_CONNECTION_MAP: Record = { [PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault, [PkiSync.AwsCertificateManager]: AppConnection.AWS, + [PkiSync.AwsSecretsManager]: AppConnection.AWS, [PkiSync.Chef]: AppConnection.Chef }; diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/create.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/create.mdx new file mode 100644 index 0000000000..802a6e6392 --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create AWS Secrets Manager PKI Sync" +openapi: "POST /api/v1/pki/syncs/aws-secrets-manager" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/delete.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/delete.mdx new file mode 100644 index 0000000000..c2f406cc03 --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete AWS Secrets Manager PKI Sync" +openapi: "DELETE /api/v1/pki/syncs/aws-secrets-manager/{syncId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/get-by-id.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/get-by-id.mdx new file mode 100644 index 0000000000..22fdf1ea1d --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get AWS Secrets Manager PKI Sync by ID" +openapi: "GET /api/v1/pki/syncs/aws-secrets-manager/{syncId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/list.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/list.mdx new file mode 100644 index 0000000000..f487770bbc --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List AWS Secrets Manager PKI Syncs" +openapi: "GET /api/v1/pki/syncs/aws-secrets-manager" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/remove-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/remove-certificates.mdx new file mode 100644 index 0000000000..6fdbc0aadc --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/remove-certificates.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Certificates from AWS Secrets Manager" +openapi: "POST /api/v1/pki/syncs/aws-secrets-manager/{syncId}/remove-certificates" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/sync-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/sync-certificates.mdx new file mode 100644 index 0000000000..f35bc39edd --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/sync-certificates.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Certificates to AWS Secrets Manager" +openapi: "POST /api/v1/pki/syncs/aws-secrets-manager/{syncId}/sync-certificates" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/update.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/update.mdx new file mode 100644 index 0000000000..fabbbbe12d --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update AWS Secrets Manager PKI Sync" +openapi: "PATCH /api/v1/pki/syncs/aws-secrets-manager/{syncId}" +--- \ No newline at end of file diff --git a/docs/docs.json b/docs/docs.json index 5245f15d6a..72182232e9 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -765,6 +765,7 @@ "pages": [ "documentation/platform/pki/certificate-syncs/overview", "documentation/platform/pki/certificate-syncs/aws-certificate-manager", + "documentation/platform/pki/certificate-syncs/aws-secrets-manager", "documentation/platform/pki/certificate-syncs/azure-key-vault", "documentation/platform/pki/certificate-syncs/chef" ] @@ -2675,6 +2676,18 @@ "api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates" ] }, + { + "group": "AWS Secrets Manager", + "pages": [ + "api-reference/endpoints/pki/syncs/aws-secrets-manager/list", + "api-reference/endpoints/pki/syncs/aws-secrets-manager/get-by-id", + "api-reference/endpoints/pki/syncs/aws-secrets-manager/create", + "api-reference/endpoints/pki/syncs/aws-secrets-manager/update", + "api-reference/endpoints/pki/syncs/aws-secrets-manager/delete", + "api-reference/endpoints/pki/syncs/aws-secrets-manager/sync-certificates", + "api-reference/endpoints/pki/syncs/aws-secrets-manager/remove-certificates" + ] + }, { "group": "Azure Key Vault", "pages": [ diff --git a/docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx b/docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx new file mode 100644 index 0000000000..aa931ff18c --- /dev/null +++ b/docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx @@ -0,0 +1,249 @@ +--- +title: "AWS Secrets Manager" +description: "Learn how to configure an AWS Secrets Manager Certificate Sync for Infisical PKI." +--- + +**Prerequisites:** + +- Create an [AWS Connection](/integrations/app-connections/aws) +- Ensure your network security policies allow incoming requests from Infisical to this certificate sync provider, if network restrictions apply. + + + The AWS Secrets Manager Certificate Sync requires the following permissions to be set on the AWS IAM user + for Infisical to sync certificates to AWS Secrets Manager: `secretsmanager:CreateSecret`, `secretsmanager:UpdateSecret`, + `secretsmanager:GetSecretValue`, `secretsmanager:DeleteSecret`, `secretsmanager:ListSecrets`. + +Any role with these permissions would work such as a custom policy with **SecretsManager** permissions. + + + + + Certificates synced to AWS Secrets Manager will be stored as JSON secrets, + preserving both the certificate and private key components as separate fields within the secret value. + + + + + 1. Navigate to **Project** > **Integrations** > **Certificate Syncs** and press **Add Sync**. + ![Certificate Syncs Tab](/images/platform/pki/certificate-syncs/general/create-certificate-sync.png) + + 2. Select the **AWS Secrets Manager** option. + ![Select AWS Secrets Manager](/images/platform/pki/certificate-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png) + + 3. Configure the **Destination** to where certificates should be deployed, then click **Next**. + ![Configure Destination](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-destination.png) + + - **AWS Connection**: The AWS Connection to authenticate with. + - **Region**: The AWS region where secrets will be stored. + - **KMS Key ID** (Optional): The KMS key ID to use for encrypting secrets. Leave blank to use the default AWS managed key. + + 4. Configure the **Sync Options** to specify how certificates should be synced, then click **Next**. + ![Configure Options](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-options.png) + + - **Enable Removal of Expired/Revoked Certificates**: If enabled, Infisical will remove certificates from the destination if they are no longer active in Infisical. + - **Preserve Secret on Renewal**: Only applies to certificate renewals. When a certificate is renewed in Infisical, this option controls how the renewed certificate is handled. If enabled, the renewed certificate will update the existing secret, preserving the same secret name. If disabled, the renewed certificate will be created as a new secret with a new name. + - **Update Existing Certificates**: If enabled, Infisical will update existing secrets when certificate content changes. + - **Certificate Name Schema** (Optional): Customize how secret names are generated in AWS Secrets Manager. Use `{{certificateId}}` as a placeholder for the certificate ID. + - **Auto-Sync Enabled**: If enabled, certificates will automatically be synced when changes occur. Disable to enforce manual syncing only. + + 5. Configure the **Field Mappings** to customize how certificate data is stored in AWS Secrets Manager secrets, then click **Next**. + ![Configure Field Mappings](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-field-mappings.png) + + - **Certificate Field**: The field name where the certificate will be stored in the secret value (default: `certificate`) + - **Private Key Field**: The field name where the private key will be stored in the secret value (default: `private_key`) + - **Certificate Chain Field**: The field name where the full certificate chain will be stored in the secret value (default: `certificate_chain`) + - **CA Certificate Field**: The field name where the CA certificate will be stored in the secret value (default: `ca_certificate`) + + + **AWS Secrets Manager Secret Structure**: Certificates are stored in AWS Secrets Manager as JSON secrets with the following structure (field names can be customized via field mappings): + ```json + { + "certificate": "-----BEGIN CERTIFICATE-----\n...", + "private_key": "-----BEGIN PRIVATE KEY-----\n...", + "certificate_chain": "-----BEGIN CERTIFICATE-----\n...", + "ca_certificate": "-----BEGIN CERTIFICATE-----\n..." + } + ``` + + **Example with Custom Field Mappings**: + ```json + { + "ssl_cert": "-----BEGIN CERTIFICATE-----\n...", + "ssl_key": "-----BEGIN PRIVATE KEY-----\n...", + "ssl_chain": "-----BEGIN CERTIFICATE-----\n...", + "ssl_ca": "-----BEGIN CERTIFICATE-----\n..." + } + ``` + + + 6. Configure the **Details** of your AWS Secrets Manager Certificate Sync, then click **Next**. + ![Configure Details](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Select which certificates should be synced to AWS Secrets Manager. + ![Select Certificates](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-certificates.png) + + 8. Review your AWS Secrets Manager Certificate Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-review.png) + + 9. If enabled, your AWS Secrets Manager Certificate Sync will begin syncing your certificates to the destination endpoint. + ![Sync Certificates](/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-synced.png) + + + To create an **AWS Secrets Manager Certificate Sync**, make an API request to the [Create AWS Secrets Manager Certificate Sync](/api-reference/endpoints/pki/syncs/aws-secrets-manager/create) API endpoint. + + ### Sample request + + + You can optionally specify `certificateIds` during sync creation to immediately add certificates to the sync. + If not provided, you can add certificates later using the certificate management endpoints. + + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/pki/syncs/aws-secrets-manager \ + --header 'Authorization: Bearer ' \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-aws-secrets-manager-cert-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example certificate sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "destination": "aws-secrets-manager", + "isAutoSyncEnabled": true, + "certificateIds": [ + "550e8400-e29b-41d4-a716-446655440000", + "660f1234-e29b-41d4-a716-446655440001" + ], + "syncOptions": { + "canRemoveCertificates": true, + "preserveSecretOnRenewal": true, + "canImportCertificates": false, + "certificateNameSchema": "myapp-{{certificateId}}", + "fieldMappings": { + "certificate": "ssl_cert", + "privateKey": "ssl_key", + "certificateChain": "ssl_chain", + "caCertificate": "ssl_ca" + } + }, + "destinationConfig": { + "region": "us-east-1", + "keyId": "alias/my-kms-key" + } + }' + ``` + + ### Example with Default Field Mappings + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/pki/syncs/aws-secrets-manager \ + --header 'Authorization: Bearer ' \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-aws-secrets-manager-cert-sync-default", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "AWS Secrets Manager sync with default field mappings", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "destination": "aws-secrets-manager", + "isAutoSyncEnabled": true, + "syncOptions": { + "canRemoveCertificates": true, + "preserveSecretOnRenewal": true, + "canImportCertificates": false, + "certificateNameSchema": "infisical-{{certificateId}}", + "fieldMappings": { + "certificate": "certificate", + "privateKey": "private_key", + "certificateChain": "certificate_chain", + "caCertificate": "ca_certificate" + } + }, + "destinationConfig": { + "region": "us-west-2" + } + }' + ``` + + ### Sample response + + ```json Response + { + "pkiSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-aws-secrets-manager-cert-sync", + "description": "an example certificate sync", + "destination": "aws-secrets-manager", + "isAutoSyncEnabled": true, + "destinationConfig": { + "region": "us-east-1", + "keyId": "alias/my-kms-key" + }, + "syncOptions": { + "canRemoveCertificates": true, + "preserveSecretOnRenewal": true, + "canImportCertificates": false, + "certificateNameSchema": "myapp-{{certificateId}}", + "fieldMappings": { + "certificate": "ssl_cert", + "privateKey": "ssl_key", + "certificateChain": "ssl_chain", + "caCertificate": "ssl_ca" + } + }, + "projectId": "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" + } + } + ``` + + + + +## Certificate Management + +Your AWS Secrets Manager Certificate Sync will: + +- **Automatic Deployment**: Deploy certificates in Infisical to AWS Secrets Manager as JSON secrets with customizable field names +- **Certificate Updates**: Update certificates in AWS Secrets Manager when renewals occur +- **Expiration Handling**: Optionally remove expired certificates from AWS Secrets Manager (if enabled) +- **Format Preservation**: Maintain certificate format during sync operations +- **Field Customization**: Map certificate data to custom field names that match your application requirements +- **CA Certificate Support**: Include CA certificates in secrets for complete certificate chain management +- **KMS Encryption**: Optionally use custom KMS keys for secret encryption +- **Regional Deployment**: Deploy secrets to specific AWS regions + + + AWS Secrets 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. + + +## Manual Certificate Sync + +You can manually trigger certificate synchronization to AWS Secrets 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-secrets-manager/sync-certificates) API endpoint or the manual sync option in the Infisical UI. + + + AWS Secrets Manager does not support importing certificates back into Infisical + due to the nature of AWS Secrets Manager where certificates are stored as JSON secrets + rather than managed certificate objects. + + +## Secret Naming Constraints + +AWS Secrets Manager has specific naming requirements for secrets: + +- **Allowed Characters**: Letters, numbers, hyphens (-), and underscores (_) only +- **Length**: 1-512 characters diff --git a/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-certificates.png b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-certificates.png new file mode 100644 index 0000000000..58eaae85fd Binary files /dev/null and b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-certificates.png differ diff --git a/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-destination.png b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-destination.png new file mode 100644 index 0000000000..559d7ab5cd Binary files /dev/null and b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-destination.png differ diff --git a/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-details.png b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-details.png new file mode 100644 index 0000000000..d3617bcd15 Binary files /dev/null and b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-details.png differ diff --git a/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-field-mappings.png b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-field-mappings.png new file mode 100644 index 0000000000..04b158346c Binary files /dev/null and b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-field-mappings.png differ diff --git a/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-options.png b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-options.png new file mode 100644 index 0000000000..a97ad21feb Binary files /dev/null and b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-options.png differ diff --git a/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-review.png b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-review.png new file mode 100644 index 0000000000..23733e1bfd Binary files /dev/null and b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-review.png differ diff --git a/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-synced.png b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-synced.png new file mode 100644 index 0000000000..b230bc5548 Binary files /dev/null and b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/aws-secrets-manager-synced.png differ diff --git a/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png new file mode 100644 index 0000000000..fbc4115b74 Binary files /dev/null and b/docs/images/platform/pki/certificate-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png differ diff --git a/frontend/src/components/pki-syncs/forms/AwsSecretsManagerPkiSyncFields.tsx b/frontend/src/components/pki-syncs/forms/AwsSecretsManagerPkiSyncFields.tsx new file mode 100644 index 0000000000..fe9e52efb1 --- /dev/null +++ b/frontend/src/components/pki-syncs/forms/AwsSecretsManagerPkiSyncFields.tsx @@ -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 AwsSecretsManagerPkiSyncFields = () => { + const { control, setValue } = useFormContext< + TPkiSyncForm & { destination: PkiSync.AwsSecretsManager } + >(); + + return ( + <> + { + setValue("destinationConfig.region", ""); + }} + /> + ( + + + + )} + /> + + ); +}; diff --git a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx index ab4d97f87c..068518a115 100644 --- a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx +++ b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx @@ -38,7 +38,7 @@ const getFormTabs = ( { name: "Sync Options", key: "options", fields: ["syncOptions"] as (keyof TPkiSyncForm)[] } ]; - if (destination === PkiSync.Chef) { + if (destination === PkiSync.Chef || destination === PkiSync.AwsSecretsManager) { baseTabs.push({ name: "Mappings", key: "mappings", @@ -82,13 +82,17 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa canRemoveCertificates: false, preserveArn: true, certificateNameSchema: syncOption?.defaultCertificateNameSchema, - ...(destination === PkiSync.Chef && { + ...((destination === PkiSync.Chef || destination === PkiSync.AwsSecretsManager) && { fieldMappings: { certificate: "certificate", privateKey: "private_key", certificateChain: "certificate_chain", caCertificate: "ca_certificate" } + }), + ...(destination === PkiSync.AwsSecretsManager && { + preserveSecretOnRenewal: true, + updateExistingCertificates: true }) }, ...initialData @@ -259,7 +263,7 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa }} /> - {destination === PkiSync.Chef && ( + {(destination === PkiSync.Chef || destination === PkiSync.AwsSecretsManager) && ( diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncDestinationFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncDestinationFields.tsx index 09502f29b7..56514c262a 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncDestinationFields.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncDestinationFields.tsx @@ -4,6 +4,7 @@ import { PkiSync } from "@app/hooks/api/pkiSyncs"; import { TPkiSyncForm } from "./schemas/pki-sync-schema"; import { AwsCertificateManagerPkiSyncFields } from "./AwsCertificateManagerPkiSyncFields"; +import { AwsSecretsManagerPkiSyncFields } from "./AwsSecretsManagerPkiSyncFields"; import { AzureKeyVaultPkiSyncFields } from "./AzureKeyVaultPkiSyncFields"; import { ChefPkiSyncFields } from "./ChefPkiSyncFields"; @@ -17,6 +18,8 @@ export const PkiSyncDestinationFields = () => { return ; case PkiSync.AwsCertificateManager: return ; + case PkiSync.AwsSecretsManager: + return ; case PkiSync.Chef: return ; default: diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncFieldMappingsFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncFieldMappingsFields.tsx index fd9a31b5cd..922ad8e6f1 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncFieldMappingsFields.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncFieldMappingsFields.tsx @@ -13,15 +13,15 @@ export const PkiSyncFieldMappingsFields = ({ destination }: Props) => { const { control, watch } = useFormContext(); const currentDestination = destination || watch("destination"); - // Only show field mappings for Chef - if (currentDestination !== PkiSync.Chef) { + if (currentDestination !== PkiSync.Chef && currentDestination !== PkiSync.AwsSecretsManager) { return null; } return ( <>

- Configure how certificate fields are mapped to your Chef data bag items. + Configure how certificate fields are mapped to your{" "} + {currentDestination === PkiSync.Chef ? "Chef data bag items" : "AWS secrets"}.

@@ -33,7 +33,7 @@ export const PkiSyncFieldMappingsFields = ({ destination }: Props) => { isError={Boolean(error)} errorText={error?.message} label="Certificate Field" - tooltipText="The field name used to store the certificate content in the Chef data bag item." + tooltipText={`The field name used to store the certificate content in the ${currentDestination === PkiSync.Chef ? "Chef data bag item" : "AWS secret"}.`} > @@ -48,7 +48,7 @@ export const PkiSyncFieldMappingsFields = ({ destination }: Props) => { isError={Boolean(error)} errorText={error?.message} label="Private Key Field" - tooltipText="The field name used to store the private key content in the Chef data bag item." + tooltipText={`The field name used to store the private key content in the ${currentDestination === PkiSync.Chef ? "Chef data bag item" : "AWS secret"}.`} > @@ -63,7 +63,7 @@ export const PkiSyncFieldMappingsFields = ({ destination }: Props) => { isError={Boolean(error)} errorText={error?.message} label="Certificate Chain Field" - tooltipText="The field name used to store the certificate chain content in the Chef data bag item." + tooltipText={`The field name used to store the certificate chain content in the ${currentDestination === PkiSync.Chef ? "Chef data bag item" : "AWS secret"}.`} > @@ -78,7 +78,7 @@ export const PkiSyncFieldMappingsFields = ({ destination }: Props) => { isError={Boolean(error)} errorText={error?.message} label="CA Certificate Field" - tooltipText="The field name used to store the CA certificate content in the Chef data bag item." + tooltipText={`The field name used to store the CA certificate content in the ${currentDestination === PkiSync.Chef ? "Chef data bag item" : "AWS secret"}.`} > diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx index 6c149a20af..9ba765568c 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx @@ -183,6 +183,51 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => { /> )} + {currentDestination === PkiSync.AwsSecretsManager && ( + ( + + +

+ Preserve Secret on Renewal{" "} + +

+ Only applies to certificate renewals: When a certificate + is renewed in Infisical, this option controls how the renewed certificate + is handled in AWS Secrets Manager. +

+

+ When enabled, the renewed certificate will update the existing secret, + preserving the same secret name and ARN. This allows consuming services to + continue using the same secret reference without requiring updates. +

+

+ When disabled, the renewed certificate will be created as a new secret + with a new name, and the old secret will be removed. +

+ + } + > + + +

+
+
+ )} + /> + )} + {currentDestination === PkiSync.Chef && ( { + if (!val) return true; + + const allowedOptionalPlaceholders = [ + "{{environment}}", + "{{profileId}}", + "{{commonName}}", + "{{friendlyName}}" + ]; + + const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders] + .map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) + .join("|"); + + const allowedContentRegex = new RegExp( + `^([a-zA-Z0-9_\\-]|${allowedPlaceholdersRegexPart})*$` + ); + const contentIsValid = allowedContentRegex.test(val); + + if (val.trim()) { + const certificateIdRegex = /\{\{certificateId\}\}/; + const certificateIdIsPresent = certificateIdRegex.test(val); + return contentIsValid && certificateIdIsPresent; + } + + return contentIsValid; + }, + { + message: + "Certificate name schema must include exactly one {{certificateId}} placeholder. It can also include {{environment}}, {{profileId}}, {{commonName}}, or {{friendlyName}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), hyphens (-), and underscores (_) are allowed besides the placeholders." + } + ), + fieldMappings: AwsSecretsManagerFieldMappingsSchema.optional().default({ + certificate: "certificate", + privateKey: "private_key", + certificateChain: "certificate_chain", + caCertificate: "ca_certificate" + }) +}); + +export const AwsSecretsManagerPkiSyncDestinationSchema = BasePkiSyncSchema( + AwsSecretsManagerSyncOptionsSchema +).merge( + z.object({ + destination: z.literal(PkiSync.AwsSecretsManager), + destinationConfig: z.object({ + region: z.string().min(1, "AWS region is required") + }) + }) +); + +export const UpdateAwsSecretsManagerPkiSyncDestinationSchema = + AwsSecretsManagerPkiSyncDestinationSchema.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.AwsSecretsManager), + 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") + }) + }) + ); diff --git a/frontend/src/components/pki-syncs/forms/schemas/pki-sync-schema.ts b/frontend/src/components/pki-syncs/forms/schemas/pki-sync-schema.ts index 4d3797e66a..559fcdebc9 100644 --- a/frontend/src/components/pki-syncs/forms/schemas/pki-sync-schema.ts +++ b/frontend/src/components/pki-syncs/forms/schemas/pki-sync-schema.ts @@ -4,6 +4,10 @@ import { AwsCertificateManagerPkiSyncDestinationSchema, UpdateAwsCertificateManagerPkiSyncDestinationSchema } from "./aws-certificate-manager-pki-sync-destination-schema"; +import { + AwsSecretsManagerPkiSyncDestinationSchema, + UpdateAwsSecretsManagerPkiSyncDestinationSchema +} from "./aws-secrets-manager-pki-sync-destination-schema"; import { AzureKeyVaultPkiSyncDestinationSchema, UpdateAzureKeyVaultPkiSyncDestinationSchema @@ -16,12 +20,14 @@ import { const PkiSyncUnionSchema = z.discriminatedUnion("destination", [ AzureKeyVaultPkiSyncDestinationSchema, AwsCertificateManagerPkiSyncDestinationSchema, + AwsSecretsManagerPkiSyncDestinationSchema, ChefPkiSyncDestinationSchema ]); const UpdatePkiSyncUnionSchema = z.discriminatedUnion("destination", [ UpdateAzureKeyVaultPkiSyncDestinationSchema, UpdateAwsCertificateManagerPkiSyncDestinationSchema, + UpdateAwsSecretsManagerPkiSyncDestinationSchema, UpdateChefPkiSyncDestinationSchema ]); diff --git a/frontend/src/helpers/pkiSyncs.ts b/frontend/src/helpers/pkiSyncs.ts index 79b90c6b2e..471a4da059 100644 --- a/frontend/src/helpers/pkiSyncs.ts +++ b/frontend/src/helpers/pkiSyncs.ts @@ -16,6 +16,10 @@ export const PKI_SYNC_MAP: Record< name: "AWS Certificate Manager", image: "Amazon Web Services.png" }, + [PkiSync.AwsSecretsManager]: { + name: "AWS Secrets Manager", + image: "Amazon Web Services.png" + }, [PkiSync.Chef]: { name: "Chef", image: "Chef.png" @@ -25,5 +29,6 @@ export const PKI_SYNC_MAP: Record< export const PKI_SYNC_CONNECTION_MAP: Record = { [PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault, [PkiSync.AwsCertificateManager]: AppConnection.AWS, + [PkiSync.AwsSecretsManager]: AppConnection.AWS, [PkiSync.Chef]: AppConnection.Chef }; diff --git a/frontend/src/hooks/api/pkiSyncs/enums.ts b/frontend/src/hooks/api/pkiSyncs/enums.ts index 64de59386a..5eabce5ad5 100644 --- a/frontend/src/hooks/api/pkiSyncs/enums.ts +++ b/frontend/src/hooks/api/pkiSyncs/enums.ts @@ -1,6 +1,7 @@ export enum PkiSync { AzureKeyVault = "azure-key-vault", AwsCertificateManager = "aws-certificate-manager", + AwsSecretsManager = "aws-secrets-manager", Chef = "chef" } diff --git a/frontend/src/hooks/api/pkiSyncs/types/aws-secrets-manager-sync.ts b/frontend/src/hooks/api/pkiSyncs/types/aws-secrets-manager-sync.ts new file mode 100644 index 0000000000..1558678dcf --- /dev/null +++ b/frontend/src/hooks/api/pkiSyncs/types/aws-secrets-manager-sync.ts @@ -0,0 +1,29 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { PkiSync } from "../enums"; +import { TRootPkiSync } from "./common"; + +export type TAwsSecretsManagerFieldMappings = { + certificate: string; + privateKey: string; + certificateChain: string; + caCertificate: string; +}; + +export type TAwsSecretsManagerPkiSync = TRootPkiSync & { + destination: PkiSync.AwsSecretsManager; + destinationConfig: { + region: string; + keyId?: string; + }; + connection: { + app: AppConnection.AWS; + name: string; + id: string; + }; + syncOptions: TRootPkiSync["syncOptions"] & { + fieldMappings?: TAwsSecretsManagerFieldMappings; + preserveSecretOnRenewal?: boolean; + updateExistingCertificates?: boolean; + }; +}; diff --git a/frontend/src/hooks/api/pkiSyncs/types/index.ts b/frontend/src/hooks/api/pkiSyncs/types/index.ts index ca8417f37b..53b910bf79 100644 --- a/frontend/src/hooks/api/pkiSyncs/types/index.ts +++ b/frontend/src/hooks/api/pkiSyncs/types/index.ts @@ -1,6 +1,7 @@ import { PkiSync } from "@app/hooks/api/pkiSyncs"; import { TAwsCertificateManagerPkiSync } from "./aws-certificate-manager-sync"; +import { TAwsSecretsManagerPkiSync } from "./aws-secrets-manager-sync"; import { TAzureKeyVaultPkiSync } from "./azure-key-vault-sync"; import { TChefPkiSync } from "./chef-sync"; @@ -17,7 +18,11 @@ export type TPkiSyncOption = { minCertificateNameLength?: number; }; -export type TPkiSync = TAzureKeyVaultPkiSync | TAwsCertificateManagerPkiSync | TChefPkiSync; +export type TPkiSync = + | TAzureKeyVaultPkiSync + | TAwsCertificateManagerPkiSync + | TAwsSecretsManagerPkiSync + | TChefPkiSync; export type TListPkiSyncs = { pkiSyncs: TPkiSync[] }; @@ -36,6 +41,13 @@ type TCreatePkiSyncDTOBase = { enableVersioning?: boolean; preserveItemOnRenewal?: boolean; updateExistingCertificates?: boolean; + preserveSecretOnRenewal?: boolean; + fieldMappings?: { + certificate: string; + privateKey: string; + certificateChain: string; + caCertificate: string; + }; }; isAutoSyncEnabled: boolean; subscriberId?: string | null; @@ -82,6 +94,7 @@ export type TTriggerPkiSyncRemoveCertificatesDTO = { }; export * from "./aws-certificate-manager-sync"; +export * from "./aws-secrets-manager-sync"; export * from "./azure-key-vault-sync"; export * from "./chef-sync"; export * from "./common"; diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx index d3fecd652b..c59d2df6dd 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection.tsx @@ -13,6 +13,7 @@ import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs"; import { AwsCertificateManagerPkiSyncDestinationSection, + AwsSecretsManagerPkiSyncDestinationSection, AzureKeyVaultPkiSyncDestinationSection, ChefPkiSyncDestinationSection } from "./PkiSyncDestinationSection/index"; @@ -39,6 +40,9 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props) case PkiSync.AwsCertificateManager: DestinationComponents = ; break; + case PkiSync.AwsSecretsManager: + DestinationComponents = ; + break; case PkiSync.AzureKeyVault: DestinationComponents = ; break; diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection/AwsSecretsManagerPkiSyncDestinationSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection/AwsSecretsManagerPkiSyncDestinationSection.tsx new file mode 100644 index 0000000000..ba97b73cf7 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection/AwsSecretsManagerPkiSyncDestinationSection.tsx @@ -0,0 +1,28 @@ +import { TAwsSecretsManagerPkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs"; + +const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => ( +
+

{label}

+
{children}
+
+); + +type Props = { + pkiSync: TPkiSync; +}; + +export const AwsSecretsManagerPkiSyncDestinationSection = ({ pkiSync }: Props) => { + const awsSecretsManagerPkiSync = pkiSync as TAwsSecretsManagerPkiSync; + const { destinationConfig } = awsSecretsManagerPkiSync; + + return ( + <> + + {destinationConfig.region || "us-east-1"} + + {destinationConfig.keyId && ( + {destinationConfig.keyId} + )} + + ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection/index.ts b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection/index.ts index 191bdaaca4..c963f7284f 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection/index.ts +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncDestinationSection/index.ts @@ -1,3 +1,4 @@ export { AwsCertificateManagerPkiSyncDestinationSection } from "./AwsCertificateManagerPkiSyncDestinationSection"; +export { AwsSecretsManagerPkiSyncDestinationSection } from "./AwsSecretsManagerPkiSyncDestinationSection"; export { AzureKeyVaultPkiSyncDestinationSection } from "./AzureKeyVaultPkiSyncDestinationSection"; export { ChefPkiSyncDestinationSection } from "./ChefPkiSyncDestinationSection"; diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncFieldMappingsSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncFieldMappingsSection.tsx index f06ca94687..66f0ea0761 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncFieldMappingsSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncFieldMappingsSection.tsx @@ -30,8 +30,7 @@ type Props = { }; export const PkiSyncFieldMappingsSection = ({ pkiSync, onEditMappings }: Props) => { - // Only show for Chef PKI syncs - if (pkiSync.destination !== PkiSync.Chef) { + if (pkiSync.destination !== PkiSync.Chef && pkiSync.destination !== PkiSync.AwsSecretsManager) { return null; }