> ## Documentation Index
> Fetch the complete documentation index at: https://docs.superblocks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Superblocks Database Reference

> Implementation details behind Superblocks Database: AWS resources, IAM scoping, etc

export const Alert = ({type, title, children}) => {
  const variant = ["info", "success", "warning", "danger", "note"].includes(type) ? type : "note";
  return <div className={`alert alert--${variant}`}>
      <div className="alert-icon" />
      <div className="alert-content">
        {title && <div className="alert-title">{title}</div>}
        <div className="alert-body">{children}</div>
      </div>
    </div>;
};

This page is a reference for the full lifecycle of an app database: what exists at each stage, what it is named, and which IAM role is allowed to act on it. For initial setup, start with [Setup](/admin/superblocks-on-aws/database-setup). For what the feature does for builders, see [Superblocks Database](/admin/superblocks-on-aws/database). For the trust boundaries and network paths behind these details, see [Security model](/admin/superblocks-on-aws/database-security).

## One-time setup

One-time setup is a single Terraform module, `modules/app-db-prereqs`, recommended for every deployment. It creates IAM roles, policies, and an S3 bucket for storing OpenTofu state.

```hcl theme={null}
module "app_db_prereqs" {
  source  = "superblocksteam/superblocks/aws//modules/app-db-prereqs"
  version = ">= 1.5.3, < 2.0.0"

  deployment_type = "fargate" # or "eks"
  region          = "<REGION>"

  # One entry per data plane. The map key is the agent name.
  agents = {
    "<AGENT_NAME>" = {
      agent_tags = ["<DATA_TAG_1>"]
      vpc_id     = "<VPC_ID>"
    }
  }
}
```

