diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 3e730048b..d890fcc35 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -590,7 +590,8 @@ export const INTEGRATION = { initialSyncBehavoir: "Type of syncing behavoir with the integration.", shouldAutoRedeploy: "Used by Render to trigger auto deploy.", secretGCPLabel: "The label for GCP secrets.", - secretAWSTag: "The tags for AWS secrets." + secretAWSTag: "The tags for AWS secrets.", + kmsKeyId: "The ID of the encryption key from AWS KMS." } }, UPDATE: { diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index 604ac0bc2..d9db7404e 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -511,6 +511,39 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) } }); + server.route({ + method: "GET", + url: "/:integrationAuthId/aws-secrets-manager/kms-keys", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + querystring: z.object({ + region: z.string().trim() + }), + response: { + 200: z.object({ + kmsKeys: z.object({ id: z.string(), alias: z.string() }).array() + }) + } + }, + handler: async (req) => { + const kmsKeys = await server.services.integrationAuth.getAwsKmsKeys({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId, + region: req.query.region + }); + return { kmsKeys }; + } + }); + server.route({ method: "GET", url: "/:integrationAuthId/qovery/projects", diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index 29ccce0a8..54e876783 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -65,7 +65,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { }) ) .optional() - .describe(INTEGRATION.CREATE.metadata.secretAWSTag) + .describe(INTEGRATION.CREATE.metadata.secretAWSTag), + kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId) }) .optional() }), diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index c8551df7d..3d42943a6 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; import { Octokit } from "@octokit/rest"; +import AWS from "aws-sdk"; import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -23,6 +24,7 @@ import { TGetIntegrationAuthTeamCityBuildConfigDTO, THerokuPipelineCoupling, TIntegrationAuthAppsDTO, + TIntegrationAuthAwsKmsKeyDTO, TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthChecklyGroupsDTO, TIntegrationAuthGithubEnvsDTO, @@ -534,6 +536,52 @@ export const integrationAuthServiceFactory = ({ return data.results.map(({ name, id: orgId }) => ({ name, orgId })); }; + const getAwsKmsKeys = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id, + region + }: TIntegrationAuthAwsKmsKeyDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + integrationAuth.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + const botKey = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessId, accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); + + AWS.config.update({ + region, + credentials: { + accessKeyId: String(accessId), + secretAccessKey: accessToken + } + }); + const kms = new AWS.KMS(); + + const aliases = await kms.listAliases({}).promise(); + const keys = await kms.listKeys({}).promise(); + const response = keys + .Keys!.map((key) => { + const keyAlias = aliases.Aliases!.find((alias) => key.KeyId === alias.TargetKeyId); + if (!keyAlias?.AliasName?.includes("alias/aws/") || keyAlias?.AliasName?.includes("alias/aws/secretsmanager")) { + return { id: String(key.KeyId), alias: String(keyAlias?.AliasName || key.KeyId) }; + } + return { id: "null", alias: "null" }; + }) + .filter((elem) => elem.id !== "null"); + + return response; + }; + const getQoveryProjects = async ({ actorId, actor, @@ -1133,6 +1181,7 @@ export const integrationAuthServiceFactory = ({ getIntegrationApps, getVercelBranches, getApps, + getAwsKmsKeys, getGithubOrgs, getGithubEnvs, getChecklyGroups, diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index 0c8671fbf..0a816035c 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -63,6 +63,11 @@ export type TIntegrationAuthQoveryProjectDTO = { orgId: string; } & Omit; +export type TIntegrationAuthAwsKmsKeyDTO = { + id: string; + region: string; +} & Omit; + export type TIntegrationAuthQoveryEnvironmentsDTO = { id: string; } & TProjectPermission; diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 267c50516..2812e91af 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -582,6 +582,7 @@ const syncSecretsAWSSecretManager = async ({ new CreateSecretCommand({ Name: integration.app as string, SecretString: JSON.stringify(secKeyVal), + KmsKeyId: metadata.kmsKeyId ? metadata.kmsKeyId : null, Tags: metadata.secretAWSTag ? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value })) : [] diff --git a/backend/src/services/integration/integration-types.ts b/backend/src/services/integration/integration-types.ts index 053ea4463..56ea46350 100644 --- a/backend/src/services/integration/integration-types.ts +++ b/backend/src/services/integration/integration-types.ts @@ -26,6 +26,7 @@ export type TCreateIntegrationDTO = { key: string; value: string; }[]; + kmsKeyId?: string; }; } & Omit; diff --git a/docs/images/integrations/aws/integrations-aws-secret-manager-create.png b/docs/images/integrations/aws/integrations-aws-secret-manager-create.png index 619ec0a8e..21f2213ef 100644 Binary files a/docs/images/integrations/aws/integrations-aws-secret-manager-create.png and b/docs/images/integrations/aws/integrations-aws-secret-manager-create.png differ diff --git a/docs/images/integrations/aws/integrations-aws-secret-manager-options.png b/docs/images/integrations/aws/integrations-aws-secret-manager-options.png new file mode 100644 index 000000000..f8492cdfa Binary files /dev/null and b/docs/images/integrations/aws/integrations-aws-secret-manager-options.png differ diff --git a/docs/integrations/cloud/aws-parameter-store.mdx b/docs/integrations/cloud/aws-parameter-store.mdx index 547387996..c872e39f0 100644 --- a/docs/integrations/cloud/aws-parameter-store.mdx +++ b/docs/integrations/cloud/aws-parameter-store.mdx @@ -30,7 +30,7 @@ Prerequisites: "ssm:DeleteParameter", "ssm:GetParametersByPath", "ssm:DeleteParameters", - "ssm:AddTagsToResource" + "ssm:AddTagsToResource" // if you need to add tags to secrets ], "Resource": "*" } diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx index a0187644d..3ecf7771e 100644 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ b/docs/integrations/cloud/aws-secret-manager.mdx @@ -29,7 +29,8 @@ Prerequisites: "secretsmanager:GetSecretValue", "secretsmanager:CreateSecret", "secretsmanager:UpdateSecret", - "secretsmanager:TagResource" + "secretsmanager:TagResource", // if you need to add tags to secrets + "kms:ListKeys" // if you need to specify the KMS key ], "Resource": "*" } @@ -51,13 +52,6 @@ Prerequisites: Press on the AWS Secrets Manager tile and input your AWS access key ID and secret access key from the previous step. ![integration auth](../../images/integrations/aws/integrations-aws-secret-manager-auth.png) - - - If this is your project's first cloud integration, then you'll have to grant - Infisical access to your project's environment variables. Although this step - breaks E2EE, it's necessary for Infisical to sync the environment variables to - the cloud platform. - Select how you want to integration to work by specifying a number of parameters: @@ -75,10 +69,20 @@ Prerequisites: The secret name/path in AWS into which you want to sync the secrets from Infisical. - Then, press `Create Integration` to start syncing secrets to AWS Secrets Manager. - ![integration create](../../images/integrations/aws/integrations-aws-secret-manager-create.png) + Optionally, you can add tags or specify the encryption key of all the secrets created via this integration: + + + The Key/Value of a tag that will be added to secrets in AWS. Please note that it is possible to add multiple tags via API. + + + The alias/ID of the AWS KMS key used for encryption. Please note that keys should be enabled in order to work. + + ![integration options](../../images/integrations/aws/integrations-aws-secret-manager-options.png) + + Then, press `Create Integration` to start syncing secrets to AWS Secrets Manager. + Infisical currently syncs environment variables to AWS Secrets Manager as key-value pairs under one secret. We're actively exploring ways to help users diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 073fde34b..e66dd5700 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -10,6 +10,7 @@ import { Environment, HerokuPipelineCoupling, IntegrationAuth, + KmsKey, NorthflankSecretGroup, Org, Project, @@ -43,6 +44,14 @@ const integrationAuthKeys = { [{ integrationAuthId }, "integrationAuthGithubOrgs"] as const, getIntegrationAuthGithubEnvs: (integrationAuthId: string, repoName: string, repoOwner: string) => [{ integrationAuthId, repoName, repoOwner }, "integrationAuthGithubOrgs"] as const, + getIntegrationAuthAwsKmsKeys: ({ + integrationAuthId, + region + }: { + integrationAuthId: string, + region: string + }) => + [{ integrationAuthId, region }, "integrationAuthAwsKmsKeyIds"] as const, getIntegrationAuthQoveryOrgs: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const, getIntegrationAuthQoveryProjects: ({ @@ -217,6 +226,27 @@ const fetchIntegrationAuthQoveryOrgs = async (integrationAuthId: string) => { return orgs; }; +const fetchIntegrationAuthAwsKmsKeys = async ({ + integrationAuthId, + region +}: { + integrationAuthId: string; + region: string; +}) => { + const { + data: { kmsKeys } + } = await apiRequest.get<{ kmsKeys: KmsKey[] }>( + `/api/v1/integration-auth/${integrationAuthId}/aws-secrets-manager/kms-keys`, + { + params: { + region + } + } + ); + + return kmsKeys; +}; + const fetchIntegrationAuthQoveryProjects = async ({ integrationAuthId, orgId @@ -544,6 +574,27 @@ export const useGetIntegrationAuthQoveryOrgs = (integrationAuthId: string) => { }); }; +export const useGetIntegrationAuthAwsKmsKeys = ({ + integrationAuthId, + region +}: { + integrationAuthId: string; + region: string; +}) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthAwsKmsKeys({ + integrationAuthId, + region + }), + queryFn: () => + fetchIntegrationAuthAwsKmsKeys({ + integrationAuthId, + region + }), + enabled: true + }); +}; + export const useGetIntegrationAuthQoveryProjects = ({ integrationAuthId, orgId diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index cfd25df00..b0e1dd9f5 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -58,6 +58,11 @@ export type Project = { projectId: string; }; +export type KmsKey = { + id: string; + alias: string; +}; + export type Service = { name: string; serviceId: string; diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 4f2088053..b855dbf73 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -67,6 +67,7 @@ export const useCreateIntegration = () => { key: string; value: string; }[]; + kmsKeyId?: string; }; }) => { const { diff --git a/frontend/src/pages/integrations/aws-secret-manager/create.tsx b/frontend/src/pages/integrations/aws-secret-manager/create.tsx index de8e3628c..b48296259 100644 --- a/frontend/src/pages/integrations/aws-secret-manager/create.tsx +++ b/frontend/src/pages/integrations/aws-secret-manager/create.tsx @@ -14,6 +14,7 @@ import { motion } from "framer-motion"; import queryString from "query-string"; import { useCreateIntegration } from "@app/hooks/api"; +import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries"; import { Button, @@ -87,6 +88,7 @@ export default function AWSSecretManagerCreateIntegrationPage() { const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState(""); const [tagKey, setTagKey] = useState(""); const [tagValue, setTagValue] = useState(""); + const [kmsKeyId, setKmsKeyId] = useState(""); // const [path, setPath] = useState(''); // const [pathErrorText, setPathErrorText] = useState(''); @@ -94,6 +96,19 @@ export default function AWSSecretManagerCreateIntegrationPage() { const [isLoading, setIsLoading] = useState(false); const [shouldTag, setShouldTag] = useState(false); + + const { data: integrationAuthAwsKmsKeys, isLoading: isIntegrationAuthAwsKmsKeysLoading } = + useGetIntegrationAuthAwsKmsKeys({ + integrationAuthId: String(integrationAuthId), + region: selectedAWSRegion + }); + + useEffect(() => { + if (integrationAuthAwsKmsKeys) { + setKmsKeyId(String(integrationAuthAwsKmsKeys?.filter(key => key.alias === "alias/aws/secretsmanager")[0]?.id)) + } + }, [integrationAuthAwsKmsKeys]) + useEffect(() => { if (workspace) { setSelectedSourceEnvironment(workspace.environments[0].slug); @@ -132,7 +147,11 @@ export default function AWSSecretManagerCreateIntegrationPage() { value: tagValue }] } - : {}) + : {}), + ...((kmsKeyId && integrationAuthAwsKmsKeys?.filter(key => key.id === kmsKeyId)[0]?.alias !== "alias/aws/secretsmanager") ? + { + kmsKeyId + }: {}) } }); @@ -145,7 +164,7 @@ export default function AWSSecretManagerCreateIntegrationPage() { } }; - return integrationAuth && workspace && selectedSourceEnvironment ? ( + return (integrationAuth && workspace && selectedSourceEnvironment && !isIntegrationAuthAwsKmsKeysLoading) ? (
Set Up AWS Secrets Manager Integration @@ -285,6 +304,31 @@ export default function AWSSecretManagerCreateIntegrationPage() {
)} + + + @@ -317,7 +361,7 @@ export default function AWSSecretManagerCreateIntegrationPage() { Set Up AWS Secrets Manager Integration - {isintegrationAuthLoading ? ( + {(isintegrationAuthLoading || isIntegrationAuthAwsKmsKeysLoading) ? (