Skip to content

Latest commit

 

History

History
273 lines (200 loc) · 26 KB

File metadata and controls

273 lines (200 loc) · 26 KB

Fixit Cloud ☁️ Module: AWS S3

Terraform module for defining an S3 Bucket with opinionated configurations that reflect security best-practices as well as sensible cost-reduction strategies.

Table of Contents

Lifecycle Rules vs MFA-Delete

AWS does not currently offer the ability for S3 buckets to have both lifecycle-rule configurations and MFA-delete enabled. This mutual exclusivity ostensibly presents users with a truly difficult tradeoff: automated cost-saving functionality on the one hand, and security best-practices on the other. However, with a little elbow grease we can emulate these features, thereby allowing us to have our cost-saving cake and securely eat it too. To help determine which feature to natively enable and which to emulate, consider the solutions outlined below.

Solution A: Emulating S3 Lifecycle Rules

Two AWS/S3-native utilities offer a close facsimile to lifecycle-rule configs: AWS Lambda Functions, and S3 Batch Operations.

Solution B: Emulating S3 MFA-Delete

For many organizations, MFA-delete is almost certainly the simplest to emulate, since requiring MFA for delete operations can (with caveats) be achieved via the use of the "aws:MultiFactorAuthPresent" condition key in bucket-policy statements like the example below. However, the "aws:MultiFactorAuthPresent" condition key is not present in requests made using long-term credentials. If long-term credentials are not prohibited by account/organization policies, the below example can be amended to use "BoolIfExists" rather than "Bool", but such a configuration obviously opens the door to non-MFA delete operations via the use of long-term credentials.

Example Bucket-Policy with "aws:MultiFactorAuthPresent" Condition Key
{
  "Effect": "Deny",
  "Principal": { "AWS": "*" },
  "Action": ["s3:DeleteObject", "s3:DeleteObjectVersion"],
  "Resource": "arn:aws:s3:::example_bucket/*",
  "Condition": {
    "Bool": {
      //
      // Using "Bool" instead of "BoolIfExists" will ensure that MFA is always used. However, be aware
      // that since the MFA condition keys are NOT present on requests made using long-term credentials,
      // this will cause any "s3:DeleteObject" calls made using long-term credentials to fail.
      //
      "aws:MultiFactorAuthPresent": "false", // Checks whether MFA was used to validate the request credentials.
      "aws:MultiFactorAuthAge": 3600 // Optionally specify a max age on the request credentials, in seconds.
    }
  }
}

Another potential issue with emulating MFA-delete would be the constant triggering of SecurityHub findings related to the standard CIS AWS Foundations Benchmark # 2.1.3, "Ensure MFA Delete is enabled on S3 buckets". If permitted, the command below can be used to disable findings related to this benchmark. However, additional measures would then need to be taken to ensure that every S3 is created with the "aws:MultiFactorAuthPresent" condition key in its bucket policy.

aws securityhub update-standards-control \
  --standards-control-arn "arn:aws:securityhub:REGION:ACCOUNT_NUM:control/cis-aws-foundations-benchmark/v/1.2.0/2.1.3*" \
  --control-status "DISABLED" \
  --disabled-reason "MFA-delete is enforced by S3 bucket policies."

Opinionated Security Configurations

Object Ownership: "BucketOwnerEnforced"

Before S3 bucket policies and object-ownership controls, access control lists were the primary means of controlling access to buckets/objects. Since ACLs offer less functionality and flexibility (e.g., ALLOW but no DENY), this module implements the "BucketOwnerEnforced" Object-Ownership control setting for all buckets, thereby providing guaranteed object ownership to the bucket owner, while also requiring the burden of access-control be managed by policy mechanisms which feature greater utility, such as S3 bucket policies, IAM policies, VPC endpoint policies, and AWS Organizations SCPs.

Relevant Security Standards:

Block Public Access

This is comprised of a group of four settings:

  1. block_public_acls: true
  2. block_public_policy: true
  3. restrict_public_buckets: true
  4. ignore_public_acls: true

