diff --git a/infrastructure/aws/iam/ci-build-workflow-user/README.md b/infrastructure/aws/iam/ci-build-workflow-user/README.md
new file mode 100644
index 00000000..28889c02
--- /dev/null
+++ b/infrastructure/aws/iam/ci-build-workflow-user/README.md
@@ -0,0 +1,101 @@
+# Module: build-user
+
+## Description
+
+Provisions the shared CI/CD build workflow IAM identity (user, access key, and group) used to publish application assets to any asset repository (ECR, S3, etc.)
+
+## Architecture
+
+The module creates a single `aws_iam_user` with an `aws_iam_access_key` for CI/CD build workflows, plus an `aws_iam_group` named `asset-publishers` and the `aws_iam_user_group_membership` that adds the user to it. The group is the attachment point for per-destination permission modules: `infrastructure/aws/iam/ecr` attaches its ECR policy to this group, and `infrastructure/aws/iam/s3-assets` attaches its S3 policy to the same group. The build user therefore accumulates the permissions of every enabled destination through a single group, which matches how the platform CLI publishes assets (one credential set used for all asset types).
+
+## Features
+
+- Creates a single namespaced `aws_iam_user` and `aws_iam_access_key` for CI/CD build workflow authentication
+- Creates a destination-agnostic `aws_iam_group` (`asset-publishers`) that permission modules attach their policies to
+- Adds the build user to the group via `aws_iam_user_group_membership`
+- Exposes `group_name` so asset-repository modules (`ecr`, `s3-assets`) can grant permissions without recreating the identity
+
+## Basic Usage
+
+```hcl
+module "build_user" {
+ source = "git::https://github.com/nullplatform/tofu-modules.git//infrastructure/aws/iam/build-user?ref=v5.0.0"
+
+ cluster_name = "your-cluster-name"
+}
+```
+
+The `group_name` output is consumed by the asset-repository permission modules:
+
+```hcl
+module "ecr" {
+ source = "git::https://github.com/nullplatform/tofu-modules.git//infrastructure/aws/iam/ecr?ref=v5.0.0"
+
+ cluster_name = "your-cluster-name"
+ build_workflow_group_name = module.build_user.group_name
+}
+
+module "s3_assets" {
+ source = "git::https://github.com/nullplatform/tofu-modules.git//infrastructure/aws/iam/s3-assets?ref=v5.0.0"
+
+ cluster_name = "your-cluster-name"
+ build_workflow_group_name = module.build_user.group_name
+ assets_bucket = "your-assets-bucket"
+}
+```
+
+## Migration from < v5.0.0 (build user previously created by `iam/ecr`)
+
+Before v5.0.0 the build workflow user, its access key and the group lived inside the `iam/ecr`
+module. This module extracts them. To migrate **without rotating the access keys** (which would
+break CI), move the user and its access key in state — the group is renamed
+(`ecr-managers` → `asset-publishers`) and is recreated, which does not affect the user's credentials:
+
+```bash
+tofu state mv 'module.ecr.aws_iam_user.nullplatform_build_workflow_user' \
+ 'module.build_user.aws_iam_user.nullplatform_build_workflow_user'
+
+tofu state mv 'module.ecr.aws_iam_access_key.nullplatform_build_workflow_user_key' \
+ 'module.build_user.aws_iam_access_key.nullplatform_build_workflow_user_key'
+```
+
+After the moves, a `tofu plan` should show **no changes** to the user and access key (only their
+state address moved), the group + membership recreated as `asset-publishers`, and the ECR policy
+re-attached to the new group.
+
+> **Security note:** the build credentials are read by the platform on each CI run (they are not
+> stored as per-repository secrets), so rotating them periodically is a good practice and this
+> module makes it easy — regenerate the access key and let the platform re-read the new value.
+
+
+
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [aws](#provider\_aws) | n/a |
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [aws_iam_access_key.nullplatform_build_workflow_user_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key) | resource |
+| [aws_iam_group.asset_publishers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group) | resource |
+| [aws_iam_user.nullplatform_build_workflow_user](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource |
+| [aws_iam_user_group_membership.asset_publishers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_group_membership) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [cluster\_name](#input\_cluster\_name) | Name of the cluster, used to namespace IAM resource names | `string` | n/a | yes |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [build\_workflow\_access\_key\_id](#output\_build\_workflow\_access\_key\_id) | Access key ID for the CI/CD build workflow IAM user |
+| [build\_workflow\_access\_key\_secret](#output\_build\_workflow\_access\_key\_secret) | Secret access key for the CI/CD build workflow IAM user |
+| [group\_name](#output\_group\_name) | Name of the IAM group that asset-repository permission modules (ecr, s3-assets) attach their policies to. The build workflow user is a member of this group. |
+
diff --git a/infrastructure/aws/iam/ci-build-workflow-user/main.tf b/infrastructure/aws/iam/ci-build-workflow-user/main.tf
new file mode 100644
index 00000000..b5f834fb
--- /dev/null
+++ b/infrastructure/aws/iam/ci-build-workflow-user/main.tf
@@ -0,0 +1,16 @@
+resource "aws_iam_user" "nullplatform_build_workflow_user" {
+ name = "nullplatform-${var.cluster_name}-build-workflow-user"
+}
+
+resource "aws_iam_access_key" "nullplatform_build_workflow_user_key" {
+ user = aws_iam_user.nullplatform_build_workflow_user.name
+}
+
+resource "aws_iam_group" "asset_publishers" {
+ name = "nullplatform-${var.cluster_name}-asset-publishers"
+}
+
+resource "aws_iam_user_group_membership" "asset_publishers" {
+ user = aws_iam_user.nullplatform_build_workflow_user.name
+ groups = [aws_iam_group.asset_publishers.name]
+}
diff --git a/infrastructure/aws/iam/ci-build-workflow-user/outputs.tf b/infrastructure/aws/iam/ci-build-workflow-user/outputs.tf
new file mode 100644
index 00000000..cf4dacf1
--- /dev/null
+++ b/infrastructure/aws/iam/ci-build-workflow-user/outputs.tf
@@ -0,0 +1,15 @@
+output "build_workflow_access_key_id" {
+ description = "Access key ID for the CI/CD build workflow IAM user"
+ value = aws_iam_access_key.nullplatform_build_workflow_user_key.id
+}
+
+output "build_workflow_access_key_secret" {
+ description = "Secret access key for the CI/CD build workflow IAM user"
+ value = aws_iam_access_key.nullplatform_build_workflow_user_key.secret
+ sensitive = true
+}
+
+output "group_name" {
+ description = "Name of the IAM group that asset-repository permission modules (ecr, s3-assets) attach their policies to. The build workflow user is a member of this group."
+ value = aws_iam_group.asset_publishers.name
+}
diff --git a/infrastructure/aws/iam/ci-build-workflow-user/variables.tf b/infrastructure/aws/iam/ci-build-workflow-user/variables.tf
new file mode 100644
index 00000000..c16e2ed1
--- /dev/null
+++ b/infrastructure/aws/iam/ci-build-workflow-user/variables.tf
@@ -0,0 +1,4 @@
+variable "cluster_name" {
+ description = "Name of the cluster, used to namespace IAM resource names"
+ type = string
+}
diff --git a/infrastructure/aws/iam/ecr/README.md b/infrastructure/aws/iam/ecr/README.md
index c23f0491..97581b11 100644
--- a/infrastructure/aws/iam/ecr/README.md
+++ b/infrastructure/aws/iam/ecr/README.md
@@ -2,31 +2,41 @@
## Description
-Provisions IAM resources for ECR image management and optional cross-account ECR pull access within a named cluster namespace
+Provisions IAM resources for ECR image management and optional cross-account ECR pull access within a named cluster namespace. The build workflow identity (user, access key, group) lives in the `ci-build-workflow-user` module; this module only grants ECR permissions to that group.
## Architecture
-The module creates two aws_iam_role resources (an application role with a configurable assume-role principal and an optional cross-account pull role), an aws_iam_policy for ECR management actions, and an aws_iam_user with an aws_iam_access_key for CI/CD build workflows. The ECR manager policy is attached to both the application role via aws_iam_role_policy_attachment and to an aws_iam_group via aws_iam_group_policy_attachment, with the build workflow user added to that group through aws_iam_user_group_membership. When enable_cross_account_pull is true, a separate aws_iam_role and aws_iam_policy scoped to read-only ECR actions are created and linked, with pull_account_ids driving the Principal trust statements.
+The module creates an aws_iam_role (application role with a configurable assume-role principal) and an aws_iam_policy for ECR management actions. The ECR manager policy is attached to the application role via aws_iam_role_policy_attachment and to the shared build-workflow group via aws_iam_group_policy_attachment. The group itself is created by the `ci-build-workflow-user` module and passed in through `build_workflow_group_name`. When enable_cross_account_pull is true, a separate aws_iam_role and aws_iam_policy scoped to read-only ECR actions are created and linked, with pull_account_ids driving the Principal trust statements.
## Features
- Creates a namespaced aws_iam_role for application image pulling with a configurable assume-role principal
- Creates an aws_iam_policy granting full ECR repository lifecycle permissions including push, pull, and repository management
-- Creates an aws_iam_user and aws_iam_access_key for CI/CD build workflow authentication to ECR
-- Creates an aws_iam_group and attaches the ECR manager policy for group-based permission management
+- Attaches the ECR manager policy to the shared build-workflow group (created by the ci-build-workflow-user module) for group-based permission management
- Optionally creates a cross-account aws_iam_role and read-only ECR pull policy for external AWS accounts
- Outputs a ready-to-use ECR repository policy JSON for cross-account pull access configuration
## Basic Usage
```hcl
-module "ecr" {
- source = "git::https://github.com/nullplatform/tofu-modules.git//infrastructure/aws/iam/ecr?ref=v4.6.0"
+module "ci_build_workflow_user" {
+ source = "git::https://github.com/nullplatform/tofu-modules.git//infrastructure/aws/iam/ci-build-workflow-user?ref=v5.0.0"
cluster_name = "your-cluster-name"
}
+
+module "ecr" {
+ source = "git::https://github.com/nullplatform/tofu-modules.git//infrastructure/aws/iam/ecr?ref=v5.0.0"
+
+ cluster_name = "your-cluster-name"
+ build_workflow_group_name = module.ci_build_workflow_user.group_name
+}
```
+> **Migration from < v5.0.0:** the build workflow user, access key and group were previously
+> created by this module. They now live in `ci-build-workflow-user`. See that module's README for the
+> `tofu state mv` steps to migrate without rotating the access keys.
+
## Using Outputs
```hcl
@@ -50,14 +60,10 @@ resource "example_resource" "this" {
| Name | Type |
|------|------|
-| [aws_iam_access_key.nullplatform_build_workflow_user_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key) | resource |
-| [aws_iam_group.nullplatform_ecr_managers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group) | resource |
| [aws_iam_group_policy_attachment.ecr_manager_policy_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group_policy_attachment) | resource |
| [aws_iam_policy.nullplatform_ecr_manager_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
| [aws_iam_role.nullplatform_application_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role_policy_attachment.ecr_manager_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
-| [aws_iam_user.nullplatform_build_workflow_user](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource |
-| [aws_iam_user_group_membership.build_workflow_ecr_managers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_group_membership) | resource |
| [terraform_data.validations](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource |
## Inputs
@@ -65,6 +71,7 @@ resource "example_resource" "this" {
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| [application\_manager\_assume\_role](#input\_application\_manager\_assume\_role) | ARN of the IAM role assumed by the application manager | `string` | `"arn:aws:iam::283477532906:role/application_manager"` | no |
+| [build\_workflow\_group\_name](#input\_build\_workflow\_group\_name) | Name of the IAM group (from the ci-build-workflow-user module) to which the ECR manager policy is attached. The build workflow user is a member of this group. | `string` | n/a | yes |
| [cluster\_name](#input\_cluster\_name) | Name of the cluster, used to namespace IAM resource names | `string` | n/a | yes |
| [enable\_cross\_account\_pull](#input\_enable\_cross\_account\_pull) | Enable cross-account ECR pull access by creating an IAM role that external accounts can assume | `bool` | `false` | no |
| [pull\_account\_ids](#input\_pull\_account\_ids) | AWS account IDs allowed to assume the cross-account ECR pull role. Required when enable\_cross\_account\_pull is true. | `list(string)` | `[]` | no |
@@ -74,21 +81,18 @@ resource "example_resource" "this" {
| Name | Description |
|------|-------------|
| [application\_role\_arn](#output\_application\_role\_arn) | ARN of the IAM role used by applications to pull ECR images |
-| [build\_workflow\_access\_key\_id](#output\_build\_workflow\_access\_key\_id) | Access key ID for the CI/CD build workflow IAM user |
-| [build\_workflow\_access\_key\_secret](#output\_build\_workflow\_access\_key\_secret) | Secret access key for the CI/CD build workflow IAM user |
| [ecr\_repository\_policy](#output\_ecr\_repository\_policy) | ECR repository policy JSON granting pull access to the configured cross-account IDs. Empty string when enable\_cross\_account\_pull is false. |
diff --git a/infrastructure/aws/iam/ecr/main.tf b/infrastructure/aws/iam/ecr/main.tf
index acab5d2f..b95f87ea 100644
--- a/infrastructure/aws/iam/ecr/main.tf
+++ b/infrastructure/aws/iam/ecr/main.tf
@@ -44,30 +44,13 @@ resource "aws_iam_policy" "nullplatform_ecr_manager_policy" {
})
}
-resource "aws_iam_user" "nullplatform_build_workflow_user" {
- name = "nullplatform-${var.cluster_name}-build-workflow-user"
-}
-
-resource "aws_iam_access_key" "nullplatform_build_workflow_user_key" {
- user = aws_iam_user.nullplatform_build_workflow_user.name
-}
-
resource "aws_iam_role_policy_attachment" "ecr_manager_policy" {
role = aws_iam_role.nullplatform_application_role.name
policy_arn = aws_iam_policy.nullplatform_ecr_manager_policy.arn
}
-resource "aws_iam_group" "nullplatform_ecr_managers" {
- name = "nullplatform-${var.cluster_name}-ecr-managers"
-}
-
resource "aws_iam_group_policy_attachment" "ecr_manager_policy_group" {
- group = aws_iam_group.nullplatform_ecr_managers.name
+ group = var.build_workflow_group_name
policy_arn = aws_iam_policy.nullplatform_ecr_manager_policy.arn
}
-resource "aws_iam_user_group_membership" "build_workflow_ecr_managers" {
- user = aws_iam_user.nullplatform_build_workflow_user.name
- groups = [aws_iam_group.nullplatform_ecr_managers.name]
-}
-
diff --git a/infrastructure/aws/iam/ecr/outputs.tf b/infrastructure/aws/iam/ecr/outputs.tf
index c6274d95..8be19eb7 100644
--- a/infrastructure/aws/iam/ecr/outputs.tf
+++ b/infrastructure/aws/iam/ecr/outputs.tf
@@ -3,17 +3,6 @@ output "application_role_arn" {
value = aws_iam_role.nullplatform_application_role.arn
}
-output "build_workflow_access_key_id" {
- description = "Access key ID for the CI/CD build workflow IAM user"
- value = aws_iam_access_key.nullplatform_build_workflow_user_key.id
-}
-
-output "build_workflow_access_key_secret" {
- description = "Secret access key for the CI/CD build workflow IAM user"
- value = aws_iam_access_key.nullplatform_build_workflow_user_key.secret
- sensitive = true
-}
-
output "ecr_repository_policy" {
description = "ECR repository policy JSON granting pull access to the configured cross-account IDs. Empty string when enable_cross_account_pull is false."
value = var.enable_cross_account_pull ? jsonencode({
diff --git a/infrastructure/aws/iam/ecr/variables.tf b/infrastructure/aws/iam/ecr/variables.tf
index 88ff1cd9..fb9f97b7 100644
--- a/infrastructure/aws/iam/ecr/variables.tf
+++ b/infrastructure/aws/iam/ecr/variables.tf
@@ -3,6 +3,11 @@ variable "cluster_name" {
type = string
}
+variable "build_workflow_group_name" {
+ description = "Name of the IAM group (from the ci-build-workflow-user module) to which the ECR manager policy is attached. The build workflow user is a member of this group."
+ type = string
+}
+
variable "application_manager_assume_role" {
description = "ARN of the IAM role assumed by the application manager"
type = string
diff --git a/infrastructure/aws/iam/s3-assets/README.md b/infrastructure/aws/iam/s3-assets/README.md
new file mode 100644
index 00000000..0c7982ed
--- /dev/null
+++ b/infrastructure/aws/iam/s3-assets/README.md
@@ -0,0 +1,62 @@
+# Module: s3-assets
+
+## Description
+
+Grants the shared build workflow group permission to publish build assets (e.g. Lambda deployment zips) to an existing S3 assets bucket
+
+## Architecture
+
+The module creates an `aws_iam_policy` allowing `s3:PutObject` and `s3:GetObject` on the objects of a given assets bucket (`arn:aws:s3:::/*`) and attaches it to the shared build-workflow group via `aws_iam_group_policy_attachment`. The group is created by the `ci-build-workflow-user` module and passed in through `build_workflow_group_name`, so the build workflow user accumulates S3 publishing permissions alongside ECR (and any other destination) through that single group. The bucket itself is managed elsewhere and only referenced by name.
+
+## Features
+
+- Creates a namespaced `aws_iam_policy` scoped to `s3:PutObject`/`s3:GetObject` on the assets bucket objects
+- Attaches the policy to the shared build-workflow group (created by the ci-build-workflow-user module)
+- Keeps the bucket out of scope: it is referenced by name, not created or managed here
+
+## Basic Usage
+
+```hcl
+module "ci_build_workflow_user" {
+ source = "git::https://github.com/nullplatform/tofu-modules.git//infrastructure/aws/iam/ci-build-workflow-user?ref=v5.0.0"
+
+ cluster_name = "your-cluster-name"
+}
+
+module "s3_assets" {
+ source = "git::https://github.com/nullplatform/tofu-modules.git//infrastructure/aws/iam/s3-assets?ref=v5.0.0"
+
+ cluster_name = "your-cluster-name"
+ build_workflow_group_name = module.ci_build_workflow_user.group_name
+ assets_bucket = "your-assets-bucket"
+}
+```
+
+
+
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [aws](#provider\_aws) | n/a |
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [aws_iam_group_policy_attachment.s3_assets_policy_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group_policy_attachment) | resource |
+| [aws_iam_policy.nullplatform_s3_assets_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [assets\_bucket](#input\_assets\_bucket) | Name of the S3 bucket where build assets (e.g. Lambda zips) are published. The bucket is managed elsewhere; this module only grants the build workflow group permission to write to it. | `string` | n/a | yes |
+| [build\_workflow\_group\_name](#input\_build\_workflow\_group\_name) | Name of the IAM group (from the ci-build-workflow-user module) to which the S3 assets policy is attached. The build workflow user is a member of this group. | `string` | n/a | yes |
+| [cluster\_name](#input\_cluster\_name) | Name of the cluster, used to namespace IAM resource names | `string` | n/a | yes |
+
+## Outputs
+
+No outputs.
+
diff --git a/infrastructure/aws/iam/s3-assets/main.tf b/infrastructure/aws/iam/s3-assets/main.tf
new file mode 100644
index 00000000..ac1227c3
--- /dev/null
+++ b/infrastructure/aws/iam/s3-assets/main.tf
@@ -0,0 +1,22 @@
+resource "aws_iam_policy" "nullplatform_s3_assets_policy" {
+ name = "nullplatform-${var.cluster_name}-s3-assets-policy"
+ description = "Policy for publishing build assets (e.g. Lambda zips) to the assets S3 bucket"
+ policy = jsonencode({
+ Version = "2012-10-17",
+ Statement = [
+ {
+ Effect = "Allow"
+ Action = [
+ "s3:PutObject",
+ "s3:GetObject"
+ ]
+ Resource = "arn:aws:s3:::${var.assets_bucket}/*"
+ }
+ ]
+ })
+}
+
+resource "aws_iam_group_policy_attachment" "s3_assets_policy_group" {
+ group = var.build_workflow_group_name
+ policy_arn = aws_iam_policy.nullplatform_s3_assets_policy.arn
+}
diff --git a/infrastructure/aws/iam/s3-assets/variables.tf b/infrastructure/aws/iam/s3-assets/variables.tf
new file mode 100644
index 00000000..ee7f095c
--- /dev/null
+++ b/infrastructure/aws/iam/s3-assets/variables.tf
@@ -0,0 +1,14 @@
+variable "cluster_name" {
+ description = "Name of the cluster, used to namespace IAM resource names"
+ type = string
+}
+
+variable "build_workflow_group_name" {
+ description = "Name of the IAM group (from the ci-build-workflow-user module) to which the S3 assets policy is attached. The build workflow user is a member of this group."
+ type = string
+}
+
+variable "assets_bucket" {
+ description = "Name of the S3 bucket where build assets (e.g. Lambda zips) are published. The bucket is managed elsewhere; this module only grants the build workflow group permission to write to it."
+ type = string
+}
diff --git a/nullplatform/asset/s3/README.md b/nullplatform/asset/s3/README.md
new file mode 100644
index 00000000..3807f916
--- /dev/null
+++ b/nullplatform/asset/s3/README.md
@@ -0,0 +1,67 @@
+# Module: s3
+
+## Description
+
+Configures an AWS S3 asset repository in nullplatform, registering the bucket where Lambda and bundle assets are published
+
+## Architecture
+
+The module creates a `nullplatform_provider_config` resource of type `s3-configuration` (a platform-global provider specification in the `assets-repository` category) whose attributes carry the target `bucket.name`. The platform maps this bucket to the `aws.s3_assets_bucket` NRN configuration (via the specification's `runtime_configuration` storage strategy), which the backend reads when generating the S3 upload URL for Lambda/bundle assets. Unlike the `ecr` asset module, this provider config does **not** carry build credentials: the CI publishes S3 assets with the shared build workflow credentials (`BUILD_AWS_*`), so the build workflow user must be granted S3 permissions separately via `infrastructure/aws/iam/s3-assets`.
+
+## Features
+
+- Registers an AWS S3 bucket as a nullplatform asset repository (`s3-configuration` provider config)
+- Supplies the `bucket.name` that the platform exposes as `aws.s3_assets_bucket`
+- Optionally segments the provider config by `dimensions` (e.g. region, environment)
+- Does not manage the bucket or credentials: the bucket is referenced by name and S3 publish permissions are granted by `infrastructure/aws/iam/s3-assets`
+
+## Basic Usage
+
+```hcl
+module "asset_s3" {
+ source = "git::https://github.com/nullplatform/tofu-modules.git//nullplatform/asset/s3?ref=v5.0.0"
+
+ nrn = var.nrn
+ bucket_name = "your-assets-bucket"
+}
+```
+
+Grant the build workflow user permission to write to that bucket with the companion IAM module:
+
+```hcl
+module "s3_assets" {
+ source = "git::https://github.com/nullplatform/tofu-modules.git//infrastructure/aws/iam/s3-assets?ref=v5.0.0"
+
+ cluster_name = "your-cluster-name"
+ build_workflow_group_name = module.build_user.group_name
+ assets_bucket = "your-assets-bucket"
+}
+```
+
+
+
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [nullplatform](#provider\_nullplatform) | ~> 0.0.88 |
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [nullplatform_provider_config.s3](https://registry.terraform.io/providers/nullplatform/nullplatform/latest/docs/resources/provider_config) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [bucket\_name](#input\_bucket\_name) | Name of the existing S3 bucket used as the asset repository, where Lambda/bundle assets are published. Maps to the platform's aws.s3\_assets\_bucket configuration. | `string` | n/a | yes |
+| [dimensions](#input\_dimensions) | Dimensions to segment the nullplatform provider config (e.g. by region, environment) | `map(string)` | `{}` | no |
+| [nrn](#input\_nrn) | The nullplatform resource name (NRN) | `string` | n/a | yes |
+
+## Outputs
+
+No outputs.
+
diff --git a/nullplatform/asset/s3/main.tf b/nullplatform/asset/s3/main.tf
new file mode 100644
index 00000000..9818d911
--- /dev/null
+++ b/nullplatform/asset/s3/main.tf
@@ -0,0 +1,11 @@
+resource "nullplatform_provider_config" "s3" {
+ provider = nullplatform
+ nrn = var.nrn
+ type = "s3-configuration"
+ dimensions = var.dimensions
+ attributes = jsonencode({
+ bucket = {
+ name = var.bucket_name
+ }
+ })
+}
diff --git a/nullplatform/asset/s3/providers.tf b/nullplatform/asset/s3/providers.tf
new file mode 100644
index 00000000..1ad0cd5e
--- /dev/null
+++ b/nullplatform/asset/s3/providers.tf
@@ -0,0 +1,8 @@
+terraform {
+ required_providers {
+ nullplatform = {
+ source = "nullplatform/nullplatform"
+ version = "~> 0.0.88"
+ }
+ }
+}
diff --git a/nullplatform/asset/s3/variables.tf b/nullplatform/asset/s3/variables.tf
new file mode 100644
index 00000000..527285b5
--- /dev/null
+++ b/nullplatform/asset/s3/variables.tf
@@ -0,0 +1,15 @@
+variable "nrn" {
+ description = "The nullplatform resource name (NRN)"
+ type = string
+}
+
+variable "dimensions" {
+ description = "Dimensions to segment the nullplatform provider config (e.g. by region, environment)"
+ type = map(string)
+ default = {}
+}
+
+variable "bucket_name" {
+ description = "Name of the existing S3 bucket used as the asset repository, where Lambda/bundle assets are published. Maps to the platform's aws.s3_assets_bucket configuration."
+ type = string
+}