diff --git a/cdk.json b/cdk.json index 8fe789f..f23ed81 100644 --- a/cdk.json +++ b/cdk.json @@ -6,6 +6,7 @@ "eks_cluster_name": "test-cluster-chaos", "security_group_id": "sg-022eb488dbd1655b3", "target_role_name": "SSM-Chaos", - "s3-bucket-to-deny": "chaos-ssm-documents/*" + "s3-bucket-to-deny": "chaos-ssm-documents/*", + "ssm_parameter_name": "chaoslambda.config" } } diff --git a/lib/fis-experiments/lambda-faults/experiments-stack.ts b/lib/fis-experiments/lambda-faults/experiments-stack.ts new file mode 100644 index 0000000..b1d2b7a --- /dev/null +++ b/lib/fis-experiments/lambda-faults/experiments-stack.ts @@ -0,0 +1,68 @@ +import * as cdk from "aws-cdk-lib"; +import { Construct } from "constructs"; +import { StackProps, Stack } from "aws-cdk-lib"; +import { aws_fis as fis } from "aws-cdk-lib"; +import { aws_iam as iam } from "aws-cdk-lib"; + +export class LambdaChaosExperiments extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + // Import FIS Role, Stop Condition, and other required parameters + const importedFISRoleArn = cdk.Fn.importValue("FISIamRoleArn"); + const importedStopConditionArn = cdk.Fn.importValue("StopConditionArn"); + const importedSSMAPutParameterStoreRoleArn = cdk.Fn.importValue( + "SSMAPutParameterStoreRoleArn" + ); + const importedPutParameterStoreSSMADocName = cdk.Fn.importValue( + "PutParameterStoreSSMADocName" + ); + + const importedParameterName = this.node.tryGetContext("ssm_parameter_name"); + + // Targets - empty since SSMA defines its own targets + + // Actions + const startAutomation = { + actionId: "aws:ssm:start-automation-execution", + description: "Put config into parameter store to enable Lambda Chaos.", + parameters: { + documentArn: `arn:aws:ssm:${this.region}:${ + this.account + }:document/${importedPutParameterStoreSSMADocName.toString()}`, + documentParameters: JSON.stringify({ + DurationMinutes: "PT1M", + AutomationAssumeRole: importedSSMAPutParameterStoreRoleArn.toString(), + ParameterName: importedParameterName.toString(), + ParameterValue: "{ \"delay\": 500, \"is_enabled\": true, \"error_code\": 404, \"exception_msg\": \"This is chaos\", \"rate\": 1, \"fault_type\": \"exception\"}", + RollbackValue: "{ \"delay\": 500, \"is_enabled\": false, \"error_code\": 404, \"exception_msg\": \"This is chaos\", \"rate\": 1, \"fault_type\": \"exception\"}" + }), + maxDuration: "PT5M", + }, + }; + + // Experiments + const templateInjectS3AccessDenied = new fis.CfnExperimentTemplate( + this, + "fis-template-inject-lambda-fault", + { + description: "Inject faults into Lambda function using chaos-lambda library", + roleArn: importedFISRoleArn.toString(), + stopConditions: [ + { + source: "aws:cloudwatch:alarm", + value: importedStopConditionArn.toString(), + }, + ], + tags: { + Name: "Inject fault to Lambda functions", + Stackname: this.stackName, + }, + actions: { + ssmaAction: startAutomation, + }, + targets: {}, + } + ); + } +} diff --git a/lib/fis-upload-ssm-docs/documents/ssma-put-config-parameterstore.yml b/lib/fis-upload-ssm-docs/documents/ssma-put-config-parameterstore.yml new file mode 100644 index 0000000..a1561d0 --- /dev/null +++ b/lib/fis-upload-ssm-docs/documents/ssma-put-config-parameterstore.yml @@ -0,0 +1,114 @@ +--- +#================================================== +# SSM Automation Document / Runbook: +# Defines the configuration as well as the +# the steps to be run by SSM Automation +#================================================== + +description: | + ### Document Name - ParameterStore-FIS-Automation + + ## What does this document do? + This document stores a particular configuration to SSM Parameter store. + https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html + + ## Security Risk + Low: This is not a fault per se, but a configuration change.The change should be restricted by a strict IAM role that only allows changing a particular ParameterName. https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html + + ## Input Parameters + * AutomationAssumeRole: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. + * ParameterName: (Required) The name of the parameter to modify. + * ParameterValue: (Required) The value of the parameter. + * RollbackValue: (Required) The value of the parameter to roll-back to. + * Type: (Optional) The type of parameter. String, StringList, or SecureString. Default String. + * DurationMinutes: (Optional) ** Default 1 minute ** Maximum duration the fault can exist for. + + ## Supports Rollback + Yes. The configuration is reverted to a . + + ## Cancellation behaviour + The parameter value is rollback to RollbackValue. + + ## Output Parameters + This document has no outputs. + + ## Minimum Permissions Required + * ssm:PutParameter + + ## Additional Permissions for logging + * logs:CreateLogStream + * logs:CreateLogGroup + * logs:PutLogEvents + * logs:DescribeLogGroups + * logs:DescribeLogStreams + +schemaVersion: "0.3" + +#================================================== +# Role assumed my the automation document / runbook +#================================================== +assumeRole: "{{ AutomationAssumeRole }}" + +#================================================== +# SSM automation document parameters +#================================================== + +parameters: + ParameterName: + type: String + description: "(Required) The name of the parameter to modify." + ParameterValue: + type: String + description: "(Required) The value of the parameter." + RollbackValue: + type: String + description: "(Required) The value of the parameter to roll-back to." + ParameterType: + type: String + description: "(Optional) The type of parameter. String, StringList, or SecureString." + default: "String" + DurationMinutes: + type: String + description: "The duration - in ISO-8601 format - until rollback. (Required)" + default: "PT1M" + AutomationAssumeRole: + type: String + description: + "(Optional) The ARN of the role that allows Automation to perform + the actions on your behalf." + +#================================================== +# Automation steps +#================================================== + +mainSteps: + - name: putParameter + description: Adding value to a particular parameter + onFailure: "step:rollback" + onCancel: "step:rollback" + action: "aws:executeAwsApi" + inputs: + Service: ssm + Api: PutParameter + Name: '{{ ParameterName }}' + Value: '{{ ParameterValue }}' + Type: '{{ ParameterType }}' + Overwrite: true + + - name: sleep + action: aws:sleep + onFailure: "step:rollback" + onCancel: "step:rollback" + inputs: + Duration: "{{ DurationMinutes }}" + + - name: rollback + description: Rolling back value to a particular parameter + action: "aws:executeAwsApi" + inputs: + Service: ssm + Api: PutParameter + Name: '{{ ParameterName }}' + Value: '{{ RollbackValue }}' + Type: '{{ ParameterType }}' + Overwrite: true \ No newline at end of file diff --git a/lib/fis-upload-ssm-docs/ssm-upload-stack.ts b/lib/fis-upload-ssm-docs/ssm-upload-stack.ts index fce0ded..64fc41e 100644 --- a/lib/fis-upload-ssm-docs/ssm-upload-stack.ts +++ b/lib/fis-upload-ssm-docs/ssm-upload-stack.ts @@ -59,6 +59,26 @@ export class FisSsmDocs extends Stack { } ); + // Deploy the SSMA document to modify a parameter store value + let parameterstore_file = path.join( + __dirname, + "documents/ssma-put-config-parameterstore.yml" + ); + + const parameterstore_content = fs + .readFileSync(parameterstore_file) + .toString(); + + const parameterstore_cfnDocument = new ssm.CfnDocument( + this, + `ParameterStore-SSM-Document`, + { + content: yaml.load(parameterstore_content), + documentType: "Automation", + documentFormat: "YAML", + } + ); + // SSMA Role for SSMA Documents fault const iamAccessFaultRole = this.node.tryGetContext("target_role_name"); @@ -187,32 +207,39 @@ export class FisSsmDocs extends Stack { }) ); - //AllowOnlyTargetResourcePolicies - ssmaIamAccessRole.addToPolicy( - new iam.PolicyStatement({ - resources: ["*"], - actions: ["iam:DetachRolePolicy", "iam:AttachRolePolicy"], - conditions: { - ArnEquals: { - "iam:PolicyARN": [`arn:aws:iam::${this.account}:policy/*`], - }, - }, - }) + // IAM role access faults 'ssma-put-config-parameterstore.yml' + const ssmParameterName = this.node.tryGetContext("ssm_parameter_name"); + + const ssmaPutParameterStoreRole = new iam.Role( + this, + "ssma-put-parameterstore-role", + { + assumedBy: new iam.CompositePrincipal( + new iam.ServicePrincipal("iam.amazonaws.com"), + new iam.ServicePrincipal("ssm.amazonaws.com") + ), + } ); - // DenyAttachDetachAllRolesExceptApplicationRole - ssmaIamAccessRole.addToPolicy( + const ssmaPutParameterStoreRoleAsCfn = ssmaPutParameterStoreRole.node + .defaultChild as iam.CfnRole; + ssmaPutParameterStoreRoleAsCfn.addOverride( + "Properties.AssumeRolePolicyDocument.Statement.0.Principal.Service", + ["ssm.amazonaws.com", "iam.amazonaws.com"] + ); + + // GetRoleandPolicyDetails + ssmaPutParameterStoreRole.addToPolicy( new iam.PolicyStatement({ - effect: iam.Effect.DENY, - actions: ["iam:DetachRolePolicy", "iam:AttachRolePolicy"], - notResources: [ - `arn:aws:iam::${this.account}:role/${iamAccessFaultRole}`, + resources: [ + `arn:aws:ssm:${this.region}:${this.account}:parameter/${ssmParameterName}`, ], + actions: ["ssm:PutParameter"], }) ); // Additional Permissions for logging - ssmaIamAccessRole.addToPolicy( + ssmaPutParameterStoreRole.addToPolicy( new iam.PolicyStatement({ resources: ["*"], actions: [ @@ -244,6 +271,12 @@ export class FisSsmDocs extends Stack { exportName: "IamAccessSSMADocName", }); + new cdk.CfnOutput(this, "PutParameterStoreSSMADocName", { + value: parameterstore_cfnDocument.ref!, + description: "The name of the SSM Doc", + exportName: "PutParameterStoreSSMADocName", + }); + new cdk.CfnOutput(this, "SSMANaclRoleArn", { value: ssmaNaclRole.roleArn, description: "The Arn of the IAM role", @@ -261,5 +294,11 @@ export class FisSsmDocs extends Stack { description: "The Arn of the IAM role", exportName: "SSMAIamAccessRoleArn", }); + + new cdk.CfnOutput(this, "SSMAPutParameterStoreRoleArn", { + value: ssmaPutParameterStoreRole.roleArn, + description: "The Arn of the IAM role", + exportName: "SSMAPutParameterStoreRoleArn", + }); } } diff --git a/lib/parent-stack.ts b/lib/parent-stack.ts index 4af15dc..0964edb 100644 --- a/lib/parent-stack.ts +++ b/lib/parent-stack.ts @@ -12,6 +12,7 @@ import { AsgExperiments } from "./fis-experiments/asg-faults/experiments-stack"; import { EksExperiments } from "./fis-experiments/eks-faults/experiments-stack"; import { SecGroupExperiments } from "./fis-experiments/security-groups-faults/experiments-stack"; import { IamAccessExperiments } from "./fis-experiments/iam-access-faults/experiments-stack"; +import { LambdaChaosExperiments } from "./fis-experiments/lambda-faults/experiments-stack"; export class FIS extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { @@ -45,6 +46,11 @@ export class FIS extends Stack { this, "IamAccExp" ); + const LambdaFaultExperimentStack = new LambdaChaosExperiments( + this, + 'LambdaExp' + ) + Ec2InstancesExperimentStack.node.addDependency(IamRoleStack); Ec2InstancesExperimentStack.node.addDependency(StopConditionStack); @@ -60,5 +66,7 @@ export class FIS extends Stack { SecGroupExperimentsStack.node.addDependency(StopConditionStack); IamAccessExperimentsStack.node.addDependency(IamRoleStack); IamAccessExperimentsStack.node.addDependency(StopConditionStack); + LambdaFaultExperimentStack.addDependency(IamRoleStack); + LambdaFaultExperimentStack.addDependency(StopConditionStack); } }