Run it once per region, declaring every data plane in that region in the same `agents` map so they share one state bucket. For the walkthrough that wires its outputs into Fargate or Helm, see [Setup: Step 1](/admin/superblocks-on-aws/database-setup#step-1-run-the-terraform-module).

### Resources created

<div className="dense-table">
  | Resource                                       | Default name                                       | Purpose                                                                                                                                                                                                                                                    |
  | ---------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | Lifecycle worker IAM role, plus seven policies | `sb-app-db-<AGENT_NAME>-lifecycle-worker-<REGION>` | To provision and modify only the resources Superblocks tagged, in the VPC you declared. See [Raw IAM policy JSON](#raw-iam-policy-json)                                                                                                                    |
  | Connector IAM role, plus its policy            | `superblocks-app-db-connector-<AGENT_NAME>`        | To authenticate at query time as `sbndb_<profile-token>_<application-token>_runtime`, assumable only by this data plane's lifecycle worker. See [Connecting](#connecting)                                                                                  |
  | Enhanced Monitoring IAM role                   | `sb-app-db-enhanced-monitoring`                    | To let RDS publish operating-system metrics. The worker may pass this role only, and only to `rds.amazonaws.com`                                                                                                                                           |
  | S3 state bucket                                | `sb-app-db-<REGION>-<ACCOUNT_ID>`                  | To store OpenTofu state at `{key_prefix}/{tag}/{resource_key}.tfstate`, versioned and private. Superblocks reads and writes this bucket in your account; Superblocks does not retain a copy. See [S3 state bucket properties](#s3-state-bucket-properties) |
</div>

### Module options

<div className="dense-table">
  | Option                         | Default                 | Purpose                                                                                                                       |
  | ------------------------------ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
  | `agents`                       | Required                | To declare one entry per data plane, keyed by [agent name](#agent-name)                                                       |
  | `deployment_type`              | Required                | To pick the lifecycle worker trust policy: `fargate` or `eks`                                                                 |
  | `existing_monitoring_role_arn` | Module creates the role | To reuse an Enhanced Monitoring role, since IAM role names are account-global and a second region cannot create the same name |
  | `iam_name_prefix`              | `sb-app-db`             | To prefix IAM role and policy names, up to 16 characters                                                                      |
  | `kms_key_arn`                  | SSE-S3                  | To encrypt state with your own KMS key, and to scope the worker's KMS access to that key alone                                |
  | `region`                       | Required                | To set the region these resources are created in                                                                              |
  | `s3_name_prefix`               | `sb-app-db`             | To prefix the state bucket name, up to 16 characters                                                                          |
  | `tags`                         | `{}`                    | To add inventory tags. `ManagedBy`, `superblocks:owned`, and `aws-apn-id` are reserved and always enforced                    |
</div>

### Per-data-plane options

Each entry in the `agents` map takes these options.

<div className="dense-table">
  | Option                   | Default               | Purpose                                                                                                                                      |
  | ------------------------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
  | `agent_tags`             | Required              | To name the [data tags](#data-tag-names) this data plane serves, which derive its database and role names                                    |
  | `existing_role_name`     | Module creates a role | To attach the worker policies to an ECS task role you already have, for brownfield deployments                                               |
  | `key_prefix`             | `app-db/<AGENT_NAME>` | To namespace this data plane's state inside the shared bucket, and to scope its S3 permissions. Must not overlap another data plane's prefix |
  | `namespace`              | `superblocks`         | To name the data plane's Kubernetes namespace, on EKS                                                                                        |
  | `oidc_provider_arn`      | Required on EKS       | To trust the data plane's Kubernetes service account through IRSA                                                                            |
  | `rds_secret_kms_key_arn` | AWS-managed key       | To encrypt the RDS-managed master secret with your own KMS key                                                                               |
  | `service_account_name`   | `superblocks-agent`   | To name the data plane's Kubernetes service account, on EKS                                                                                  |
  | `vpc_id`                 | Required              | To scope every IAM policy for this data plane to one [VPC](#vpc)                                                                             |
</div>

### Multiple data planes in one account and region

Superblocks recommends one data plane per AWS account and VPC, so most deployments never need this. When you do run more than one in the same account and region, declare them all in one `app_db_prereqs` invocation. They share the state bucket, but not IAM roles or VPCs.

```hcl theme={null}
module "app_db_prereqs" {
  # ...same source, version, deployment_type, and region as a single data plane...
  agents = {
    "<AGENT_NAME>" = {
      agent_tags = ["<DATA_TAG>"]
      vpc_id     = "<VPC_ID>"
    }
    "<AGENT_NAME_2>" = {
      agent_tags = ["<DATA_TAG_2>"]
      vpc_id     = "<VPC_ID_2>"
    }
  }
}
```

### Additional AWS regions

Each region needs its own `app_db_prereqs` invocation with an AWS provider alias, and gets its own S3 state bucket. The Enhanced Monitoring role is the exception: IAM role names are account-global, so designate the first region as the role's durable home and pass its ARN to every additional region through `existing_monitoring_role_arn`.

```hcl theme={null}
provider "aws" {
  alias  = "secondary"
  region = "<SECOND_REGION>"
}

module "app_db_prereqs_secondary" {
  source  = "superblocksteam/superblocks/aws//modules/app-db-prereqs"
  version = ">= 1.5.3, < 2.0.0"

  providers = {
    aws = aws.secondary
  }

  deployment_type              = "<fargate-or-eks>"
  region                       = "<SECOND_REGION>"
  existing_monitoring_role_arn = module.app_db_prereqs.enhanced_monitoring_role_arn

  agents = {
    # Configure the data planes in this region as shown above.
  }
}
```

The example above assumes both regional module calls share one Terraform root and state. If each region uses separate state, pass the owning stack's output through `terraform_remote_state` or supply the concrete ARN (`arn:aws:iam::<ACCOUNT_ID>:role/<iam_name_prefix>-enhanced-monitoring`) as `existing_monitoring_role_arn`.

The secondary prerequisite stack re-exports the supplied role through its own `enhanced_monitoring_role_arn` output. Use `module.app_db_prereqs_secondary.enhanced_monitoring_role_arn` as `monitoring_role_arn` in that region's Fargate `physical_module_inputs` or EKS `physicalModuleInputs`.

<Alert type="warning">
  The first region's prerequisite stack owns a cross-region dependency. Do not destroy that stack while any other region uses its Enhanced Monitoring role. Do not add `existing_monitoring_role_arn` later to the stack that created the role without first moving every consumer to another role; Terraform would destroy the role that stack currently owns.
</Alert>

If that role was deleted or changed accidentally, first check for `<iam_name_prefix>-enhanced-monitoring` in IAM. Restore the original owning prerequisite stack and run `terraform apply` to recreate its trust policy and managed-policy attachment. Because the recreated role has the same ARN, existing regional consumers can resume using it. To move ownership deliberately, create a replacement role, update every regional prerequisite stack and physical database input to the replacement ARN, apply those changes, and only then remove the old role.

## Immutable setup inputs

There is no automated migration path, so changing any of these values requires manual recovery.

### Agent name

This is a unique value represented in the Terraform inputs as `agents = { "<AGENT_NAME>" = { ... } }`. The agent name must be passed as an input into Fargate or Helm. The name is used to derive these outputs:

* The [IAM role and policy names](#raw-iam-policy-json), one set per data plane
* The [`AgentName` tag](#how-everything-is-tagged) that every resource must carry before the lifecycle worker can change it
* The [OpenTofu state prefix](#one-time-setup), default `app-db/<AGENT_NAME>`, which also scopes the worker's S3 permissions

If the names become mismatched for any reason, you will see `AccessDenied` errors.

### Data tag names

Database and role names are derived from a hash of the tag: `sbndb_<profile-token>_<application-token>` for the database, `sbndb_<profile-token>_<application-token>_runtime` for its role. `<profile-token>` is the first 16 hex characters of `SHA-256(lowercase(tag))`; `<application-token>` is a 24-hex hash of the application ID. The connector role's permissions are scoped to that pattern, so renaming a tag orphans every existing database and role under the old name. Tag names must be non-empty and unique across all agents in the module invocation. Settle your tags before any database exists; see [Plan your data planes](/enterprise/hybrid-architecture/plan/index).

### VPC

Every IAM policy created for a data plane is scoped to its VPC. Changing it means none of the existing roles can act on resources in the new VPC.

### Cluster autoscaling

Capacity is configured per data plane. Superblocks recommends the same always-on shape for every data tag, including edit and preview: two instances at 2-32 ACUs each. Choose it when you provision the data plane. After the data plane registers serverless scaling changes (ACU range and instance count), Superblocks automatically queues updates for existing clusters. See [Apply configuration changes to existing clusters](/admin/superblocks-on-aws/database-setup#apply-configuration-changes-to-existing-clusters).

<div className="dense-table">
  | Setting                  | Recommended value                                        |
  | ------------------------ | -------------------------------------------------------- |
  | `instance_count`         | `2`                                                      |
  | `min_acu` (per instance) | `2`                                                      |
  | `max_acu` (per instance) | `32`                                                     |
  | Cost while idle          | Two instances billed at the ACU floor (4 ACUs aggregate) |
  | First query after idle   | Served immediately                                       |
</div>

Omitting the `deployment` block gives you this same shape: the released Aurora module defaults to two instances at 2-32 ACUs each, for an aggregate idle floor of 4 ACUs. Set the block explicitly anyway so the capacity is visible in your configuration. With a nonzero `min_acu`, the cluster never pauses.

The default `min_acu` is `2`. Setting it to `0` lets an idle cluster pause, which costs less but adds a cold start to the next connection (not recommended). A cluster that has been paused for about 24 hours can take over a minute to resume.

## Provisioning the cluster pool

Your data plane provisions the first Aurora cluster when an admin clicks **Provision**, and adds another whenever remaining pool capacity hits the pool settings from the data plane configuration in [Setup: Step 1](/admin/superblocks-on-aws/database-setup#step-1-run-the-terraform-module) (`max_databases` on Fargate; that cap plus `minAvailableCapacityPercent` on Helm). This usually takes 5 to 15 minutes, depending on AWS latency. It runs `tofu apply` with the configuration it already has, so nothing is re-entered in the admin panel.

Each cluster added to the pool arrives with:

* **Aurora PostgreSQL cluster and its instances**: named `sb-{hash}`. Physical modules enable IAM database authentication, keep the endpoint private, encrypt storage, and manage the master password in AWS Secrets Manager. The lifecycle worker reads that secret by ref, so there is no plaintext master credential in Superblocks systems.
* **CloudWatch log group**: a per-cluster `/aws/rds/cluster/<cluster-id>/postgresql` group that retains exported PostgreSQL logs for 30 days
* **DB cluster parameter group**: carries the logging parameters described under [Observing](#observing)
* **DB subnet group**: scoped to the subnets you specified in `physical_module_inputs`
* **Security group and its ingress/egress rules**: controls which sources (security groups or CIDRs) can reach the database on port 5432. The security group and each rule are separate AWS resources, and both carry the tags described under [How everything is tagged](#how-everything-is-tagged)

App databases themselves are logical databases inside these clusters, each with its own PostgreSQL role. They are not separate AWS resources, so they never show up in the RDS console as their own entries.

The lifecycle worker role does the creating, under the tag requirements described under [How everything is tagged](#how-everything-is-tagged). IAM policy conditions require `publicly_accessible = false`, storage encryption, and an AWS Secrets Manager master password, so a modified module cannot opt out of those. IAM database authentication is enabled by the physical modules (`iam_database_authentication_enabled = true`); there is no corresponding IAM condition. See [Raw IAM policy JSON](#raw-iam-policy-json).

### Pool options

Lowering `max_databases` spreads apps across more clusters, which shrinks the blast radius of a failover or an engine patch and raises your baseline cost; raising it does the reverse.

Fargate takes these under `pool`; Helm takes them under `databaseLifecycle.pool` in lowerCamelCase.

<div className="dense-table">
  | Option                                                           | Default                            | Purpose                                                                                                                                    |
  | ---------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
  | `max_databases` / `maxDatabases`                                 | `100`                              | To cap the logical databases one cluster holds before the next app database waits on a new cluster                                         |
  | `min_available_capacity_percent` / `minAvailableCapacityPercent` | Fargate: not published. Helm: `20` | To enqueue a new cluster when remaining capacity falls to this percent of `max_databases`. Helm only. Set `0` to disable proactive enqueue |
</div>

### Physical database options

Fargate takes these under `physical_module_inputs`; Helm takes the same keys under `databaseLifecycle.physicalModuleInputs`. New clusters use these settings when they are provisioned. After the data plane registers changes to supported settings, Superblocks automatically queues updates for existing clusters. See [Apply configuration changes to existing clusters](/admin/superblocks-on-aws/database-setup#apply-configuration-changes-to-existing-clusters).

<div className="dense-table">
  | Option                      | Default                                       | Purpose                                                                                                                                          |
  | --------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
  | `allowed_cidr_blocks`       | `[]`                                          | To let CIDR ranges reach the database on port 5432                                                                                               |
  | `backup_retention_period`   | `7`                                           | To retain automated backups for 1 to 35 days                                                                                                     |
  | `delete_automated_backups`  | `false`                                       | To discard automated backups when a cluster is destroyed                                                                                         |
  | `deletion_protection`       | `true`                                        | To block accidental cluster deletion                                                                                                             |
  | `deployment`                | Two instances at 2-32 ACUs each, never pauses | To set Aurora capacity through nested `serverless_v2` or `provisioned`. See [Cluster autoscaling](#cluster-autoscaling)                          |
  | `monitoring_interval`       | `60`                                          | To set the Enhanced Monitoring interval in seconds, or `0` to turn it off                                                                        |
  | `monitoring_role_arn`       | None                                          | To pass the shared Enhanced Monitoring role, required unless `monitoring_interval` is `0`                                                        |
  | `skip_final_snapshot`       | `false`                                       | To skip the final snapshot when a cluster is destroyed                                                                                           |
  | `source_security_group_ids` | `[]`                                          | To let security groups reach the database on port 5432, typically your data plane's                                                              |
  | `subnet_ids`                | Required                                      | To become the DB subnet group. At least two, spanning at least two Availability Zones                                                            |
  | `tags`                      | `{}`                                          | To add inventory tags to every provisioned resource. Tags are merged with `superblocks:owned`, `aws-apn-id`, `AgentName`, `ManagedBy`, and `Vpc` |
  | `vpc_id`                    | Required                                      | To place clusters in one VPC. Must match this data plane's `agents` entry                                                                        |
</div>

`publicly_accessible` is not an option. It is always false, enforced by IAM rather than module default.

Additional options `allocated_storage`, `instance_class`, and `multi_az` exist for alternative database configurations within AWS.

## Creating an app database

A builder with database creation permission asks Clark for a feature that needs persistent data. Superblocks queues one task to create an isolated logical database on a cluster that already has capacity under your pool settings; if none does, it provisions a new cluster first. Your data plane claims the task, runs `tofu apply` in your AWS account, and reports back when the database is ready. Superblocks only queues the work. Your data plane is the only thing that calls AWS.

The result is a PostgreSQL database and a matching role inside an existing cluster, named `sbndb_<profile-token>_<application-token>` and `sbndb_<profile-token>_<application-token>_runtime` (see [Data tag names](#data-tag-names) for how the tokens are derived). Each app gets one database per [data tag](/development-lifecycle/build/data-tags) the data plane serves.

The lifecycle worker creates both and grants the role `rds_iam`. The connector role stays out of it until the app runs a query.

## Applying schema migrations

Clark writes the schema as `.sql` files in the application's own code, so it is versioned alongside the app. Once a database reports ready, Superblocks queues a migration task and your data plane applies those files to that one database. No AWS resources change.

Edit-time and deploy-time databases use the same path.

## Connecting

<Tabs>
  <Tab title="ECS Fargate">
    ```hcl theme={null}
    module "app_db" {
      # Wired from the prereq module so the two cannot diverge.
      agent_tags         = module.app_db_prereqs.agents["<AGENT_NAME>"].agent_tags
      connector_role_arn = module.app_db_prereqs.agents["<AGENT_NAME>"].connector_role_arn
    }
    ```
  </Tab>

  <Tab title="EKS (Helm)">
    ```yaml theme={null}
    superblocks:
      # Comma-separated, each data tag prefixed with profile:.
      agentTags: profile:<DATA_TAG_1>

    postgres:
      appDbConnectorRoleArn: "<CONNECTOR_ROLE_ARN>"

      iamAllowedRoleArnPrefixes:
        - "<CONNECTOR_ROLE_ARN>"
    ```
  </Tab>
</Tabs>

When an app runs a query, the data plane authenticates with the connector role. Neither that role's ARN nor the lifecycle worker's identity appears inside PostgreSQL: the database only ever sees a PostgreSQL role name and an auth token, and IAM decides whether that token is valid. The sequence for a single query is:

1. **Provisioning** creates the app's PostgreSQL role (`sbndb_<profile-token>_<application-token>_runtime`) and grants it `rds_iam`. That grant makes PostgreSQL delegate authentication for this role to IAM instead of checking a stored password. No password is ever generated for it.
2. **The data plane checks the target** against the connector role ARNs in its own configuration, and refuses the connection if they disagree.
3. **The data plane assumes the connector role**, attaching an inline session policy that allows exactly one action on exactly one resource: `rds-db:connect` on this cluster, as this one PostgreSQL role.
4. **It signs an RDS IAM auth token** for that PostgreSQL role with those session credentials, and connects using the token as the password, over TLS with full certificate and hostname verification.

The effective permission on any given connection is the intersection of two policies. The connector role's own policy bounds it to `sbndb_<profile-token>_*_runtime` users for the declared data tags (see [Raw IAM policy JSON](#raw-iam-policy-json)), and the per-connection session policy narrows that to the single user for the single app being served. One app's credentials cannot reach another app's database, even though both use the same connector role.

## Deploying

When a builder publishes an app, the deploy-time [data tag](/development-lifecycle/build/data-tags) gets its own database. If the app does not have one there yet, Superblocks queues a create task and then a migration task, producing the same database and role as at edit time under a different tag. The deploy does not proceed until both finish.

No data is copied. The deploy-time database gets the schema and none of the development rows.

## Observing

<Tabs>
  <Tab title="ECS Fargate">
    ```hcl theme={null}
    module "app_db" {
      physical_module_inputs = {
        # Enhanced Monitoring. Set the interval to 0 and omit the role to opt out.
        monitoring_interval = 60
        monitoring_role_arn = module.app_db_prereqs.enhanced_monitoring_role_arn
      }
    }
    ```
  </Tab>

  <Tab title="EKS (Helm)">
    ```yaml theme={null}
    databaseLifecycle:
      physicalModuleInputs:
        # Enhanced Monitoring. Set the interval to 0 and omit the role to opt out.
        monitoring_interval: 60
        monitoring_role_arn: "<ENHANCED_MONITORING_ROLE_ARN>"

        # Helm only, both on by default. Fargate cannot disable them as of v1.5.2.
        # enabled_cloudwatch_logs_exports: []
        # performance_insights_enabled: false
    ```
  </Tab>
</Tabs>

The default configuration provides the following signals in your AWS account:

* Standard RDS CloudWatch metrics
* Enhanced Monitoring at 60-second resolution through the account- and region-level `RDSOSMetrics` CloudWatch Logs group, with one log stream per database instance. Its retention is independent of the per-cluster PostgreSQL log group's 30-day setting
* Database Insights Standard, with Performance Insights retained for 7 days
* PostgreSQL logs exported to CloudWatch Logs with 30-day retention
* PostgreSQL diagnostic logs for lock waits and deadlocks, temporary files larger than 64 MiB, checkpoints, and autovacuum operations running at least 5 seconds
* Slow-query logging for every statement running at least 5 seconds and a 10% sample of statements running between 1 and 5 seconds. Statements under 1 second are not duration-logged. Bound parameter values are excluded, although the parameterized statement text is retained
* `pg_stat_statements` tracking for top-level statements. Its storage is bounded and shared across the cluster, so older or less-used entries can be evicted; the module does not fix a universal entry limit

Settings declared in the physical module inputs or supplied by its defaults, including log retention and PostgreSQL parameters, are managed through OpenTofu, so a later lifecycle apply can revert manual console changes. Superblocks does not ingest these signals or monitor your databases on your behalf.

### How everything is tagged

Every App Database AWS resource carries two kinds of tags.

**IAM scoping tags** (required for the lifecycle worker to mutate the resource):

* `AgentName={agents-map-key}`, the [agent name](/admin/superblocks-on-aws/database-reference#agent-name) for this data plane (Terraform `agents` map key, Fargate `agent_name`, or Helm `superblocks.agentName`)
* `ManagedBy=superblocks-app-database-lifecycle`
* `Vpc={your-vpc-id}`

The IAM policies only allow create/modify/delete on resources carrying all three. The worker stamps `AgentName` itself; physical modules merge `ManagedBy`; `Vpc` comes from your networking inputs.

**Ownership and partnership tags** (required on every Superblocks-created resource, including runtime Aurora):

* `superblocks:owned=true`
* `aws-apn-id=pc:ctelqp437y3cvjkv5rv0z2w4f`

These do **not** change who pays the bill or which AWS account owns the resources, which remains your account. `aws-apn-id` is identical in every deployment rather than per-account.

`app-db-prereqs` enforces both keys on setup resources. The physical OpenTofu modules stamp them on runtime Aurora, RDS, subnet groups, and security groups, overwriting conflicting caller values. The Fargate `app-db` module also merges the same pair into runtime module inputs. On EKS, `databaseLifecycle.physicalModuleTags` is for optional inventory tags only (cost center, team, and similar). Do not set `ManagedBy`, `AgentName`, `Vpc`, `superblocks:owned`, or `aws-apn-id` there.

The lifecycle worker IAM policy also denies removing any of the five keys above (`AgentName`, `ManagedBy`, `Vpc`, `superblocks:owned`, and `aws-apn-id`), including via named Deny Sids (`DenyRemoveProtectedTags` / `DenyDeleteProtectedTags` / `DenyUntagProtectedTags`) so AccessDenied is attributable. EC2 `DenyDeleteProtectedTags` is further scoped with `AgentName` / `ManagedBy` / `Vpc` ResourceTags so attaching policies via `existing_role_name` cannot Deny `DeleteTags` on unrelated security groups.

A Resource Group or the CLI lists everything the feature manages:

```bash theme={null}
aws resourcegroupstaggingapi get-resources \
  --tag-filters Key=ManagedBy,Values=superblocks-app-database-lifecycle

# Or filter on ownership:
aws resourcegroupstaggingapi get-resources \
  --tag-filters Key=superblocks:owned,Values=true
```

## S3 state bucket properties

The Terraform module creates the state bucket, so these properties only matter if your organization creates it by hand instead.

<Accordion title="Required S3 state bucket properties">
  <div className="dense-table">
    | Property                           | Required value              | Notes                                                                                                                                                              |
    | ---------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
    | Versioning                         | Enabled                     | Required for state recovery                                                                                                                                        |
    | Block public access                | All four settings enabled   | `block_public_acls`, `block_public_policy`, `ignore_public_acls`, `restrict_public_buckets`                                                                        |
    | Server-side encryption             | SSE-S3 (default) or SSE-KMS | SSE-S3 uses AWS-managed keys and incurs no KMS charges. SSE-KMS only when `kms_key_arn` is provided; use the same key ARN supplied to the module                   |
    | S3 bucket key                      | Disabled                    | The module sets `bucket_key_enabled = false`. This is separate from `kms_key_arn`; you can use SSE-KMS with a customer-managed key and still have bucket keys off. |
    | Non-current version expiration     | 90 days                     | Keeps state recoverable without unbounded storage growth                                                                                                           |
    | Abort incomplete multipart uploads | 7 days                      |                                                                                                                                                                    |
    | Bucket policy                      | None                        | Access is controlled via the IAM policies in [Raw IAM policy JSON](#raw-iam-policy-json); no explicit bucket policy is required                                    |
  </div>
</Accordion>

## Raw IAM policy JSON

The Terraform module creates the following IAM resources per data plane. If your organization requires policies to be pre-approved or manually provisioned before Terraform can attach them, use the JSON below.

`iam_name_prefix` prefixes the IAM role and policy names below, and `s3_name_prefix` prefixes the state bucket name. Both default to `sb-app-db`.

Placeholders to substitute before use:

<div className="dense-table">
  | Placeholder                         | Example value                                                                         | Description                                                                                                                                                                                                                            |
  | ----------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `<REGION>`                          | `us-east-1`                                                                           | AWS region                                                                                                                                                                                                                             |
  | `<ACCOUNT_ID>`                      | `123456789012`                                                                        | 12-digit AWS account ID                                                                                                                                                                                                                |
  | `<VPC_ID>`                          | `vpc-0123456789abcdef0`                                                               | VPC ID declared for this data plane                                                                                                                                                                                                    |
  | `<SUBNET_ID_1>`, `<SUBNET_ID_2>`, … | `subnet-0123456789abcdef0`                                                            | Private subnet IDs for the Aurora subnet group                                                                                                                                                                                         |
  | `<SECURITY_GROUP_ID>`               | `sg-0123456789abcdef0`                                                                | Security group allowed to reach Aurora on port 5432                                                                                                                                                                                    |
  | `<BUCKET_NAME>`                     | `sb-app-db-us-east-1-123456789012`                                                    | S3 state bucket name                                                                                                                                                                                                                   |
  | `<KEY_PREFIX>`                      | `app-db/<AGENT_NAME>`                                                                 | OpenTofu state prefix IAM-scoped to this data plane. Equals `module.app_db_prereqs.agents["<AGENT_NAME>"].key_prefix` (override via `agents[].key_prefix`). See [Agent name](/admin/superblocks-on-aws/database-reference#agent-name). |
  | `<AGENT_NAME>`                      | No default (max 15 characters, lowercase alphanumeric, unique per AWS account)        | [Agent name](/admin/superblocks-on-aws/database-reference#agent-name) for this data plane. Used in IAM role and policy names.                                                                                                          |
  | `<LIFECYCLE_WORKER_ROLE_ARN>`       | `arn:aws:iam::123456789012:role/sb-app-db-<AGENT_NAME>-lifecycle-worker-<REGION>`     | ARN of the lifecycle worker role                                                                                                                                                                                                       |
  | `<CONNECTOR_ROLE_ARN>`              | `arn:aws:iam::123456789012:role/superblocks-app-db-connector-<AGENT_NAME>`            | ARN of the connector role (fixed `superblocks-app-db-connector-<AGENT_NAME>` name; not affected by `iam_name_prefix`)                                                                                                                  |
  | `<OIDC_PROVIDER_ARN>`               | `arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE` | EKS OIDC provider ARN (EKS only)                                                                                                                                                                                                       |
  | `<OIDC_PROVIDER_URL>`               | `oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE`                                         | OIDC URL (the ARN above with `arn:aws:iam::<ACCOUNT_ID>:oidc-provider/` stripped)                                                                                                                                                      |
  | `<ENHANCED_MONITORING_ROLE_ARN>`    | `arn:aws:iam::123456789012:role/sb-app-db-enhanced-monitoring`                        | ARN of the shared Enhanced Monitoring role                                                                                                                                                                                             |
  | `<KMS_KEY_ARN>`                     | `arn:aws:kms:us-east-1:123456789012:key/mrk-example`                                  | Customer-managed KMS key ARN. Used only when `kms_key_arn` is set on `app-db-prereqs` (SSE-KMS). Omit `StateBucketKms` entirely for the SSE-S3 default.                                                                                |
</div>

When creating these manually, create the lifecycle worker role first (it has no dependencies), then the connector role (its trust policy references the lifecycle worker role ARN), then create and attach all policies.

<AccordionGroup>
  <Accordion title="Lifecycle worker role: trust policy (ECS Fargate)">
    Allows the ECS task service principal to assume the role.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": "sts:AssumeRole",
          "Principal": {
            "Service": "ecs-tasks.amazonaws.com"
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Lifecycle worker role: trust policy (EKS with IRSA)">
    Allows the data plane pod's service account to assume the role via OIDC web identity. `<OIDC_PROVIDER_URL>` is the OIDC provider URL without the ARN prefix (everything after `oidc-provider/`). The `sub` condition scopes trust to the specific Kubernetes service account. Adjust `namespace` and `service_account_name` if you deviate from the Superblocks defaults.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": "sts:AssumeRoleWithWebIdentity",
          "Principal": {
            "Federated": "<OIDC_PROVIDER_ARN>"
          },
          "Condition": {
            "StringEquals": {
              "<OIDC_PROVIDER_URL>:aud": "sts.amazonaws.com"
            },
            "StringLike": {
              "<OIDC_PROVIDER_URL>:sub": "system:serviceaccount:superblocks:superblocks-agent"
            }
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 1: Assume connector (sb-app-db-<AGENT_NAME>-assume-connector)">
    Allows the lifecycle worker to assume its connector role for RDS IAM authentication. Attached to the lifecycle worker role.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "AssumeAppDatabaseConnector",
          "Effect": "Allow",
          "Action": "sts:AssumeRole",
          "Resource": ["<CONNECTOR_ROLE_ARN>"]
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 2: State backend (sb-app-db-<AGENT_NAME>-state-bucket-<REGION>)">
    Allows the lifecycle worker to read and write OpenTofu state in the shared S3 bucket under that agent's `key_prefix` only (default `app-db/<AGENT_NAME>`; override via `agents["<AGENT_NAME>"].key_prefix`). Replace `<KEY_PREFIX>` with the same string you wire into the data plane (see [Agent name](/admin/superblocks-on-aws/database-reference#agent-name)).

    `GetBucketLocation` / `GetBucketVersioning` live in a separate statement from `ListBucket` because those metadata APIs do not send `s3:prefix` and would fail closed if they shared the list condition.

    The default bucket uses SSE-S3. The released module emits **no** `StateBucketKms` statement in that case. Do not add a `Resource: "*"` KMS grant to match older examples; that widens decrypt for brownfield roles that already have broader S3 permissions.

    `StateBucketKms` is present only when you set `kms_key_arn` (SSE-KMS). It names that key ARN and has no `CalledVia` condition:

    ```json theme={null}
    {
      "Sid": "StateBucketKms",
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt",
        "kms:DescribeKey",
        "kms:Encrypt",
        "kms:GenerateDataKey",
        "kms:ReEncryptFrom",
        "kms:ReEncryptTo"
      ],
      "Resource": "<KMS_KEY_ARN>"
    }
    ```

    The default SSE-S3 policy is the three S3 statements below.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "StateBucketMetadata",
          "Effect": "Allow",
          "Action": [
            "s3:GetBucketLocation",
            "s3:GetBucketVersioning"
          ],
          "Resource": "arn:aws:s3:::<BUCKET_NAME>"
        },
        {
          "Sid": "StateBucketList",
          "Effect": "Allow",
          "Action": [
            "s3:ListBucket"
          ],
          "Resource": "arn:aws:s3:::<BUCKET_NAME>",
          "Condition": {
            "StringLike": {
              "s3:prefix": [
                "<KEY_PREFIX>/",
                "<KEY_PREFIX>/*"
              ]
            }
          }
        },
        {
          "Sid": "StateBucketObjectReadWrite",
          "Effect": "Allow",
          "Action": [
            "s3:AbortMultipartUpload",
            "s3:DeleteObject",
            "s3:GetObject",
            "s3:GetObjectVersion",
            "s3:PutObject"
          ],
          "Resource": "arn:aws:s3:::<BUCKET_NAME>/<KEY_PREFIX>/*"
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 3: RDS provisioning (sb-app-db-<AGENT_NAME>-rds-provisioning-<REGION>)">
    Allows the lifecycle worker to describe RDS and Aurora resources globally (required by AWS, because describe APIs do not support resource-level restrictions) and to create new RDS and Aurora resources scoped to the `sb-*` namespace and the declared VPC. All create actions enforce tagging at request time, `publicly_accessible = false`, and `manage_master_user_password = true`. The `RdsCreateDbInstance` and `RdsCreateAuroraClusterInstance` statements both cover `rds:CreateDBInstance` but are cleanly discriminated: standalone RDS creates pass `rds:ManageMasterUserPassword = true` and Aurora member creates do not, so each request matches exactly one statement. `iam:CreateServiceLinkedRole` is scoped to `rds.amazonaws.com`; it runs once, and only if your account does not already have the RDS service-linked role.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "RdsDescribe",
          "Effect": "Allow",
          "Action": [
            "rds:DescribeDBClusterParameterGroups",
            "rds:DescribeDBClusterParameters",
            "rds:DescribeDBClusterSnapshots",
            "rds:DescribeDBClusters",
            "rds:DescribeDBEngineVersions",
            "rds:DescribeDBInstances",
            "rds:DescribeDBParameterGroups",
            "rds:DescribeDBParameters",
            "rds:DescribeDBSnapshots",
            "rds:DescribeDBSubnetGroups",
            "rds:DescribeGlobalClusters",
            "rds:DescribePendingMaintenanceActions",
            "rds:ListTagsForResource"
          ],
          "Resource": "*"
        },
        {
          "Sid": "CreateRdsServiceLinkedRole",
          "Effect": "Allow",
          "Action": "iam:CreateServiceLinkedRole",
          "Resource": "*",
          "Condition": {
            "StringEquals": {
              "iam:AWSServiceName": "rds.amazonaws.com"
            }
          }
        },
        {
          "Sid": "RdsCreateDbInstance",
          "Effect": "Allow",
          "Action": "rds:CreateDBInstance",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:*"
          ],
          "Condition": {
            "Bool": {
              "rds:ManageMasterUserPassword": "true",
              "rds:PubliclyAccessible": "false",
              "rds:StorageEncrypted": "true"
            },
            "StringEquals": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>",
              "rds:DatabaseEngine": "postgres"
            }
          }
        },
        {
          "Sid": "RdsCreateAuroraCluster",
          "Effect": "Allow",
          "Action": "rds:CreateDBCluster",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-pg:*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*"
          ],
          "Condition": {
            "Bool": {
              "rds:ManageMasterUserPassword": "true",
              "rds:StorageEncrypted": "true"
            },
            "StringEquals": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>",
              "rds:DatabaseEngine": "aurora-postgresql"
            }
          }
        },
        {
          "Sid": "RdsCreateAuroraClusterInstance",
          "Effect": "Allow",
          "Action": "rds:CreateDBInstance",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*"
          ],
          "Condition": {
            "Bool": {
              "rds:PubliclyAccessible": "false"
            },
            "StringEquals": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>",
              "rds:DatabaseEngine": "aurora-postgresql"
            }
          }
        },
        {
          "Sid": "RdsCreateParameterGroups",
          "Effect": "Allow",
          "Action": [
            "rds:CreateDBClusterParameterGroup",
            "rds:CreateDBParameterGroup"
          ],
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:sb-*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsCreateDbSubnetGroup",
          "Effect": "Allow",
          "Action": "rds:CreateDBSubnetGroup",
          "Resource": "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*",
          "Condition": {
            "StringEquals": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsTagOnCreate",
          "Effect": "Allow",
          "Action": "rds:AddTagsToResource",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            },
            "StringEqualsIfExists": {
              "aws:RequestTag/aws-apn-id": "pc:ctelqp437y3cvjkv5rv0z2w4f",
              "aws:RequestTag/superblocks:owned": "true"
            }
          }
        },
        {
          "Sid": "RdsTagAppSnapshotOnCreate",
          "Effect": "Allow",
          "Action": "rds:AddTagsToResource",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-snapshot:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:snapshot:sb-*"
          ],
          "Condition": {
            "StringEqualsIfExists": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>",
              "aws:RequestTag/aws-apn-id": "pc:ctelqp437y3cvjkv5rv0z2w4f",
              "aws:RequestTag/superblocks:owned": "true"
            }
          }
        },
        {
          "Sid": "RdsEncryptedStorageKmsViaRds",
          "Effect": "Allow",
          "Action": [
            "kms:CreateGrant",
            "kms:Decrypt",
            "kms:DescribeKey",
            "kms:GenerateDataKey"
          ],
          "Resource": "*",
          "Condition": {
            "ForAnyValue:StringEquals": {
              "aws:CalledVia": "rds.amazonaws.com"
            }
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 4: RDS mutation (sb-app-db-<AGENT_NAME>-rds-mutation-<REGION>)">
    Allows the lifecycle worker to modify, delete, snapshot, and retag RDS and Aurora resources it created. The target must already carry the `AgentName`, `ManagedBy`, and `Vpc` tags, so the lifecycle worker cannot modify any RDS resource it did not create. `RdsRemoveTagsExceptProtectedTags` only Allows removing non-protected tags. `DenyRemoveProtectedTags` is an explicit Deny that blocks removing `AgentName`, `ManagedBy`, `Vpc`, `superblocks:owned`, or `aws-apn-id`, so AccessDenied is attributable to that Sid.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "RdsMutate",
          "Effect": "Allow",
          "Action": [
            "rds:DeleteDBCluster",
            "rds:DeleteDBClusterParameterGroup",
            "rds:DeleteDBInstance",
            "rds:DeleteDBParameterGroup",
            "rds:DeleteDBSubnetGroup",
            "rds:ModifyDBCluster",
            "rds:ModifyDBClusterParameterGroup",
            "rds:ModifyDBInstance",
            "rds:ModifyDBParameterGroup",
            "rds:ModifyDBSubnetGroup",
            "rds:RebootDBCluster",
            "rds:RebootDBInstance",
            "rds:ResetDBClusterParameterGroup",
            "rds:ResetDBParameterGroup"
          ],
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*"
          ],
          "Condition": {
            "BoolIfExists": {
              "rds:ManageMasterUserPassword": "true"
            },
            "StringEquals": {
              "aws:ResourceTag/AgentName": "<AGENT_NAME>",
              "aws:ResourceTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsDeleteAuroraClusterFinalSnapshot",
          "Effect": "Allow",
          "Action": "rds:DeleteDBCluster",
          "Resource": "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-snapshot:sb-*",
          "Condition": {
            "StringEqualsIfExists": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsCreateSnapshotFromManagedCluster",
          "Effect": "Allow",
          "Action": "rds:CreateDBClusterSnapshot",
          "Resource": "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/AgentName": "<AGENT_NAME>",
              "aws:ResourceTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsCreateAppClusterSnapshot",
          "Effect": "Allow",
          "Action": "rds:CreateDBClusterSnapshot",
          "Resource": "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-snapshot:sb-*",
          "Condition": {
            "StringEqualsIfExists": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsCreateSnapshotFromManagedInstance",
          "Effect": "Allow",
          "Action": "rds:CreateDBSnapshot",
          "Resource": "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/AgentName": "<AGENT_NAME>",
              "aws:ResourceTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsCreateAppSnapshot",
          "Effect": "Allow",
          "Action": "rds:CreateDBSnapshot",
          "Resource": "arn:aws:rds:<REGION>:<ACCOUNT_ID>:snapshot:sb-*",
          "Condition": {
            "StringEqualsIfExists": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsAddTagsToManagedResources",
          "Effect": "Allow",
          "Action": "rds:AddTagsToResource",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-snapshot:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:snapshot:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/AgentName": "<AGENT_NAME>",
              "aws:ResourceTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            },
            "StringEqualsIfExists": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>",
              "aws:RequestTag/aws-apn-id": "pc:ctelqp437y3cvjkv5rv0z2w4f",
              "aws:RequestTag/superblocks:owned": "true"
            }
          }
        },
        {
          "Sid": "RdsRemoveTagsExceptProtectedTags",
          "Effect": "Allow",
          "Action": "rds:RemoveTagsFromResource",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-snapshot:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:snapshot:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/AgentName": "<AGENT_NAME>",
              "aws:ResourceTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            },
            "ForAllValues:StringNotEquals": {
              "aws:TagKeys": ["AgentName", "ManagedBy", "Vpc", "aws-apn-id", "superblocks:owned"]
            }
          }
        },
        {
          "Sid": "DenyRemoveProtectedTags",
          "Effect": "Deny",
          "Action": "rds:RemoveTagsFromResource",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-snapshot:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:snapshot:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*"
          ],
          "Condition": {
            "ForAnyValue:StringEquals": {
              "aws:TagKeys": ["AgentName", "ManagedBy", "Vpc", "aws-apn-id", "superblocks:owned"]
            }
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 5: EC2 networking (sb-app-db-<AGENT_NAME>-ec2-provisioning-<REGION>)">
    Allows the lifecycle worker to describe VPC and subnet topology (globally, as required by AWS) and to create and manage security groups scoped to the declared VPC. The `Ec2CreateSecurityGroupVpcScope` statement additionally restricts `CreateSecurityGroup` to the specific VPC resource via `ec2:VpcID`, so the lifecycle worker cannot create security groups in any other VPC. `Ec2CreateTagsOnCreateSecurityGroup` authorizes create-time tagging for `CreateSecurityGroup` and for `AuthorizeSecurityGroupIngress` / `AuthorizeSecurityGroupEgress`, so ownership tags can land on both the security group and its individual rules when they are created. `Ec2DeleteTagsExceptProtectedTags` only Allows removing non-protected tags from managed security groups and rules when `aws:TagKeys` is present; `DenyDeleteProtectedTags` Denies removing the scoping and ownership keys; `DenyDeleteTagsWhenTagKeysAbsent` Denies the delete-all request that omits `Tags` / `aws:TagKeys`.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "Ec2VpcDescribe",
          "Effect": "Allow",
          "Action": [
            "ec2:DescribeAccountAttributes",
            "ec2:DescribeAvailabilityZones",
            "ec2:DescribeNetworkInterfaces",
            "ec2:DescribeRouteTables",
            "ec2:DescribeSecurityGroupRules",
            "ec2:DescribeSecurityGroups",
            "ec2:DescribeSubnets",
            "ec2:DescribeTags",
            "ec2:DescribeVpcAttribute",
            "ec2:DescribeVpcs"
          ],
          "Resource": "*"
        },
        {
          "Sid": "Ec2CreateSecurityGroupResource",
          "Effect": "Allow",
          "Action": ["ec2:CreateSecurityGroup"],
          "Resource": "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group/*",
          "Condition": {
            "StringEquals": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "Ec2CreateSecurityGroupVpcScope",
          "Effect": "Allow",
          "Action": ["ec2:CreateSecurityGroup"],
          "Resource": "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:vpc/<VPC_ID>",
          "Condition": {
            "StringEquals": {
              "ec2:VpcID": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "Ec2SecurityGroupMutate",
          "Effect": "Allow",
          "Action": [
            "ec2:AuthorizeSecurityGroupEgress",
            "ec2:AuthorizeSecurityGroupIngress",
            "ec2:DeleteSecurityGroup",
            "ec2:ModifySecurityGroupRules",
            "ec2:RevokeSecurityGroupEgress",
            "ec2:RevokeSecurityGroupIngress"
          ],
          "Resource": "*",
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/AgentName": "<AGENT_NAME>",
              "aws:ResourceTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "Ec2CreateTagsOnCreateSecurityGroup",
          "Effect": "Allow",
          "Action": ["ec2:CreateTags"],
          "Resource": [
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group/*",
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group-rule/*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>",
              "ec2:CreateAction": [
                "AuthorizeSecurityGroupEgress",
                "AuthorizeSecurityGroupIngress",
                "CreateSecurityGroup"
              ]
            },
            "StringEqualsIfExists": {
              "aws:RequestTag/aws-apn-id": "pc:ctelqp437y3cvjkv5rv0z2w4f",
              "aws:RequestTag/superblocks:owned": "true"
            }
          }
        },
        {
          "Sid": "Ec2CreateTagsOnManagedResources",
          "Effect": "Allow",
          "Action": ["ec2:CreateTags"],
          "Resource": [
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group/*",
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group-rule/*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/AgentName": "<AGENT_NAME>",
              "aws:ResourceTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            },
            "StringEqualsIfExists": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>",
              "aws:RequestTag/aws-apn-id": "pc:ctelqp437y3cvjkv5rv0z2w4f",
              "aws:RequestTag/superblocks:owned": "true"
            }
          }
        },
        {
          "Sid": "Ec2DeleteTagsExceptProtectedTags",
          "Effect": "Allow",
          "Action": ["ec2:DeleteTags"],
          "Resource": [
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group/*",
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group-rule/*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/AgentName": "<AGENT_NAME>",
              "aws:ResourceTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            },
            "Null": {
              "aws:TagKeys": "false"
            },
            "ForAllValues:StringNotEquals": {
              "aws:TagKeys": ["AgentName", "ManagedBy", "Vpc", "aws-apn-id", "superblocks:owned"]
            }
          }
        },
        {
          "Sid": "DenyDeleteProtectedTags",
          "Effect": "Deny",
          "Action": ["ec2:DeleteTags"],
          "Resource": [
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group/*",
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group-rule/*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/AgentName": "<AGENT_NAME>",
              "aws:ResourceTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            },
            "ForAnyValue:StringEquals": {
              "aws:TagKeys": ["AgentName", "ManagedBy", "Vpc", "aws-apn-id", "superblocks:owned"]
            }
          }
        },
        {
          "Sid": "DenyDeleteTagsWhenTagKeysAbsent",
          "Effect": "Deny",
          "Action": ["ec2:DeleteTags"],
          "Resource": [
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group/*",
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group-rule/*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/AgentName": "<AGENT_NAME>",
              "aws:ResourceTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            },
            "Null": {
              "aws:TagKeys": "true"
            }
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 6: Secrets Manager (sb-app-db-<AGENT_NAME>-secrets-<REGION>)">
    Allows the lifecycle worker to read RDS-managed master secrets for databases it owns. Secret creation is only permitted via `aws:CalledVia: rds.amazonaws.com`, so the lifecycle worker cannot directly create Secrets Manager secrets; only RDS can, on its behalf. The worker reads these secrets by ref when provisioning logical databases, so there is no plaintext master credential in Superblocks systems.

    The last statement (`DecryptRdsManagedSecretKmsKey`) handles KMS decryption for the RDS-managed master secret. **Without a customer KMS key** (`rds_secret_kms_key_arn` not set), `Resource` is `"*"` and access is constrained via `kms:ViaService` and `kms:EncryptionContext` as shown below. **With a customer KMS key**, replace `"Resource": "*"` with the specific key ARN.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "CreateRdsManagedMasterSecrets",
          "Effect": "Allow",
          "Action": "secretsmanager:CreateSecret",
          "Resource": [
            "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!cluster-*",
            "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!db-*"
          ],
          "Condition": {
            "ForAnyValue:StringEquals": {
              "aws:CalledVia": "rds.amazonaws.com"
            },
            "StringEquals": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "TagRdsManagedMasterSecretsViaRds",
          "Effect": "Allow",
          "Action": "secretsmanager:TagResource",
          "Resource": [
            "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!cluster-*",
            "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!db-*"
          ],
          "Condition": {
            "ForAnyValue:StringEquals": {
              "aws:CalledVia": "rds.amazonaws.com"
            },
            "StringEquals": {
              "aws:RequestTag/AgentName": "<AGENT_NAME>",
              "aws:RequestTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "DescribeRdsManagedSecretKmsKeyViaRds",
          "Effect": "Allow",
          "Action": "kms:DescribeKey",
          "Resource": "*",
          "Condition": {
            "ForAnyValue:StringEquals": {
              "aws:CalledVia": "rds.amazonaws.com"
            }
          }
        },
        {
          "Sid": "ReadTaggedRdsManagedMasterSecrets",
          "Effect": "Allow",
          "Action": [
            "secretsmanager:DescribeSecret",
            "secretsmanager:GetSecretValue"
          ],
          "Resource": [
            "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!cluster-*",
            "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!db-*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/AgentName": "<AGENT_NAME>",
              "aws:ResourceTag/ManagedBy": "superblocks-app-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "DecryptRdsManagedSecretKmsKey",
          "Effect": "Allow",
          "Action": [
            "kms:Decrypt",
            "kms:DescribeKey"
          ],
          "Resource": "*",
          "Condition": {
            "StringEquals": {
              "kms:ViaService": "secretsmanager.<REGION>.amazonaws.com"
            },
            "StringLike": {
              "kms:EncryptionContext:SecretARN": [
                "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!cluster-*",
                "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!db-*"
              ]
            }
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Connector role: trust policy">
    Allows only the lifecycle worker role for this data plane to assume the connector role.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "AllowOnlyTrustedOpa",
          "Effect": "Allow",
          "Action": "sts:AssumeRole",
          "Principal": {
            "AWS": ["<LIFECYCLE_WORKER_ROLE_ARN>"]
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Connector role: policy (superblocks-app-db-connector-<AGENT_NAME>)">
    Grants `rds-db:connect` for each declared data tag, scoped to DB users matching `sbndb_<profile-token>_*_runtime` on both RDS instances and Aurora clusters. `<profile-token>` is the first 16 hex characters of `SHA-256(lowercase(tag))`, which you can compute with `printf '%s' '<DATA_TAG>' | shasum -a 256 | cut -c1-16`. Add or remove statements to match your declared `agent_tags`. The example below shows two data tags (`<PROFILE_TOKEN_1>` and `<PROFILE_TOKEN_2>`).

    This is the outer bound, not the effective permission on a connection. See [Connecting](#connecting).

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "ConnectTag1",
          "Effect": "Allow",
          "Action": "rds-db:connect",
          "Resource": [
            "arn:aws:rds-db:<REGION>:<ACCOUNT_ID>:dbuser:cluster-*/sbndb_<PROFILE_TOKEN_1>_*_runtime",
            "arn:aws:rds-db:<REGION>:<ACCOUNT_ID>:dbuser:db-*/sbndb_<PROFILE_TOKEN_1>_*_runtime"
          ]
        },
        {
          "Sid": "ConnectTag2",
          "Effect": "Allow",
          "Action": "rds-db:connect",
          "Resource": [
            "arn:aws:rds-db:<REGION>:<ACCOUNT_ID>:dbuser:cluster-*/sbndb_<PROFILE_TOKEN_2>_*_runtime",
            "arn:aws:rds-db:<REGION>:<ACCOUNT_ID>:dbuser:db-*/sbndb_<PROFILE_TOKEN_2>_*_runtime"
          ]
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 7: Observability (sb-app-db-<AGENT_NAME>-observability-<REGION>)">
    Allows the lifecycle worker to manage CloudWatch log groups for Aurora clusters and instances it provisions, and to pass the shared Enhanced Monitoring role to RDS.

    `logs:DescribeLogGroups` must be scoped to `"Resource": "*"` because AWS does not support resource-level restrictions on describe log group APIs.

    `logs:UntagResource` is split into an Allow that excludes protected keys (`CloudWatchUntagExceptProtectedTags`) and an explicit Deny (`DenyUntagProtectedTags`), matching the RDS/EC2 protected-tag pattern. `logs:TagResource` is similarly split (`CloudWatchTagResourceWithCanonicalOwnership`) so ownership request-tag values cannot be overwritten.

    `iam:PassRole` is conditioned on `iam:PassedToService: rds.amazonaws.com`, which is the service principal that RDS uses when calling `CreateDBInstance`. It is not `monitoring.rds.amazonaws.com`, which is what the monitoring agent assumes later.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "CloudWatchLogGroupsForAppDatabases",
          "Effect": "Allow",
          "Action": [
            "logs:CreateLogGroup",
            "logs:DeleteLogGroup",
            "logs:ListTagsForResource",
            "logs:PutRetentionPolicy"
          ],
          "Resource": [
            "arn:aws:logs:<REGION>:<ACCOUNT_ID>:log-group:/aws/rds/cluster/sb-*",
            "arn:aws:logs:<REGION>:<ACCOUNT_ID>:log-group:/aws/rds/instance/sb-*"
          ]
        },
        {
          "Sid": "CloudWatchTagResourceWithCanonicalOwnership",
          "Effect": "Allow",
          "Action": "logs:TagResource",
          "Resource": [
            "arn:aws:logs:<REGION>:<ACCOUNT_ID>:log-group:/aws/rds/cluster/sb-*",
            "arn:aws:logs:<REGION>:<ACCOUNT_ID>:log-group:/aws/rds/instance/sb-*"
          ],
          "Condition": {
            "StringEqualsIfExists": {
              "aws:RequestTag/aws-apn-id": "pc:ctelqp437y3cvjkv5rv0z2w4f",
              "aws:RequestTag/superblocks:owned": "true"
            }
          }
        },
        {
          "Sid": "CloudWatchUntagExceptProtectedTags",
          "Effect": "Allow",
          "Action": "logs:UntagResource",
          "Resource": [
            "arn:aws:logs:<REGION>:<ACCOUNT_ID>:log-group:/aws/rds/cluster/sb-*",
            "arn:aws:logs:<REGION>:<ACCOUNT_ID>:log-group:/aws/rds/instance/sb-*"
          ],
          "Condition": {
            "ForAllValues:StringNotEquals": {
              "aws:TagKeys": ["AgentName", "ManagedBy", "Vpc", "aws-apn-id", "superblocks:owned"]
            }
          }
        },
        {
          "Sid": "DenyUntagProtectedTags",
          "Effect": "Deny",
          "Action": "logs:UntagResource",
          "Resource": [
            "arn:aws:logs:<REGION>:<ACCOUNT_ID>:log-group:/aws/rds/cluster/sb-*",
            "arn:aws:logs:<REGION>:<ACCOUNT_ID>:log-group:/aws/rds/instance/sb-*"
          ],
          "Condition": {
            "ForAnyValue:StringEquals": {
              "aws:TagKeys": ["AgentName", "ManagedBy", "Vpc", "aws-apn-id", "superblocks:owned"]
            }
          }
        },
        {
          "Sid": "DescribeLogGroupsIsNotResourceScopable",
          "Effect": "Allow",
          "Action": "logs:DescribeLogGroups",
          "Resource": "*"
        },
        {
          "Sid": "PassEnhancedMonitoringRole",
          "Effect": "Allow",
          "Action": "iam:PassRole",
          "Resource": "<ENHANCED_MONITORING_ROLE_ARN>",
          "Condition": {
            "StringEquals": {
              "iam:PassedToService": "rds.amazonaws.com"
            }
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Enhanced Monitoring role: trust and permissions">
    `app_db_prereqs` creates one account-level role named `sb-app-db-enhanced-monitoring` (or `<iam_name_prefix>-enhanced-monitoring`) with the following trust policy. Additional regional prerequisite stacks reuse this same role through `existing_monitoring_role_arn`. If your organization must create IAM roles out of band, create the role with this policy and pass its ARN as `existing_monitoring_role_arn` to every prerequisite invocation.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "AllowRdsEnhancedMonitoringForAppDatabases",
          "Effect": "Allow",
          "Principal": {
            "Service": "monitoring.rds.amazonaws.com"
          },
          "Action": "sts:AssumeRole",
          "Condition": {
            "ArnLike": {
              "aws:SourceArn": [
                "arn:aws:rds:*:<ACCOUNT_ID>:cluster:sb-*",
                "arn:aws:rds:*:<ACCOUNT_ID>:db:sb-*"
              ]
            },
            "StringEquals": {
              "aws:SourceAccount": "<ACCOUNT_ID>"
            }
          }
        }
      ]
    }
    ```

    The module attaches the AWS-managed `arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole` policy to this role. If you create the role out of band, attach the same policy. The trust policy uses `monitoring.rds.amazonaws.com`, while the lifecycle worker's `iam:PassRole` condition above intentionally uses `rds.amazonaws.com`; these principals serve different parts of the Enhanced Monitoring flow.
  </Accordion>
</AccordionGroup>
