Terraform module for defining an S3 Bucket with opinionated configurations that reflect security best-practices as well as sensible cost-reduction strategies.
- Lifecycle Rules vs MFA-Delete
- Opinionated Security Configurations
- ⚙️ Module Usage
- 📝 License
- 💬 Contact
- Contributing / Roadmap
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.
Two AWS/S3-native utilities offer a close facsimile to lifecycle-rule configs: AWS Lambda Functions, and S3 Batch Operations.
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."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:
- AWS Foundational Security Best Practices
This is comprised of a group of four settings:
block_public_acls: trueblock_public_policy: truerestrict_public_buckets: trueignore_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).
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,
| 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.
- Terragrunt: view Terragrunt usage exmaple
- Terraform: view vanilla Terraform usage exmaple
| Name | Version |
|---|---|
| terraform | 1.3.2 |
| aws | ~> 4.34.0 |
| Name | Version |
|---|---|
| aws | ~> 4.34.0 |
No modules.
| 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 |
| 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({ |
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({ |
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( |
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({ |
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({ |
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({ |
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({ |
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({ |
null |
no |
| 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. |
All scripts and source code contained herein are for commercial use only by Nerdware, LLC.
See LICENSE for more information.
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