# s3-bucket

Creates a hardened S3 bucket with encryption, versioning, lifecycle rules, access logging, and optional cross-region replication.

Provisions an S3 bucket with all public access blocked by default, server-side encryption (aws:kms or AES256), object versioning, configurable lifecycle transitions and expirations, access log delivery to a target bucket, and optional cross-region replication with per-rule destination encryption keys.

## Usage

### Minimal

The smallest valid call. Versioning, SSE-KMS with the AWS-managed key, `BucketOwnerEnforced` ownership, and a fully-blocking public access block are all applied by default.

```hcl
module "minimal_bucket" {
  source  = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
  version = "0.1.0"

  bucket_name = "my-org-minimal-bucket"
}
```

### Customer-managed KMS key

Recommended for most HCA workloads. Passing an explicit `kms_key_id` also means the module's `kms_key_id` output actually returns a usable ARN — see the [Known limitations](#known-limitations) note below on why the AWS-managed key case differs.

```hcl
module "data_bucket" {
  source  = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
  version = "0.1.0"

  bucket_name = "my-org-data-bucket"
  kms_key_id  = module.kms.key_arn

  lifecycle_rules = {
    archive = {
      transitions = [
        { days = 90, storage_class = "STANDARD_IA" },
        { days = 365, storage_class = "GLACIER" },
      ]
    }
  }
}
```

### Lifecycle rules with expiration

Each transition's `days` must be less than the rule's `expiration_days` when both are set — a transition scheduled to happen after the object has already expired is rejected at `terraform validate`, not silently accepted as dead configuration.

```hcl
module "logs_bucket" {
  source  = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
  version = "0.1.0"

  bucket_name = "my-org-app-logs"

  lifecycle_rules = {
    standard_tiering = {
      prefix                                 = "logs/"
      abort_incomplete_multipart_upload_days = 7
      noncurrent_version_expiration_days     = 90
      expiration_days                        = 365
      transitions = [
        { days = 30, storage_class = "STANDARD_IA" },
        { days = 90, storage_class = "GLACIER" },
        { days = 180, storage_class = "DEEP_ARCHIVE" },
      ]
    }
    short_lived_temp = {
      prefix          = "tmp/"
      expiration_days = 14
      # transitions omitted -> object simply expires at day 14, no tiering
    }
  }
}
```

### Access logging

```hcl
module "app_bucket" {
  source  = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
  version = "0.1.0"

  bucket_name            = "my-org-app-bucket"
  logging_target_bucket  = "my-org-access-logs"
  logging_target_prefix  = "app-bucket-logs/"
}
```

