Merge pull request #1539 from akhilmhdh/feat/secret-rotation-aws-iam
Secret rotation for AWS IAM User
846
backend/package-lock.json
generated
@ -70,6 +70,7 @@
|
||||
"vitest": "^1.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-iam": "^3.525.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@fastify/cookie": "^9.3.1",
|
||||
|
@ -1,3 +1,10 @@
|
||||
import {
|
||||
CreateAccessKeyCommand,
|
||||
DeleteAccessKeyCommand,
|
||||
GetAccessKeyLastUsedCommand,
|
||||
IAMClient
|
||||
} from "@aws-sdk/client-iam";
|
||||
|
||||
import { SecretKeyEncoding, SecretType } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import {
|
||||
@ -18,7 +25,12 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
import { TSecretRotationDALFactory } from "../secret-rotation-dal";
|
||||
import { rotationTemplates } from "../templates";
|
||||
import { TDbProviderClients, TProviderFunctionTypes, TSecretRotationProviderTemplate } from "../templates/types";
|
||||
import {
|
||||
TAwsProviderSystems,
|
||||
TDbProviderClients,
|
||||
TProviderFunctionTypes,
|
||||
TSecretRotationProviderTemplate
|
||||
} from "../templates/types";
|
||||
import {
|
||||
getDbSetQuery,
|
||||
secretRotationDbFn,
|
||||
@ -127,7 +139,10 @@ export const secretRotationQueueFactory = ({
|
||||
internal: {}
|
||||
};
|
||||
|
||||
// when its a database we keep cycling the variables accordingly
|
||||
/* Rotation Function For Database
|
||||
* A database like sql cannot have multiple password for a user
|
||||
* thus we ask users to create two users with required permission and then we keep cycling between these two db users
|
||||
*/
|
||||
if (provider.template.type === TProviderFunctionTypes.DB) {
|
||||
const lastCred = variables.creds.at(-1);
|
||||
if (lastCred && variables.creds.length === 1) {
|
||||
@ -170,6 +185,65 @@ export const secretRotationQueueFactory = ({
|
||||
if (variables.creds.length === 2) variables.creds.pop();
|
||||
}
|
||||
|
||||
/*
|
||||
* Rotation Function For AWS Services
|
||||
* Due to complexity in AWS Authorization hashing signature process we keep it as seperate entity instead of http template mode
|
||||
* We first delete old key before creating a new one because aws iam has a quota limit of 2 keys
|
||||
* */
|
||||
if (provider.template.type === TProviderFunctionTypes.AWS) {
|
||||
if (provider.template.client === TAwsProviderSystems.IAM) {
|
||||
const client = new IAMClient({
|
||||
region: newCredential.inputs.manager_user_aws_region as string,
|
||||
credentials: {
|
||||
accessKeyId: newCredential.inputs.manager_user_access_key as string,
|
||||
secretAccessKey: newCredential.inputs.manager_user_secret_key as string
|
||||
}
|
||||
});
|
||||
|
||||
const iamUserName = newCredential.inputs.iam_username as string;
|
||||
|
||||
if (variables.creds.length === 2) {
|
||||
const deleteCycleCredential = variables.creds.pop();
|
||||
if (deleteCycleCredential) {
|
||||
const deletedIamAccessKey = await client.send(
|
||||
new DeleteAccessKeyCommand({
|
||||
UserName: iamUserName,
|
||||
AccessKeyId: deleteCycleCredential.outputs.iam_user_access_key as string
|
||||
})
|
||||
);
|
||||
|
||||
if (
|
||||
!deletedIamAccessKey?.$metadata?.httpStatusCode ||
|
||||
deletedIamAccessKey?.$metadata?.httpStatusCode > 300
|
||||
) {
|
||||
throw new DisableRotationErrors({
|
||||
message: "Failed to delete aws iam access key. Check managed iam user policy"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newIamAccessKey = await client.send(new CreateAccessKeyCommand({ UserName: iamUserName }));
|
||||
if (!newIamAccessKey.AccessKey)
|
||||
throw new DisableRotationErrors({ message: "Failed to create access key. Check managed iam user policy" });
|
||||
|
||||
// test
|
||||
const testAccessKey = await client.send(
|
||||
new GetAccessKeyLastUsedCommand({ AccessKeyId: newIamAccessKey.AccessKey.AccessKeyId })
|
||||
);
|
||||
if (testAccessKey?.UserName !== iamUserName)
|
||||
throw new DisableRotationErrors({ message: "Failed to create access key. Check managed iam user policy" });
|
||||
|
||||
newCredential.outputs.iam_user_access_key = newIamAccessKey.AccessKey.AccessKeyId;
|
||||
newCredential.outputs.iam_user_secret_key = newIamAccessKey.AccessKey.SecretAccessKey;
|
||||
}
|
||||
}
|
||||
|
||||
/* Rotation function of HTTP infisical template
|
||||
* This is a generic http based template system for rotation
|
||||
* we use this for sendgrid and for custom secret rotation
|
||||
* This will ensure user provided rotation is easier to make
|
||||
* */
|
||||
if (provider.template.type === TProviderFunctionTypes.HTTP) {
|
||||
if (provider.template.functions.set?.pre) {
|
||||
secretRotationPreSetFn(provider.template.functions.set.pre, newCredential);
|
||||
@ -185,6 +259,9 @@ export const secretRotationQueueFactory = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// insert the new variables to start
|
||||
// encrypt the data - save it
|
||||
variables.creds.unshift({
|
||||
outputs: newCredential.outputs,
|
||||
internal: newCredential.internal
|
||||
@ -200,6 +277,7 @@ export const secretRotationQueueFactory = ({
|
||||
key
|
||||
)
|
||||
}));
|
||||
// map the final values to output keys in the board
|
||||
await secretRotationDAL.transaction(async (tx) => {
|
||||
await secretRotationDAL.updateById(
|
||||
rotationId,
|
||||
|
21
backend/src/ee/services/secret-rotation/templates/aws-iam.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { TAwsProviderSystems, TProviderFunctionTypes } from "./types";
|
||||
|
||||
export const AWS_IAM_TEMPLATE = {
|
||||
type: TProviderFunctionTypes.AWS as const,
|
||||
client: TAwsProviderSystems.IAM,
|
||||
inputs: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
manager_user_access_key: { type: "string" as const },
|
||||
manager_user_secret_key: { type: "string" as const },
|
||||
manager_user_aws_region: { type: "string" as const },
|
||||
iam_username: { type: "string" as const }
|
||||
},
|
||||
required: ["manager_user_access_key", "manager_user_secret_key", "manager_user_aws_region", "iam_username"],
|
||||
additionalProperties: false
|
||||
},
|
||||
outputs: {
|
||||
iam_user_access_key: { type: "string" },
|
||||
iam_user_secret_key: { type: "string" }
|
||||
}
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import { AWS_IAM_TEMPLATE } from "./aws-iam";
|
||||
import { MYSQL_TEMPLATE } from "./mysql";
|
||||
import { POSTGRES_TEMPLATE } from "./postgres";
|
||||
import { SENDGRID_TEMPLATE } from "./sendgrid";
|
||||
@ -24,5 +25,12 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
|
||||
image: "mysql.png",
|
||||
description: "Rotate MySQL@7/MariaDB user credentials",
|
||||
template: MYSQL_TEMPLATE
|
||||
},
|
||||
{
|
||||
name: "aws-iam",
|
||||
title: "AWS IAM",
|
||||
image: "aws-iam.svg",
|
||||
description: "Rotate AWS IAM User credentials",
|
||||
template: AWS_IAM_TEMPLATE
|
||||
}
|
||||
];
|
||||
|
@ -1,6 +1,7 @@
|
||||
export enum TProviderFunctionTypes {
|
||||
HTTP = "http",
|
||||
DB = "database"
|
||||
DB = "database",
|
||||
AWS = "aws"
|
||||
}
|
||||
|
||||
export enum TDbProviderClients {
|
||||
@ -10,6 +11,10 @@ export enum TDbProviderClients {
|
||||
MySql = "mysql"
|
||||
}
|
||||
|
||||
export enum TAwsProviderSystems {
|
||||
IAM = "iam"
|
||||
}
|
||||
|
||||
export enum TAssignOp {
|
||||
Direct = "direct",
|
||||
JmesPath = "jmesopath"
|
||||
@ -42,7 +47,7 @@ export type TSecretRotationProviderTemplate = {
|
||||
title: string;
|
||||
image?: string;
|
||||
description?: string;
|
||||
template: THttpProviderTemplate | TDbProviderTemplate;
|
||||
template: THttpProviderTemplate | TDbProviderTemplate | TAwsProviderTemplate;
|
||||
};
|
||||
|
||||
export type THttpProviderTemplate = {
|
||||
@ -70,3 +75,14 @@ export type TDbProviderTemplate = {
|
||||
};
|
||||
outputs: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type TAwsProviderTemplate = {
|
||||
type: TProviderFunctionTypes.AWS;
|
||||
client: TAwsProviderSystems;
|
||||
inputs: {
|
||||
type: "object";
|
||||
properties: Record<string, { type: string; [x: string]: unknown; desc?: string }>;
|
||||
required?: string[];
|
||||
};
|
||||
outputs: Record<string, unknown>;
|
||||
};
|
||||
|
143
docs/documentation/platform/secret-rotation/aws-iam.mdx
Normal file
@ -0,0 +1,143 @@
|
||||
---
|
||||
title: "AWS IAM User"
|
||||
description: "Rotated access key id and secret key of AWS IAM Users"
|
||||
---
|
||||
|
||||
Infisical's AWS IAM User secret rotation capability lets you update the **Access key** and **Secret access key** credentials of a target IAM user from within Infisical
|
||||
at a specified interval or on-demand.
|
||||
|
||||
## Workflow
|
||||
|
||||
The typical workflow for using the AWS IAM User rotation strategy consists of four steps:
|
||||
|
||||
1. Creating the target IAM user whose credentials you wish to rotate.
|
||||
2. Creating the managing IAM user used by Infisical to rotate the credentials of the target IAM user.
|
||||
3. Configuring the rotation strategy in Infisical with the credentials of the managing IAM user.
|
||||
4. Pressing the **Rotate** button in the Infisical dashboard to trigger the rotation of the target IAM user's credentials. The strategy can also be configured to rotate the credentials automatically at a specified interval.
|
||||
|
||||
In the following steps, we explore the end-to-end workflow for setting up this strategy in Infisical.
|
||||
|
||||
<Steps>
|
||||
<Step title="Create the target IAM user">
|
||||
To begin, create an IAM user whose credentials you wish to rotate. If you already have an IAM user,
|
||||
then you can skip this step.
|
||||
</Step>
|
||||
<Step title="Create the managing IAM user">
|
||||
Next, create another IAM user to be used by Infisical to rotate the credentials of the IAM user in the previous step.
|
||||
|
||||
2.1. In your AWS console, head to IAM > Access management > Users and press **Create user**.
|
||||
|
||||

|
||||
|
||||
2.2. Next, give the user a username like **infisical-rotation-manager** and press **Next**.
|
||||
|
||||

|
||||
|
||||
2.3. Next, in the **Set permissions** step, select **Attach policies directly** and then press **Create policy**.
|
||||
|
||||

|
||||
|
||||
2.4. Next, in the **Policy editor**, paste the following JSON and press **Next**:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "VisualEditor0",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"iam:DeleteAccessKey",
|
||||
"iam:GetAccessKeyLastUsed",
|
||||
"iam:CreateAccessKey"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
The IAM policy above uses the wildcard option in Resource: "*".
|
||||
|
||||
You may want to restrict the policy to a specific path, and make any adjustments as necessary, to control access for the managing user in production.
|
||||
|
||||
Read more about this [here](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/).
|
||||
</Note>
|
||||
|
||||
In the **Review and create** step, give the policy a name like **infisical-rotation-manager**, press **Create policy** to finish creating the policy.
|
||||
|
||||

|
||||
|
||||
2.5. Back in the **Set permissions** step from step 2.3, refresh the policy list and search for the policy you just created from step 2.4.
|
||||
|
||||
Select the policy and press **Next**.
|
||||
|
||||

|
||||
|
||||
In the **Review and create** step, press **Create user** to finish creating the IAM user.
|
||||
|
||||

|
||||
|
||||
2.5. Having created the user, head to its Security credentials > Access keys and press **Create access key**.
|
||||
|
||||
Follow the subsequent steps to create the **access key** and **secret access key** credential pair for the user.
|
||||
|
||||

|
||||
|
||||
At the end of the flow, copy the **Access key** and **Secret access key** to use when configuring the AWS IAM User rotation strategy back in Infisical next.
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Configure the AWS IAM User secret rotation strategy in Infisical">
|
||||
3.1. Back in Infisical, head to the Project > Secrets > Environment and path where you want the rotated AWS IAM credentials to appear and create two placeholder secrets.
|
||||
|
||||
In this example, we'll create two secrets called `AWS_ACCESS_KEY` and `AWS_SECRET_ACCESS_KEY`.
|
||||
|
||||

|
||||
|
||||
3.2. Next, in the **Secret Rotation** tab, press on the **AWS IAM** tile to configure the AWS IAM User rotation strategy.
|
||||
|
||||

|
||||
|
||||
3.3. Input the configuration details for the AWS IAM User rotation strategy obtained from steps 1 and 2:
|
||||
|
||||

|
||||
|
||||
Here's some guidance on each field:
|
||||
|
||||
- Manager User Access Key: The managing IAM user's access key from step 2.5.
|
||||
- Manager User Secret Key: The managing IAM user's secret access key from step 2.5.
|
||||
- Manager User AWS Region: The [AWS region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html) for Infisical to make requests to such as `us-east-1`.
|
||||
- IAM Username: The IAM username of the user from step 1.
|
||||
|
||||
Next, specify the output secret mappings configuration for the rotated AWS IAM credentials; this is the secrets whose values will be replaced with new credentials after each rotation.
|
||||
Here, you can also specify a rotation interval for the credentials to be automatically rotated periodically.
|
||||
|
||||
In this example, we want to map the output of the rotated AWS IAM credentials to the secrets that we created in step 3.1 (i.e. `AWS_ACCESS_KEY` and `AWS_SECRET_ACCESS_KEY`).
|
||||
|
||||

|
||||
|
||||
Finally, press **Submit** to create the secret rotation strategy.
|
||||
</Step>
|
||||
<Step title="Rotate secrets in Infisical">
|
||||
You should now see the AWS IAM User rotation strategy listed in the **Secret Rotation** tab.
|
||||
|
||||
To manually trigger a rotation, you can press the **Rotate** button on the strategy.
|
||||
Once triggered, the secrets in step 3.1 should be updated with new rotated credential values.
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
**FAQ**
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Why are my AWS IAM credentials not rotating?">
|
||||
There are a few reasons for why this might happen:
|
||||
|
||||
- The strategy configuration is invalid (e.g. the managing IAM user's credentials are incorrect, the target IAM username is incorrect, etc.).
|
||||
- The managing IAM user is insufficently permissioned to rotate the credentials of the target IAM user. For instance, you may have setup [paths](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) for the managing IAM user and the policy does not have the necessary permissions to rotate the credentials.
|
||||
- The target IAM user already has 2 access keys configured in AWS; you should delete one of the access keys to allow for rotation.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
After Width: | Height: | Size: 471 KiB |
After Width: | Height: | Size: 494 KiB |
After Width: | Height: | Size: 532 KiB |
After Width: | Height: | Size: 412 KiB |
After Width: | Height: | Size: 341 KiB |
After Width: | Height: | Size: 361 KiB |
After Width: | Height: | Size: 434 KiB |
After Width: | Height: | Size: 402 KiB |
After Width: | Height: | Size: 288 KiB |
After Width: | Height: | Size: 367 KiB |
After Width: | Height: | Size: 343 KiB |
After Width: | Height: | Size: 311 KiB |
After Width: | Height: | Size: 597 KiB |
After Width: | Height: | Size: 691 KiB |
@ -134,7 +134,8 @@
|
||||
"documentation/platform/secret-rotation/overview",
|
||||
"documentation/platform/secret-rotation/sendgrid",
|
||||
"documentation/platform/secret-rotation/postgres",
|
||||
"documentation/platform/secret-rotation/mysql"
|
||||
"documentation/platform/secret-rotation/mysql",
|
||||
"documentation/platform/secret-rotation/aws-iam"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
1
frontend/public/images/secretRotation/aws-iam.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="1306" height="2500" viewBox="0 0 256 490" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M21 165.75l-21 6.856 21.75 2.519-.75-9.375M19.955 206.806L128 213.714l108.045-6.908L128 185.75 19.955 206.806M234.5 175.125l21.5-2.519-21.5-5.731v8.25" fill="#3C4929"/><path d="M157.387 352.929l56.606 13.396-56.756 17.116.15-30.512" fill="#B7CA9D"/><path d="M19.955 92.221V54.019L128 0l.482.405-.248 48.496-.234.102-.405 1.117-59.098 23.856-.542 84.037 31.452-5.29L128 147.002V490.03l-32.369-16.177v-45.771l-28.354-11.338V202.069l-47.322 4.737v-38.195L0 172.606v-72.408l19.955-7.977" fill="#4B612C"/><path d="M99.408 152.727l-32.131 6.424V73.28l32.131 10.018v69.429M183.925 27.959l52.106 26.06v38.202L256 100.198V172.6l-19.969-3.989v38.195l-25.441-2.538-21.881-2.199v42.939h47.336v39.284l-21.997 1.974v39.611l-53.692 10.672v45.77l53.57-15.899.122 40.38-53.692 21.282v45.771L128 490.03V147.002l28.572 5.71 30.583 4.038V73.966l-58.338-22.498-.817-2.465V0l55.925 27.959" fill="#759C3E"/><path d="M160.356 61.941L128 49.01 67.277 73.28l32.131 10.018 60.948-21.357" fill="#3C4929"/><path d="M67.277 73.28L128 49.01l12.775 5.104 19.581 7.827 28.353 11.353-1.515 1.541-28.876 8.991-1.74-.528L128 73.28 99.408 83.298 67.277 73.28" fill="#3C4929"/><path d="M156.578 83.298l32.131-10.004v85.864l-32.131-6.446V83.298" fill="#4B612C"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -351,7 +351,7 @@ export const SecretRotationPage = withProjectPermission(
|
||||
{!isRotationProviderLoading &&
|
||||
secretRotationProviders?.providers.map((provider) => (
|
||||
<div
|
||||
className="group relative flex h-32 cursor-pointer flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 hover:border-primary/40 hover:bg-primary/10"
|
||||
className="group relative flex h-32 cursor-pointer flex-row items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 hover:border-primary/40 hover:bg-primary/10"
|
||||
key={`infisical-rotation-provider-${provider.name}`}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
@ -362,8 +362,8 @@ export const SecretRotationPage = withProjectPermission(
|
||||
>
|
||||
<img
|
||||
src={`/images/secretRotation/${provider.image}`}
|
||||
height={70}
|
||||
width={70}
|
||||
className="max-h-16"
|
||||
style={{ maxWidth: "6rem" }}
|
||||
alt="rotation provider logo"
|
||||
/>
|
||||
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
|