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

View File

@@ -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>
)}
/>
</>
);
};

View File

@@ -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>

View File

@@ -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:

View File

@@ -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>

View File

@@ -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}

View File

@@ -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")
})
})
);

View File

@@ -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
]);