Skip to content

Commit

Permalink
feat: add IAM managed policy for accessing S3 bucket and DynamoDB tab…
Browse files Browse the repository at this point in the history
…le (#41)

- Added a new IAM managed policy to allow reading/writing to the S3 bucket and objects, as well as reading/writing to the DynamoDB table.
- The policy statements include actions such as `s3:ListBucket`, `s3:GetObject`, `s3:PutObject`, `s3:DeleteObject`, `dynamodb:DescribeTable`, `dynamodb:GetItem`, `dynamodb:PutItem`, and `dynamodb:DeleteItem`.
- The managed policy is named "TerraformStateBackendPolicy" and is associated with the Terraform state backend construct.
  • Loading branch information
stefanfreitag committed Jul 27, 2023
1 parent 6926511 commit 4c93afe
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 55 deletions.
6 changes: 5 additions & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@

## DynamoDB Table

- [[DynamoDB.2] DynamoDB tables should have point-in-time recovery enabled](https://docs.aws.amazon.com/securityhub/latest/userguide/dynamodb-controls.html#dynamodb-2)
- [[DynamoDB.2] DynamoDB tables should have point-in-time recovery enabled](https://docs.aws.amazon.com/securityhub/latest/userguide/dynamodb-controls.html#dynamodb-2)

## IAM

- Managed policy for accessing the S3 bucket and keys as well as the DynamoDB table.
64 changes: 63 additions & 1 deletion src/terraformStateBackend.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { aws_dynamodb as dynamodb, aws_s3 as s3, Duration, RemovalPolicy } from 'aws-cdk-lib';
import {
aws_dynamodb as dynamodb,
aws_s3 as s3,
Duration,
RemovalPolicy,
CfnOutput,
} from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
import { TerraformStateBackendProperties } from './terraformStateBackendProperties';

Expand Down Expand Up @@ -53,5 +60,60 @@ export class TerraformStateBackend extends Construct {
pointInTimeRecovery: true,
removalPolicy: RemovalPolicy.DESTROY,
});

this.createIamPolicies();

new CfnOutput(this, 'output-table',
{
description: 'ARN of the DynamoDB table',
exportName: 'tableArn',
value: this.table.tableArn,
});

new CfnOutput(this, 'output-bucket',
{
description: 'ARN of the S3 bucket',
exportName: 'bucketArn',
value: this.bucket.bucketArn,
});

}

private createIamPolicies() {
// Policy Statement for reading/ writing DyanmoDB table
const ddbStatement = new iam.PolicyStatement({
sid: 'DynamoDBTable',
effect: iam.Effect.ALLOW,
actions: [
'dynamodb:DescribeTable',
'dynamodb:GetItem',
'dynamodb:PutItem',
'dynamodb:DeleteItem',
],
resources: [this.table.tableArn],
});
// Policy Statement for reading/ writing to the S3 bucket and objects
const s3Statement = new iam.PolicyStatement({
sid: 'S3Bucket',
effect: iam.Effect.ALLOW,
actions: [
's3:ListBucket',
's3:GetObject',
's3:PutObject',
's3:DeleteObject',
],
resources: [this.bucket.bucketArn, this.bucket.bucketArn + '/*'],
});

new iam.PolicyDocument({
statements: [ddbStatement, s3Statement],
});
new iam.ManagedPolicy(this, 'managed-policy', {
description: 'Managed policy for Terraform state backend',
statements: [ddbStatement, s3Statement],
managedPolicyName: 'TerraformStateBackendPolicy',
});


}
}
202 changes: 149 additions & 53 deletions test/terraformStateBackend.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import * as cdk from 'aws-cdk-lib';
import { Aspects, assertions } from 'aws-cdk-lib';
import { Annotations, Match } from 'aws-cdk-lib/assertions';
import { AwsSolutionsChecks, HIPAASecurityChecks, NagSuppressions } from 'cdk-nag';
import {
AwsSolutionsChecks,
HIPAASecurityChecks,
NagSuppressions,
} from 'cdk-nag';
import { TerraformStateBackend } from '../src';