As stated in CIS Amazon Web Services Foundations Benchmark v1.4.0, #2.1.5:

Amazon S3 provides Block Public Access to help you manage public access to Amazon S3 resources. By default, S3 buckets and objects are created with public access disabled. However, an IAM principal with sufficient S3 permissions can enable public access at the bucket and/or object level. While enabled, Block Public Access prevents an individual bucket, and its contained objects, from becoming publicly accessible. Amazon S3 Block Public Access prevents the accidental or malicious public exposure of data contained within the respective bucket(s).

Bucket Versioning: "Enabled"

Object Lock: "Enabled"

Server-Side Encryption

Server-side encryption by default is enforced for all buckets. Users may choose to use S3-managed keys (SSE-S3), or use their own KMS key (SSE-KMS).

Whichever method is used,

Comparison: SSE-S3 vs SSE-KMS

Factor SSE-S3 SSE-KMS
Cost Almost $0 KMS-related charges
Management Overhead None KMS-related permissions
Can Audit NO YES
Cross-Account Object Access NO YES, if permitted by the key policy

Cost: Requests to configure the default encryption feature incur standard Amazon S3 request charges. According to AWS S3 pricing data, the price of such requests for a Standard-class S3 in the us-east-2 region runs $0.0004 per 1,000 requests.

Bucket Key: "Enabled"


⚙️ Module Usage

Usage Examples

Requirements

Name Version
terraform 1.3.2
aws ~> 4.34.0

Providers

Name Version
aws ~> 4.34.0

Modules

No modules.

Resources

Name Type
aws_s3_bucket.this resource
aws_s3_bucket_accelerate_configuration.list resource
aws_s3_bucket_cors_configuration.list resource
aws_s3_bucket_lifecycle_configuration.list resource
aws_s3_bucket_logging.list resource
aws_s3_bucket_object_lock_configuration.list resource
aws_s3_bucket_ownership_controls.this resource
aws_s3_bucket_policy.this resource
aws_s3_bucket_public_access_block.this resource
aws_s3_bucket_replication_configuration.list resource
aws_s3_bucket_server_side_encryption_configuration.this resource
aws_s3_bucket_versioning.this resource
aws_s3_bucket_website_configuration.list resource

Inputs

