diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index 12ae88bb02d2e..d6a45e77cb7e1 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -120,6 +120,18 @@ bucket.grantReadWrite(lambda); Will give the Lambda's execution role permissions to read and write from the bucket. +## AWS Foundational Security Best Practices + +### Enforcing SSL + +To require all requests use Secure Socket Layer (SSL): + +```ts +const bucket = new Bucket(this, 'Bucket', { + enforceSSL: true +}); +``` + ## Sharing buckets between stacks To use a bucket in a different stack in the same CDK application, pass the object to the other stack: diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 4323f47daf575..1edbb9c7a5040 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -1061,6 +1061,14 @@ export interface BucketProps { */ readonly encryptionKey?: kms.IKey; + /** + * Enforces SSL for requests. S3.5 of the AWS Foundational Security Best Practices Regarding S3. + * @see https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-ssl-requests-only.html + * + * @default false + */ + readonly enforceSSL?: boolean; + /** * Specifies whether Amazon S3 should use an S3 Bucket Key with server-side * encryption using KMS (SSE-KMS) for new objects in the bucket. @@ -1357,6 +1365,11 @@ export class Bucket extends BucketBase { this.disallowPublicAccess = props.blockPublicAccess && props.blockPublicAccess.blockPublicPolicy; this.accessControl = props.accessControl; + // Enforce AWS Foundational Security Best Practice + if (props.enforceSSL) { + this.enforceSSLStatement(); + } + if (props.serverAccessLogsBucket instanceof Bucket) { props.serverAccessLogsBucket.allowLogDelivery(); } @@ -1479,6 +1492,22 @@ export class Bucket extends BucketBase { this.inventories.push(inventory); } + /** + * Adds an iam statement to enforce SSL requests only. + */ + private enforceSSLStatement() { + const statement = new iam.PolicyStatement({ + actions: ['s3:*'], + conditions: { + Bool: { 'aws:SecureTransport': 'false' }, + }, + effect: iam.Effect.DENY, + resources: [this.arnForObjects('*')], + principals: [new iam.AnyPrincipal()], + }); + this.addToResourcePolicy(statement); + } + private validateBucketName(physicalName: string): void { const bucketName = physicalName; if (!bucketName || Token.isUnresolved(bucketName)) { diff --git a/packages/@aws-cdk/aws-s3/test/bucket.test.ts b/packages/@aws-cdk/aws-s3/test/bucket.test.ts index 0fb1bffa3726f..5a5f89a3eba94 100644 --- a/packages/@aws-cdk/aws-s3/test/bucket.test.ts +++ b/packages/@aws-cdk/aws-s3/test/bucket.test.ts @@ -277,6 +277,58 @@ describe('bucket', () => { }); + test('enforceSsl can be enabled', () => { + const stack = new cdk.Stack(); + new s3.Bucket(stack, 'MyBucket', { enforceSSL: true }); + + expect(stack).toMatchTemplate({ + 'Resources': { + 'MyBucketF68F3FF0': { + 'Type': 'AWS::S3::Bucket', + 'UpdateReplacePolicy': 'Retain', + 'DeletionPolicy': 'Retain', + }, + 'MyBucketPolicyE7FBAC7B': { + 'Type': 'AWS::S3::BucketPolicy', + 'Properties': { + 'Bucket': { + 'Ref': 'MyBucketF68F3FF0', + }, + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 's3:*', + 'Condition': { + 'Bool': { + 'aws:SecureTransport': 'false', + }, + }, + 'Effect': 'Deny', + 'Principal': '*', + 'Resource': { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'MyBucketF68F3FF0', + 'Arn', + ], + }, + '/*', + ], + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + }, + }, + }, + }); + }); + test('bucketKeyEnabled can be enabled', () => { const stack = new cdk.Stack();