describe('Ensure passing AWSSolutionChecks', () => {
Expand All @@ -11,12 +15,22 @@ describe('Ensure passing AWSSolutionChecks', () => {
app = new cdk.App();
stack = new cdk.Stack(app, 'stack', {});
NagSuppressions.addStackSuppressions(stack, [
{ id: 'AwsSolutions-S1', reason: 'Access Logs for Terraform Bucket not implemented.' },
{
id: 'AwsSolutions-S1',
reason: 'Access Logs for Terraform Bucket not implemented.',
},
{
id: 'AwsSolutions-IAM5',
reason:
'Wilcard permission in place for accessing objects in an S3 bucket.',
},
]);

Aspects.of(app).add(new AwsSolutionsChecks({
verbose: true,
}));
Aspects.of(app).add(
new AwsSolutionsChecks({
verbose: true,
}),
);

new TerraformStateBackend(stack, 'backend', {
bucketName: 'tf-state-bucket',
Expand Down Expand Up @@ -48,16 +62,31 @@ describe('Ensure passing HIPAASecurityChecks', () => {
app = new cdk.App();
stack = new cdk.Stack(app, 'stack', {});
NagSuppressions.addStackSuppressions(stack, [
{ id: 'HIPAA.Security-S3BucketLoggingEnabled', reason: 'Access Logs for Terraform Bucket not implemented.' },
{ id: 'HIPAA.Security-S3BucketReplicationEnabled', reason: 'Cross-region replication for Terraform Bucket not implemented.' },
{ id: 'HIPAA.Security-DynamoDBInBackupPlan', reason: 'Backup plan for DynamoDB table not implemented.' },
{ id: 'HIPAA.Security-S3DefaultEncryptionKMS', reason: 'KMS usage currently not implemented.' },
{
id: 'HIPAA.Security-S3BucketLoggingEnabled',
reason: 'Access Logs for Terraform Bucket not implemented.',
},
{
id: 'HIPAA.Security-S3BucketReplicationEnabled',
reason:
'Cross-region replication for Terraform Bucket not implemented.',
},
{
id: 'HIPAA.Security-DynamoDBInBackupPlan',
reason: 'Backup plan for DynamoDB table not implemented.',
},
{
id: 'HIPAA.Security-S3DefaultEncryptionKMS',
reason: 'KMS usage currently not implemented.',
},
]);

Aspects.of(app).add(new HIPAASecurityChecks({
reports: true,
verbose: true,
}));
Aspects.of(app).add(
new HIPAASecurityChecks({
reports: true,
verbose: true,
}),
);

new TerraformStateBackend(stack, 'backend', {
bucketName: 'tf-state-bucket',
Expand Down Expand Up @@ -128,51 +157,44 @@ describe('Bucket Configuration', () => {
const template = assertions.Template.fromStack(stack);
template.resourceCountIs('AWS::S3::BucketPolicy', 1);

const logicalId = stack.getLogicalId(backend.bucket.node.defaultChild as cdk.CfnResource);
const logicalId = stack.getLogicalId(
backend.bucket.node.defaultChild as cdk.CfnResource,
);

template.hasResourceProperties(
'AWS::S3::BucketPolicy',
{
PolicyDocument: {
Statement: [
{
Action: 's3:*',
Condition: {
Bool: {
'aws:SecureTransport': 'false',
},
template.hasResourceProperties('AWS::S3::BucketPolicy', {
PolicyDocument: {
Statement: [
{
Action: 's3:*',
Condition: {
Bool: {
'aws:SecureTransport': 'false',
},
Effect: 'Deny',
Principal: {
AWS: '*',
},
Effect: 'Deny',
Principal: {
AWS: '*',
},
Resource: [
{
'Fn::GetAtt': [logicalId, 'Arn'],
},
Resource: [
{
'Fn::GetAtt': [
logicalId,
'Arn',
],
},
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
logicalId,
'Arn',
],
},
'/*',
],
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [logicalId, 'Arn'],
},
'/*',
],
},
],
},
],
},
],
},
],
},
],
},
);
});
});

test('[S3.14] S3 buckets should use versioning', () => {
Expand Down Expand Up @@ -234,3 +256,77 @@ describe('DynamoDB Configuration', () => {
);
});
});

describe('IAM Configuration', () => {
let stack: cdk.Stack;
let app: cdk.App;
let backend: TerraformStateBackend;
beforeEach(() => {
app = new cdk.App();
stack = new cdk.Stack(app, 'stack', {});

backend = new TerraformStateBackend(stack, 'backend', {
bucketName: 'tf-state-bucket',
tableName: 'tf-state-lock',
});
});

test('Managed policy for accessing S3 Bucket and DynamoDB', () => {

const logicalIdTable = stack.getLogicalId(
backend.table.node.defaultChild as cdk.CfnResource,
);

const logicalIdBucket = stack.getLogicalId(
backend.bucket.node.defaultChild as cdk.CfnResource,
);
assertions.Template.fromStack(stack).hasResourceProperties(
'AWS::IAM::ManagedPolicy',
{
ManagedPolicyName: 'TerraformStateBackendPolicy',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Sid: 'DynamoDBTable',
Action: [
'dynamodb:DescribeTable',
'dynamodb:GetItem',
'dynamodb:PutItem',
'dynamodb:DeleteItem',
],
Resource: { 'Fn::GetAtt': [logicalIdTable, 'Arn'] },
},
{
Sid: 'S3Bucket',
Effect: 'Allow',
Action: [
's3:ListBucket',
's3:GetObject',
's3:PutObject',
's3:DeleteObject',
],
Resource: [
{
'Fn::GetAtt': [logicalIdBucket, 'Arn'],
},
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [logicalIdBucket, 'Arn'],
},
'/*',
],
],
},
],
},
],
},
},
);
});
});

0 comments on commit 4c93afe

Please sign in to comment.