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,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
]);
+5
View File
@@ -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
View File
@@ -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;
};
};
+14 -1
View File
@@ -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;
}