Name Description Type Default Required
access_logs_config Config object for logging bucket access events. This variable should
only be excluded if the bucket being created will itself be designated
to receive access logs from other buckets (in which case, do not provide
a value for var.sse_kms_config - access logs buckets must use SSE-S3).
"bucket_name" must be the name of an existing S3 bucket to which to send
access logs. By default, "access_logs_prefix" will be set to the
var.bucket_name value.
object({
bucket_name = string
access_logs_prefix = optional(string)
})
null no
bucket_name The name of the S3 bucket. Warning: this value cannot be changed
after bucket creation - changing this input will therefore force
the creation of a new bucket. The list of bucket-naming rules is
available at the link below.
https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html.
string n/a yes
bucket_policy A JSON-encoded string which can be properly decoded into a valid IAM
bucket policy for the bucket.
string n/a yes
bucket_tags The tags of the S3 bucket. map(string) null no
cors_rules Config map for CORS rules; map rule IDs/names to objects which define
them. The map keys (rule IDs/names) cannot be longer than 255 characters.
For "allowed_methods", valid values are GET, PUT, HEAD, POST, and DELETE.
HTTP headers included in "allowed_headers" will be specified in the
"Access-Control-Request-Headers" header. "max_age_seconds" sets the time
in seconds that a browser is to cache the preflight response for the
specified resource.
map(object({
allowed_methods = list(string)
allowed_origins = list(string)
allowed_headers = optional(list(string))
expose_headers = optional(list(string))
max_age_seconds = number
}))
null no
lifecycle_rules Map of lifecycle rule config objects. If the bucket must utilize the
MFA-delete feature, simply exclude this variable. Each key should be
the name of a lifecycle rule with a value set to an object with rule
config properties. Please refer to the relevant resource docs (link
below) in TF Registry for info regarding the available rule params:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_lifecycle_configuration#argument-reference.
map(
# map keys: rule names
object({
filter = optional(object({
prefix = optional(string)
object_size_greater_than = optional(number) # Min size in bytes to which the rule applies.
object_size_less_than = optional(number) # Max size in bytes to which the rule applies.
tag = optional(object({ key = string, value = string }))
and = optional(object({
prefix = optional(string)
object_size_greater_than = optional(number)
object_size_less_than = optional(number)
tags = optional(map(string))
}))
}))
abort_incomplete_multipart_upload = optional(object({
days_after_initiation = number
}))
transition = optional(object({
date = optional(string) # If provided, must be in RFC 3339 format.
days = optional(number) # If provided, must a non-zero positive integer.
storage_class = string
}))
expiration = optional(object({
date = optional(string)
days = optional(number)
expired_object_delete_marker = optional(bool)
}))
noncurrent_version_transition = optional(object({
newer_noncurrent_versions = optional(number)
noncurrent_days = optional(number)
storage_class = string
}))
noncurrent_version_expiration = optional(object({
newer_noncurrent_versions = number
noncurrent_days = number
}))
status = optional(string) # Defaults to "Enabled", can optionally pass "Disabled".
})
)
null no
mfa_delete_config Config object for the MFA-delete feature. If the bucket will instead
enable the lifecycle-rule configs feature, simply exclude this variable.
object({
auth_device_serial_number = string
auth_device_code_value = string
})
null no
object_lock_default_retention Config object to set the bucket's default object-lock retention policy.
The "mode" property can be either "COMPLIANCE" or "GOVERNANCE". For the
retention period duration, supply a number to either "days" or "years",
but not both.
object({
mode = string
days = optional(number)
years = optional(number)
})
null no
replication_config Config object for replication of bucket objects. For the "rules" property, map each
rule name to an object configuring the rule. Rule names must be less than or equal
to 255 characters.

Rule Properties: Each rule's "priority" should be a number unique among all provided
rules; these numbers determine which rule's filter takes precedence, with higher
numbers taking priority (if no rules contain filters, "priority" values have no effect).
"is_enabled" defaults to "true" if not provided. The "should_replicate ..." properties
all default to "true" if not provided.

