Add AWS secret manager PKI Sync

This commit is contained in:
Carlos Monastyrski
2025-11-19 20:46:39 -03:00
parent 350fcc86c3
commit 1ddabab16d
43 changed files with 1525 additions and 20 deletions
@@ -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
}
});
@@ -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<PkiSync, (server: FastifyZodProvider) => Promise<void>> = {
[PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter,
[PkiSync.AwsCertificateManager]: registerAwsCertificateManagerPkiSyncRouter,
[PkiSync.AwsSecretsManager]: registerAwsSecretsManagerPkiSyncRouter,
[PkiSync.Chef]: registerChefPkiSyncRouter
};
@@ -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;
@@ -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<TCertificateDALFactory, "findById">;
certificateSyncDAL: Pick<
TCertificateSyncDALFactory,
| "removeCertificates"
| "addCertificates"
| "findByPkiSyncAndCertificate"
| "updateById"
| "findByPkiSyncId"
| "updateSyncStatus"
>;
};
export const awsSecretsManagerPkiSyncFactory = ({
certificateDAL,
certificateSyncDAL
}: TAwsSecretsManagerPkiSyncFactoryDeps) => {
const $getSecretsManagerSecrets = async (
pkiSync: TAwsSecretsManagerPkiSyncWithCredentials,
syncId = "unknown"
): Promise<Record<string, string>> => {
const client = await getSecretsManagerClient(pkiSync);
const secrets: Record<string, string> = {};
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<SyncCertificatesResult> => {
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<string, TCertificateSyncs>();
const syncRecordsByExternalId = new Map<string, TCertificateSyncs>();
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<string>();
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<string, unknown>;
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<TCertificateMap> => {
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<typeof awsSecretsManagerPkiSyncFactory>;
@@ -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)
});
@@ -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<typeof AwsSecretsManagerPkiSyncConfigSchema>;
export type TAwsSecretsManagerFieldMappings = z.infer<typeof AwsSecretsManagerFieldMappingsSchema>;
export type TAwsSecretsManagerPkiSync = z.infer<typeof AwsSecretsManagerPkiSyncSchema>;
export type TAwsSecretsManagerPkiSyncInput = z.infer<typeof CreateAwsSecretsManagerPkiSyncSchema>;
export type TAwsSecretsManagerPkiSyncUpdate = z.infer<typeof UpdateAwsSecretsManagerPkiSyncSchema>;
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;
}
@@ -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";
@@ -1,6 +1,7 @@
export enum PkiSync {
AzureKeyVault = "azure-key-vault",
AwsCertificateManager = "aws-certificate-manager",
AwsSecretsManager = "aws-secrets-manager",
Chef = "chef"
}
+36 -6
View File
@@ -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<TAppConnectionDALFactory, "findById" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
certificateDAL: TCertificateDALFactory;
certificateSyncDAL: TCertificateSyncDALFactory;
}
): Promise<TCertificateMap> => {
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<void> => {
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
@@ -5,11 +5,13 @@ import { PkiSync } from "./pki-sync-enums";
export const PKI_SYNC_NAME_MAP: Record<PkiSync, string> = {
[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, AppConnection> = {
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault,
[PkiSync.AwsCertificateManager]: AppConnection.AWS,
[PkiSync.AwsSecretsManager]: AppConnection.AWS,
[PkiSync.Chef]: AppConnection.Chef
};