Skip to content

Commit

Permalink
Merge pull request #1 from mineiros-io/mariux/initial-features
Browse files Browse the repository at this point in the history
initial secure s3-bucket setup
  • Loading branch information
mariux authored Jan 2, 2020
2 parents 2d4f357 + 171ff1c commit 68f32e9
Show file tree
Hide file tree
Showing 5 changed files with 446 additions and 1 deletion.
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,41 @@
# aws-s3-bucket
# Terraform Module for creating a secure S3-Bucket

## Features
In contrast to the plain `aws_s3_bucket` resource this module creates secure
buckets by default. While all security features can be disabled as needed best practices
are pre-configured.

In addition to security easy cross-account access can be granted to the objects
of the bucket enforcing `bucket-owner-full-control` acl for objects created by other accounts.

### Default Security Settings
- :heavy_check_mark: Bucket public access blocking all set to `true` by default
- :heavy_check_mark: Server-Side-Encryption (SSE) at rest `enabled` by default (AES256)
- :heavy_check_mark: Bucket ACL defaults to canned `private` ACL

### Standard S3 Features
- :heavy_check_mark: Server-Side-Encryption (SSE) enabled by default
- :heavy_check_mark: Versioning
- :heavy_check_mark: Bucket Logging
- :heavy_check_mark: Lifecycle Rules
- :heavy_check_mark: Request Payer
- :heavy_check_mark: Cross-Origin Resource Sharing (CORS)
- :heavy_check_mark: Acceleration Status
- :heavy_check_mark: Bucket Policy
- :heavy_check_mark: Tags
- :x: Replication Configuration (not yet implemented)
- :x: Website Configuration (not yet implemented)
- :x: S3 Object Locking (not yet implemented)

### Extended S3 Features
- :heavy_check_mark: Bucket Public Access Blocking defaulting to `true`
- :x: Bucket Notifications (not yet implemented)
- :x: Bucket Metrics (not yet implemented)
- :x: Bucket Inventory (not yet implemented)
- :x: S3 Access Points (not yet supported by terraform aws provider :warning:)

### Additional Features
- :heavy_check_mark: Cross-Account access policy with forced `bucket-owner-full-control` ACL for direct access
- :x: Cloudfront Origin Access Identity (OAI) policy
- :x: Generate Cross-Account role for OAI enabled buckets if desired
- :x: Generate KMS key to encrypt objects at rest if desired
215 changes: 215 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
locals {
cors_enabled = length(keys(var.cors_rule)) > 0
versioning_enabled = length(keys(var.versioning)) > 0
logging_enabled = length(keys(var.logging)) > 0
sse_enabled = length(keys(var.apply_server_side_encryption_by_default)) > 0

cors = local.cors_enabled ? [var.cors_rule] : []
versioning = local.versioning_enabled ? [var.versioning] : []
logging = local.logging_enabled ? [var.logging] : []
encryption = local.sse_enabled ? [var.apply_server_side_encryption_by_default] : []
}

resource "aws_s3_bucket" "bucket" {
count = var.create ? 1 : 0

bucket = var.bucket
bucket_prefix = var.bucket_prefix
acl = var.acl
tags = var.tags
force_destroy = var.force_destroy
acceleration_status = var.acceleration_status
region = var.region
request_payer = var.request_payer

dynamic "cors_rule" {
for_each = local.cors

content {
allowed_headers = lookup(cors_rule.value, "allowed_headers", null)
allowed_methods = cors_rule.value.allowed_methods
allowed_origins = cors_rule.value.allowed_origins
expose_headers = lookup(cors_rule.value, "expose_headers", null)
max_age_seconds = lookup(cors_rule.value, "max_age_seconds", null)
}
}

dynamic "versioning" {
for_each = local.versioning

content {
enabled = lookup(versioning.value, "enabled", null)
mfa_delete = lookup(versioning.value, "mfa_delete", null)
}
}

dynamic "logging" {
for_each = local.logging

content {
target_bucket = logging.value.target_bucket
target_prefix = lookup(logging.value, "target_prefix", null)
}
}

dynamic "server_side_encryption_configuration" {
for_each = local.encryption
iterator = sse

content {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = sse.value.sse_algorithm
kms_master_key_id = lookup(sse.value, "kms_master_key_id", null)
}
}
}
}