Destination Properties: In the "destination" config, "should_destination_become_owner"
defaults to "true" if not provided. "should_enable_metrics" defaults to "false"; if
metrics are enabled, the S3 service will send replication metrics to CloudWatch. If the
destination bucket uses SSE-KMS, the ARN of the dest bucket's encryption key must be
provided via the "replica_kms_key_arn" property. "should_replication_complete_within_15_minutes"
defaults to "false"; when set to "true", the S3 service will create S3 Event Notifications
if the replication process fails and/or takes longer than 15 minutes (the amount of time
is not currently customizable). By default, if "storage_class" is not specified, the S3
service uses the storage class of the source object to create the replica; valid override
values are "STANDARD_IA", "INTELLIGENT_TIERING", "ONEZONE_IA", "GLACIER_IR", "GLACIER",
and "DEEP_ARCHIVE".
object({
s3_replication_service_role_arn = string
rules = map(object({
priority = number
is_enabled = optional(bool)
should_replicate_delete_markers = optional(bool)
should_replicate_existing_objects = optional(bool)
should_replicate_encrypted_objects = optional(bool)
filter = optional(object({
prefix = optional(string)
tag = optional(object({ key = string, value = string }))
and = optional(object({
prefix = optional(string)
tags = optional(map(string))
}))
}))
destination = object({
account = optional(string)
bucket_arn = string
storage_class = optional(string)
replica_kms_key_arn = optional(string) # dest bucket SSE-KMS key ARN
should_destination_become_owner = optional(bool)
should_enable_metrics = optional(bool)
should_replication_complete_within_15_minutes = optional(bool)
})
}))
})
null no
sse_kms_config Config object to utilize an existing KMS key for server-side encryption
(SSE-by-default is enabled on all buckets created by this module). If not
provided, the AWS-managed SSE-S3 key will be used instead. If a KMS key
ARN is provided, it is highly recommended to permit the bucket to use
the key as a Bucket-Key, thereby reducing KMS-related costs by more than
90% on average. This is not enforced by default, however, since enabling
the Bucket-Key feature may require refactoring of the KMS key policy (S3
encryption context conditions for Bucket-Keys must point to the ARN of
the BUCKET - not the ARN of OBJECTS, as is the case for non-Bucket-Key
SSE-KMS keys). A KMS key alias may be provided in lieu of an ARN, but this
is highly discouraged if the bucket may be used in cross-account operations,
as the KMS service will resolve the key within the REQUESTER'S account,
which can result in data encrypted with a KMS key that belongs to the
requester, and not the bucket administrator. Please be aware that SSE-KMS
may NOT be used for access-logs buckets.
object({
key_arn = string
should_enable_bucket_key = bool
})
null no
transfer_acceleration Set to "Enabled" to enable transfer acceleration. May also be set to
"Suspended" after transfer acceleration has been enabled to temporarily
turn off transfer acceleration.
string null no
web_host_config Config object for setting up the bucket as a web host. Either "routing" OR
"redirect_all_requests_to" must be specified, but not both. The property
"routing.redirect_rules", if provided, must map names of routing-rules to
objects which define them. The "replace" property may use either "key_with"
OR "key_prefix_with", but not both. The two "protocol" properties can be
either "http" or "https"; if not provided (null) the default behavior will
be to use the protocol used in the original request.
object({
routing = optional(object({
index_document = string
error_document = optional(string)
redirect_rules = optional(map(object({
host_name = optional(string) # The host name to use in the redirect request
protocol = optional(string) # http / https / null
http_redirect_code = optional(string) # The HTTP redirect code to use on the response
replace = optional(object({
key_prefix_with = optional(string) # docs/ --> documents/
key_with = optional(string) # foo.html --> error.html
}))
condition = optional(object({
http_error_code_returned_equals = optional(string) # "404"
key_prefix_equals = optional(string) # docs/
}))
})))
}))
redirect_all_requests_to = optional(object({
host_name = string
protocol = optional(string) # http / https / null
}))
})
null no

Outputs

Name Description
S3_Bucket The S3 bucket resource.
S3_Bucket_CORS_Config The S3 bucket CORS config resource.
S3_Bucket_Lifecycle_Config Map of S3 bucket lifecycle rule config resources.
S3_Bucket_Logging_Config The S3 bucket access logging config resource.
S3_Bucket_Object_Lock_Default_Retention_Config The S3 bucket's object-lock default retention config resource.
S3_Bucket_Policy The S3 bucket policy resource.
S3_Bucket_Public_Access_Block The S3 bucket public access block config resource.
S3_Bucket_Replication_Config The S3 bucket replication config resource.
S3_Bucket_SSE_Config The S3 bucket SSE config resource.
S3_Bucket_Transfer_Acceleration_Config The S3 bucket transfer acceleration config resource.
S3_Bucket_Versioning_Config The S3 bucket versioning config resource.
S3_Bucket_Web_Host_Config The S3 bucket web host config resource.
S3_Buket_Ownership_Controls The S3 bucket ownership controls config resource.

📝 License

All scripts and source code contained herein are for commercial use only by Nerdware, LLC.

See LICENSE for more information.

💬 Contact

Trevor Anderson - @TeeRevTweets - Trevor@Nerdware.cloud

     

Dare Mighty Things.

Contributing / Roadmap

As both AWS S3 and associated Terraform resources change over time, so too must this module. If you don't yet see a feature you'd like, feel free to drop a pull request. Please note, however, that features which run contrary to the module's opinionated configs will not be implemented unless a fundamental change has been implemented by AWS or Terraform in the underlying services/components which significantly alters the circumstances behind the reasoning for a given config.

Features in the Pipeline:

  • S3 Bucket Analytics Config
  • S3 Bucket Intelligent-Tiering Config
  • S3 Bucket Inventory Config
  • S3 Bucket Metrics
  • S3 Bucket Notifications
  • S3 Objects/Object-Copies