Merge pull request #4910 from Infisical/feat/PKI-42
Add AWS secret manager PKI Sync
@@ -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
|
||||
docs/documentation/platform/pki/enrollment-methods/api.mdx:private-key:139
|
||||
docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx:private-key:62
|
||||
@@ -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,561 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import {
|
||||
CreateSecretCommand,
|
||||
DeleteSecretCommand,
|
||||
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 };
|
||||
};
|
||||
|
||||
return {
|
||||
syncCertificates,
|
||||
removeCertificates
|
||||
};
|
||||
};
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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,9 @@ export const PkiSyncFns = {
|
||||
"AWS Certificate Manager does not support importing certificates into Infisical (private keys cannot be extracted)"
|
||||
);
|
||||
}
|
||||
case PkiSync.AwsSecretsManager: {
|
||||
throw new Error("AWS Secrets Manager does not support importing certificates into Infisical");
|
||||
}
|
||||
case PkiSync.Chef: {
|
||||
throw new Error(
|
||||
"Chef does not support importing certificates into Infisical (private keys cannot be extracted securely)"
|
||||
@@ -211,7 +219,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 +229,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 +238,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 +272,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 +286,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 +299,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
|
||||
};
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create AWS Secrets Manager PKI Sync"
|
||||
openapi: "POST /api/v1/pki/syncs/aws-secrets-manager"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete AWS Secrets Manager PKI Sync"
|
||||
openapi: "DELETE /api/v1/pki/syncs/aws-secrets-manager/{syncId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get AWS Secrets Manager PKI Sync by ID"
|
||||
openapi: "GET /api/v1/pki/syncs/aws-secrets-manager/{syncId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List AWS Secrets Manager PKI Syncs"
|
||||
openapi: "GET /api/v1/pki/syncs/aws-secrets-manager"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Remove Certificates from AWS Secrets Manager"
|
||||
openapi: "POST /api/v1/pki/syncs/aws-secrets-manager/{syncId}/remove-certificates"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sync Certificates to AWS Secrets Manager"
|
||||
openapi: "POST /api/v1/pki/syncs/aws-secrets-manager/{syncId}/sync-certificates"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update AWS Secrets Manager PKI Sync"
|
||||
openapi: "PATCH /api/v1/pki/syncs/aws-secrets-manager/{syncId}"
|
||||
---
|
||||
@@ -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": [
|
||||
|
||||
@@ -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.
|
||||
|
||||
<Note>
|
||||
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.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
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.
|
||||
</Note>
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
1. Navigate to **Project** > **Integrations** > **Certificate Syncs** and press **Add Sync**.
|
||||

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

|
||||
|
||||
3. Configure the **Destination** to where certificates should be deployed, then click **Next**.
|
||||

|
||||
|
||||
- **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**.
|
||||

|
||||
|
||||
- **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**.
|
||||

|
||||
|
||||
- **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`)
|
||||
|
||||
<Tip>
|
||||
**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..."
|
||||
}
|
||||
```
|
||||
</Tip>
|
||||
|
||||
6. Configure the **Details** of your AWS Secrets Manager Certificate Sync, then click **Next**.
|
||||

|
||||
|
||||
- **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.
|
||||

|
||||
|
||||
8. Review your AWS Secrets Manager Certificate Sync configuration, then click **Create Sync**.
|
||||

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

|
||||
</Tab>
|
||||
<Tab title="API">
|
||||
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
|
||||
|
||||
<Note>
|
||||
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.
|
||||
</Note>
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://app.infisical.com/api/v1/pki/syncs/aws-secrets-manager \
|
||||
--header 'Authorization: Bearer <access-token>' \
|
||||
--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 <access-token>' \
|
||||
--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"
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## 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
|
||||
|
||||
<Note>
|
||||
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.
|
||||
</Note>
|
||||
|
||||
## 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.
|
||||
|
||||
<Note>
|
||||
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.
|
||||
</Note>
|
||||
|
||||
## Secret Naming Constraints
|
||||
|
||||
AWS Secrets Manager has specific naming requirements for secrets:
|
||||
|
||||
- **Allowed Characters**: Letters, numbers, hyphens (-), and underscores (_) only
|
||||
- **Length**: 1-512 characters
|
||||
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 375 KiB |
|
After Width: | Height: | Size: 331 KiB |
|
After Width: | Height: | Size: 272 KiB |
|
After Width: | Height: | Size: 271 KiB |
@@ -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 (
|
||||
<>
|
||||
<PkiSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.region", "");
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.region"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="AWS Region"
|
||||
tooltipText="Select the AWS region where your secrets will be stored in AWS Secrets Manager."
|
||||
>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
placeholder="Select an AWS region"
|
||||
>
|
||||
{AWS_REGIONS.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
}}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
{destination === PkiSync.Chef && (
|
||||
{(destination === PkiSync.Chef || destination === PkiSync.AwsSecretsManager) && (
|
||||
<Tab.Panel className="max-h-full overflow-y-auto">
|
||||
<PkiSyncFieldMappingsFields destination={destination} />
|
||||
</Tab.Panel>
|
||||
|
||||
@@ -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 <AzureKeyVaultPkiSyncFields />;
|
||||
case PkiSync.AwsCertificateManager:
|
||||
return <AwsCertificateManagerPkiSyncFields />;
|
||||
case PkiSync.AwsSecretsManager:
|
||||
return <AwsSecretsManagerPkiSyncFields />;
|
||||
case PkiSync.Chef:
|
||||
return <ChefPkiSyncFields />;
|
||||
default:
|
||||
|
||||
@@ -13,15 +13,15 @@ export const PkiSyncFieldMappingsFields = ({ destination }: Props) => {
|
||||
const { control, watch } = useFormContext<TPkiSyncForm>();
|
||||
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 (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
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"}.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -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"}.`}
|
||||
>
|
||||
<Input {...field} placeholder="certificate" />
|
||||
</FormControl>
|
||||
@@ -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"}.`}
|
||||
>
|
||||
<Input {...field} placeholder="private_key" />
|
||||
</FormControl>
|
||||
@@ -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"}.`}
|
||||
>
|
||||
<Input {...field} placeholder="certificate_chain" />
|
||||
</FormControl>
|
||||
@@ -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"}.`}
|
||||
>
|
||||
<Input {...field} placeholder="ca_certificate" />
|
||||
</FormControl>
|
||||
|
||||
@@ -183,6 +183,51 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentDestination === PkiSync.AwsSecretsManager && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.preserveSecretOnRenewal"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="preserve-secret-on-renewal"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p>
|
||||
Preserve Secret on Renewal{" "}
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
<strong>Only applies to certificate renewals:</strong> When a certificate
|
||||
is renewed in Infisical, this option controls how the renewed certificate
|
||||
is handled in AWS Secrets Manager.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
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.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
When disabled, the renewed certificate will be created as a new secret
|
||||
with a new name, and the old secret will be removed.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentDestination === PkiSync.Chef && (
|
||||
<Controller
|
||||
control={control}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
|
||||
|
||||
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 AwsSecretsManagerSyncOptionsSchema = 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(
|
||||
(val) => {
|
||||
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")
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -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
|
||||
]);
|
||||
|
||||
|
||||
@@ -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, AppConnection> = {
|
||||
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
||||
[PkiSync.AwsCertificateManager]: AppConnection.AWS,
|
||||
[PkiSync.AwsSecretsManager]: AppConnection.AWS,
|
||||
[PkiSync.Chef]: AppConnection.Chef
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum PkiSync {
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AwsCertificateManager = "aws-certificate-manager",
|
||||
AwsSecretsManager = "aws-secrets-manager",
|
||||
Chef = "chef"
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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 = <AwsCertificateManagerPkiSyncDestinationSection pkiSync={pkiSync} />;
|
||||
break;
|
||||
case PkiSync.AwsSecretsManager:
|
||||
DestinationComponents = <AwsSecretsManagerPkiSyncDestinationSection pkiSync={pkiSync} />;
|
||||
break;
|
||||
case PkiSync.AzureKeyVault:
|
||||
DestinationComponents = <AzureKeyVaultPkiSyncDestinationSection pkiSync={pkiSync} />;
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { TAwsSecretsManagerPkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
};
|
||||
|
||||
export const AwsSecretsManagerPkiSyncDestinationSection = ({ pkiSync }: Props) => {
|
||||
const awsSecretsManagerPkiSync = pkiSync as TAwsSecretsManagerPkiSync;
|
||||
const { destinationConfig } = awsSecretsManagerPkiSync;
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="AWS Region">
|
||||
{destinationConfig.region || "us-east-1"}
|
||||
</GenericFieldLabel>
|
||||
{destinationConfig.keyId && (
|
||||
<GenericFieldLabel label="KMS Key">{destinationConfig.keyId}</GenericFieldLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export { AwsCertificateManagerPkiSyncDestinationSection } from "./AwsCertificateManagerPkiSyncDestinationSection";
|
||||
export { AwsSecretsManagerPkiSyncDestinationSection } from "./AwsSecretsManagerPkiSyncDestinationSection";
|
||||
export { AzureKeyVaultPkiSyncDestinationSection } from "./AzureKeyVaultPkiSyncDestinationSection";
|
||||
export { ChefPkiSyncDestinationSection } from "./ChefPkiSyncDestinationSection";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||