`logging_target_bucket` must already exist and accept log delivery writes — this module does not validate its format, existence, or permissions. See [Known limitations](#known-limitations).

### Cross-region / cross-account replication

Replication requires `versioning_enabled = true` (enforced at `terraform validate`) and a pre-existing IAM role with the standard S3 replication trust policy and permissions.

```hcl
module "replicated_bucket" {
  source  = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
  version = "0.1.0"

  bucket_name           = "my-org-primary-bucket"
  versioning_enabled    = true
  replication_role_arn  = "arn:aws:iam::123456789012:role/s3-replication-role"

  replication_rules = {
    same_region_dr = {
      destination_bucket_arn = "arn:aws:s3:::my-org-dr-bucket"
      priority                = 1
    }
    cross_account_archive = {
      destination_bucket_arn = "arn:aws:s3:::my-org-archive-bucket"
      destination_account_id = "234567890123"
      destination_kms_key_id = "arn:aws:kms:us-east-1:234567890123:key/abcd1234-ef56-7890-ab12-cd34ef567890"
      priority                = 2
    }
  }
}
```

Every rule needs a unique `priority` — duplicate priorities across rules in the same `replication_rules` map are rejected at `terraform validate`.

### Object Lock (compliance / WORM)

`object_lock_enabled` can only be set at bucket creation and can never be disabled afterward. It also requires `versioning_enabled = true`.

```hcl
module "compliance_bucket" {
  source  = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
  version = "0.1.0"

  bucket_name         = "my-org-compliance-records"
  versioning_enabled  = true
  object_lock_enabled = true

  object_lock_default_retention = {
    mode = "GOVERNANCE"
    days = 365
  }
}
```

> [!WARNING]
> A bucket with Object Lock enabled and objects written to it **cannot be destroyed** until each object's retention period expires (`GOVERNANCE` mode can be bypassed with `s3:BypassGovernanceRetention` permission and explicit action; `COMPLIANCE` mode cannot be bypassed at all, by anyone, including the account root). An _empty_ Object Lock bucket can still be destroyed normally. Do not apply this configuration against a bucket you intend to tear down casually — test with a plain bucket first.

### Restrictive public access overrides

All four public access block settings default to `true` (fully blocking). Override only when you have a specific, reviewed reason — for example, a bucket serving public static assets behind CloudFront with OAC, where the bucket policy (not ACLs) governs access.

```hcl
module "public_assets_bucket" {
  source  = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
  version = "0.1.0"

  bucket_name          = "my-org-public-assets"
  object_ownership     = "BucketOwnerEnforced"
  block_public_acls    = true
  block_public_policy  = false
  ignore_public_acls   = true
  restrict_public_buckets = false

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid       = "PublicReadGetObject"
      Effect    = "Allow"
      Principal = "*"
      Action    = "s3:GetObject"
      Resource  = "arn:aws:s3:::my-org-public-assets/*"
    }]
  })
}
```

### Full example

Combines encryption, lifecycle, logging, tags, and a custom object ownership mode in one call.

```hcl
module "full_example" {
  source  = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
  version = "0.1.0"

  bucket_name        = "my-org-full-example"
  versioning_enabled = true

  sse_algorithm      = "aws:kms"
  kms_key_id         = module.kms.key_arn
  bucket_key_enabled = true

  object_ownership = "BucketOwnerEnforced"

  logging_target_bucket = "my-org-access-logs"
  logging_target_prefix = "full-example/"

  lifecycle_rules = {
    standard_tiering = {
      prefix                                 = "data/"
      abort_incomplete_multipart_upload_days = 7
      noncurrent_version_expiration_days     = 90
      expiration_days                        = 730
      transitions = [
        { days = 30, storage_class = "STANDARD_IA" },
        { days = 180, storage_class = "GLACIER" },
      ]
    }
  }

  tags = {
    environment = "production"
    owner       = "platform-team"
  }
}
```

## Known limitations

- **`kms_key_id` output is empty when using the AWS-managed key.** When `kms_key_id` is left unset (the default), AWS automatically encrypts objects with the account's default `aws/s3` managed key — but the AWS provider does not read that resolved key ARN back into Terraform state, so the module's `kms_key_id` output returns an empty string in that case. It only returns a usable value when you supply your own `kms_key_id` explicitly. If a downstream consumer needs the key ARN (for example, to grant `kms:Decrypt` to another principal), pass a customer-managed key rather than relying on the AWS-managed default.
- **`mfa_delete` is not exposed.** Enabling MFA Delete requires a live MFA-authenticated session (root or IAM user), which is incompatible with this platform's OIDC/assumed-role pipeline authentication. If MFA Delete is required on a specific bucket, it must be enabled manually, out-of-band, by someone with root or IAM-user credentials and a physical/virtual MFA device — Terraform will not manage that attribute.
- **`logging_target_bucket` is not format- or existence-validated.** The module does not check that the target bucket exists, is in a compatible region, or has the correct log-delivery permissions. These are AWS API-level failures at `apply` time, not something format validation can catch, so none is applied.
- **Replication priority uniqueness is stricter than AWS technically requires.** AWS only needs unique priorities among rules whose prefixes can overlap on the same object. This module enforces uniqueness across _all_ rules in a `replication_rules` map regardless of prefix overlap, since prefix-overlap analysis isn't validated either. If you have a legitimate case for two non-overlapping rules sharing a priority, this module will currently reject it.

<!-- BEGIN_TF_DOCS — DO NOT EDIT BELOW THIS LINE -->

## Requirements

| Name                                                                     | Version            |
| ------------------------------------------------------------------------ | ------------------ |
| <a name="requirement_terraform"></a> [terraform](#requirement_terraform) | >= 1.15.0, < 2.0.0 |
| <a name="requirement_aws"></a> [aws](#requirement_aws)                   | >= 6.0, < 7.0      |

## Providers

| Name                                             | Version       |
| ------------------------------------------------ | ------------- |
| <a name="provider_aws"></a> [aws](#provider_aws) | >= 6.0, < 7.0 |

## Resources

| Name                                                                                                                                                                                  | Type     |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| [aws_s3_bucket.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket)                                                                           | resource |
| [aws_s3_bucket_lifecycle_configuration.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_lifecycle_configuration)                           | resource |
| [aws_s3_bucket_logging.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_logging)                                                           | resource |
| [aws_s3_bucket_object_lock_configuration.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_object_lock_configuration)                       | resource |
| [aws_s3_bucket_ownership_controls.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_ownership_controls)                                     | resource |
| [aws_s3_bucket_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy)                                                             | resource |
| [aws_s3_bucket_public_access_block.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block)                                   | resource |
| [aws_s3_bucket_replication_configuration.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_replication_configuration)                       | resource |
| [aws_s3_bucket_server_side_encryption_configuration.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource |
| [aws_s3_bucket_versioning.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning)                                                     | resource |

## Inputs

| Name                                                                                                                     | Description                                                                                                                                                           | Type                                                                                                                                                                                                                                                                                                                                                                                            | Default                 | Required |
| ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | :------: |
| <a name="input_bucket_name"></a> [bucket_name](#input_bucket_name)                                                       | Globally unique S3 bucket name                                                                                                                                        | `string`                                                                                                                                                                                                                                                                                                                                                                                        | n/a                     |   yes    |
| <a name="input_block_public_acls"></a> [block_public_acls](#input_block_public_acls)                                     | Block public ACLs                                                                                                                                                     | `bool`                                                                                                                                                                                                                                                                                                                                                                                          | `true`                  |    no    |
| <a name="input_block_public_policy"></a> [block_public_policy](#input_block_public_policy)                               | Block public bucket policies                                                                                                                                          | `bool`                                                                                                                                                                                                                                                                                                                                                                                          | `true`                  |    no    |
| <a name="input_bucket_key_enabled"></a> [bucket_key_enabled](#input_bucket_key_enabled)                                  | Enable S3 bucket key to reduce KMS API calls                                                                                                                          | `bool`                                                                                                                                                                                                                                                                                                                                                                                          | `true`                  |    no    |
| <a name="input_force_destroy"></a> [force_destroy](#input_force_destroy)                                                 | Allow destruction of non-empty bucket                                                                                                                                 | `bool`                                                                                                                                                                                                                                                                                                                                                                                          | `false`                 |    no    |
| <a name="input_ignore_public_acls"></a> [ignore_public_acls](#input_ignore_public_acls)                                  | Ignore public ACLs                                                                                                                                                    | `bool`                                                                                                                                                                                                                                                                                                                                                                                          | `true`                  |    no    |
| <a name="input_kms_key_id"></a> [kms_key_id](#input_kms_key_id)                                                          | KMS key ARN for server-side encryption (null uses aws:kms with managed key)                                                                                           | `string`                                                                                                                                                                                                                                                                                                                                                                                        | `null`                  |    no    |
| <a name="input_lifecycle_rules"></a> [lifecycle_rules](#input_lifecycle_rules)                                           | Map of lifecycle rules to apply                                                                                                                                       | <pre>map(object({<br/> enabled = optional(bool, true)<br/> prefix = optional(string, null)<br/> abort_incomplete_multipart_upload_days = optional(number, 7)<br/> expiration_days = optional(number, null)<br/> noncurrent_version_expiration_days = optional(number, 90)<br/> transitions = optional(list(object({<br/> days = number<br/> storage_class = string<br/> })), [])<br/> }))</pre> | `{}`                    |    no    |
| <a name="input_logging_target_bucket"></a> [logging_target_bucket](#input_logging_target_bucket)                         | S3 bucket name to receive access logs (optional)                                                                                                                      | `string`                                                                                                                                                                                                                                                                                                                                                                                        | `null`                  |    no    |
| <a name="input_logging_target_prefix"></a> [logging_target_prefix](#input_logging_target_prefix)                         | Prefix for access log objects                                                                                                                                         | `string`                                                                                                                                                                                                                                                                                                                                                                                        | `"logs/"`               |    no    |
| <a name="input_object_lock_default_retention"></a> [object_lock_default_retention](#input_object_lock_default_retention) | Default retention rule applied to objects when Object Lock is<br/>enabled. mode must be 'GOVERNANCE' or 'COMPLIANCE'. Only used<br/>when object_lock_enabled is true. | <pre>object({<br/> mode = string<br/> days = number<br/> })</pre>                                                                                                                                                                                                                                                                                                                               | `null`                  |    no    |
| <a name="input_object_lock_enabled"></a> [object_lock_enabled](#input_object_lock_enabled)                               | Enable S3 Object Lock on the bucket. Can only be set at bucket<br/>creation, cannot be enabled on an existing bucket. Requires<br/>versioning_enabled = true.         | `bool`                                                                                                                                                                                                                                                                                                                                                                                          | `false`                 |    no    |
| <a name="input_object_ownership"></a> [object_ownership](#input_object_ownership)                                        | Object ownership                                                                                                                                                      | `string`                                                                                                                                                                                                                                                                                                                                                                                        | `"BucketOwnerEnforced"` |    no    |
| <a name="input_policy"></a> [policy](#input_policy)                                                                      | JSON bucket policy document (optional)                                                                                                                                | `string`                                                                                                                                                                                                                                                                                                                                                                                        | `null`                  |    no    |
| <a name="input_replication_role_arn"></a> [replication_role_arn](#input_replication_role_arn)                            | IAM role ARN for S3 replication (required when replication_rules is set)                                                                                              | `string`                                                                                                                                                                                                                                                                                                                                                                                        | `null`                  |    no    |
| <a name="input_replication_rules"></a> [replication_rules](#input_replication_rules)                                     | Map of replication rules                                                                                                                                              | <pre>map(object({<br/> destination_bucket_arn = string<br/> destination_kms_key_id = optional(string, null)<br/> destination_account_id = optional(string, null)<br/> prefix = optional(string, "")<br/> priority = optional(number, 1)<br/> delete_marker_replication = optional(bool, true)<br/> }))</pre>                                                                                    | `{}`                    |    no    |
| <a name="input_restrict_public_buckets"></a> [restrict_public_buckets](#input_restrict_public_buckets)                   | Restrict public bucket access                                                                                                                                         | `bool`                                                                                                                                                                                                                                                                                                                                                                                          | `true`                  |    no    |
| <a name="input_sse_algorithm"></a> [sse_algorithm](#input_sse_algorithm)                                                 | SSE algorithm (aws:kms or AES256)                                                                                                                                     | `string`                                                                                                                                                                                                                                                                                                                                                                                        | `"aws:kms"`             |    no    |
| <a name="input_tags"></a> [tags](#input_tags)                                                                            | Resource tags to apply to all resources                                                                                                                               | `map(string)`                                                                                                                                                                                                                                                                                                                                                                                   | `{}`                    |    no    |
| <a name="input_versioning_enabled"></a> [versioning_enabled](#input_versioning_enabled)                                  | Enable object versioning                                                                                                                                              | `bool`                                                                                                                                                                                                                                                                                                                                                                                          | `true`                  |    no    |

## Outputs

| Name                                                                                                                    | Description                                                |
| ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| <a name="output_arn"></a> [arn](#output_arn)                                                                            | S3 bucket ARN                                              |
| <a name="output_bucket_domain_name"></a> [bucket_domain_name](#output_bucket_domain_name)                               | Bucket-style domain name                                   |
| <a name="output_bucket_regional_domain_name"></a> [bucket_regional_domain_name](#output_bucket_regional_domain_name)    | Regional domain name (for CloudFront origins)              |
| <a name="output_hosted_zone_id"></a> [hosted_zone_id](#output_hosted_zone_id)                                           | Route 53 hosted zone ID for the bucket's region            |
| <a name="output_id"></a> [id](#output_id)                                                                               | S3 bucket name                                             |
| <a name="output_kms_key_id"></a> [kms_key_id](#output_kms_key_id)                                                       | KMS key ARN actually applied for SSE-KMS encryption        |
| <a name="output_object_lock_enabled"></a> [object_lock_enabled](#output_object_lock_enabled)                            | Whether S3 Object Lock is enabled on the bucket            |
| <a name="output_replication_configuration_id"></a> [replication_configuration_id](#output_replication_configuration_id) | Replication configuration ID, if replication is configured |
| <a name="output_versioning_status"></a> [versioning_status](#output_versioning_status)                                  | Effective versioning status of the bucket                  |

<!-- END_TF_DOCS — DO NOT EDIT ABOVE THIS LINE -->