dynamic "lifecycle_rule" {
for_each = var.lifecycle_rules
iterator = rule

content {
id = lookup(rule.value, "id", null)
prefix = lookup(rule.value, "prefix", null)
tags = lookup(rule.value, "tags", null)
abort_incomplete_multipart_upload_days = lookup(rule.value, "abort_incomplete_multipart_upload_days", null)
enabled = rule.value.enabled

dynamic "expiration" {
for_each = length(keys(lookup(rule.value, "expiration", {}))) == 0 ? [] : [rule.value.expiration]

content {
date = lookup(expiration.value, "date", null)
days = lookup(expiration.value, "days", null)
expired_object_delete_marker = lookup(expiration.value, "expired_object_delete_marker", null)
}
}

dynamic "transition" {
for_each = lookup(rule.value, "transition", [])

content {
date = lookup(transition.value, "date", null)
days = lookup(transition.value, "days", null)
storage_class = transition.value.storage_class
}
}

dynamic "noncurrent_version_expiration" {
for_each = length(keys(lookup(rule.value, "noncurrent_version_expiration", {}))) == 0 ? [] : [rule.value.noncurrent_version_expiration]
iterator = expiration

content {
days = lookup(expiration.value, "days", null)
}
}

dynamic "noncurrent_version_transition" {
for_each = lookup(rule.value, "noncurrent_version_transition", [])
iterator = transition

content {
days = lookup(transition.value, "days", null)
storage_class = transition.value.storage_class
}
}
}
}
}

locals {
bucket_id = join("", aws_s3_bucket.bucket.*.id)
bucket_arn = join("", aws_s3_bucket.bucket.*.arn)

cross_account_bucket_actions_enabled = length(var.cross_account_bucket_actions) > 0
cross_account_object_actions_enabled = length(var.cross_account_object_actions) > 0
cross_account_object_actions_with_forced_acl_enabled = length(var.cross_account_object_actions_with_forced_acl) > 0

cross_account_actions_enabled = local.cross_account_bucket_actions_enabled || local.cross_account_object_actions_enabled || local.cross_account_object_actions_with_forced_acl_enabled

cross_account_enabled = length(var.cross_account_identifiers) > 0 && local.cross_account_actions_enabled

policy_enabled = var.create && (var.policy != null || local.cross_account_enabled)
}

resource "aws_s3_bucket_public_access_block" "bucket" {
count = var.create ? 1 : 0

bucket = local.bucket_id

block_public_acls = var.block_public_acls
block_public_policy = var.block_public_policy
ignore_public_acls = var.ignore_public_acls
restrict_public_buckets = var.restrict_public_buckets
}

resource "aws_s3_bucket_policy" "bucket" {
count = local.policy_enabled ? 1 : 0

depends_on = [
aws_s3_bucket_public_access_block.bucket
]

bucket = local.bucket_id
policy = join("", data.aws_iam_policy_document.bucket.*.json)
}

data "aws_iam_policy_document" "bucket" {
count = local.policy_enabled ? 1 : 0

source_json = var.policy

dynamic "statement" {
for_each = length(var.cross_account_bucket_actions) == 0 ? [] : [1]

content {
actions = var.cross_account_bucket_actions
resources = [local.bucket_arn]

principals {
type = "AWS"
identifiers = var.cross_account_identifiers
}
}
}

dynamic "statement" {
for_each = length(var.cross_account_object_actions) == 0 ? [] : [1]

content {
actions = var.cross_account_object_actions
resources = ["${local.bucket_arn}/*"]

principals {
type = "AWS"
identifiers = var.cross_account_identifiers
}
}
}

dynamic "statement" {
for_each = length(var.cross_account_object_actions_with_forced_acl) == 0 ? [] : [1]

content {
actions = var.cross_account_object_actions_with_forced_acl
resources = ["${local.bucket_arn}/*"]

principals {
type = "AWS"
identifiers = var.cross_account_identifiers
}

dynamic "condition" {
for_each = length(var.cross_account_forced_acls) == 0 ? [] : [1]

content {
test = "StringEquals"
variable = "s3:x-amz-acl"
values = var.cross_account_forced_acls
}
}
}
}
}
29 changes: 29 additions & 0 deletions outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
output "id" {
description = "The name of the bucket."
value = join("", aws_s3_bucket.bucket.*.id)
}

output "arn" {
description = "The ARN of the bucket."
value = join("", aws_s3_bucket.bucket.*.arn)
}

output "bucket_domain_name" {
description = "The domain name of the bucket."
value = join("", aws_s3_bucket.bucket.*.bucket_domain_name)
}

output "bucket_regional_domain_name" {
description = "The region-specific domain name of the bucket."
value = join("", aws_s3_bucket.bucket.*.bucket_regional_domain_name)
}

output "hosted_zone_id" {
description = "The Route 53 Hosted Zone ID for this bucket's region."
value = join("", aws_s3_bucket.bucket.*.hosted_zone_id)
}

output "region" {
description = "The AWS region this bucket resides in."
value = join("", aws_s3_bucket.bucket.*.region)
}
Loading

0 comments on commit 68f32e9

Please sign in to comment.