> ## 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, and the mechanics behind setup choices that can't be changed later

This page documents the mechanics behind Superblocks Database: the AWS resources it creates, how IAM permissions are scoped, and what's behind the setup choices that can't be changed later. If you're doing initial setup, start with [Setup](/admin/superblocks-on-aws/database-setup).

## Setup decisions

Three values are fixed once a data plane has databases. There's no automated migration path today — changing any of them requires manual recovery.

### Agent name

The agent name is the `agents` map key for a data plane — `agents = { "<AGENT_NAME>" = { ... } }` — reused as Fargate `agent_name` or Helm `superblocks.agentName`. It's the identity Superblocks uses everywhere the data plane touches AWS:

* **IAM scoping.** Every create and mutate is gated on the tag `AgentName=<AGENT_NAME>`, stamped by the lifecycle worker on every resource it creates.
* **Role and policy naming.** Each data plane gets its own lifecycle worker and connector role.
* **OpenTofu state isolation.** `app_db_prereqs` derives this data plane's state prefix from it (default `app-db/<AGENT_NAME>`), scopes S3 permissions to that prefix, and exports it as `agents["<AGENT_NAME>"].key_prefix`.

A name or prefix that doesn't match what created the roles fails every call with `AccessDenied`.

Changing the agent name means Terraform destroys and recreates the IAM roles under the new name — the data plane can't manage its database pool again until it's redeployed with the new role ARNs. No data is lost, but it's a coordinated migration, not a rename.

### 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.

### 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.

## How databases go from edit to deployed

A database is created when a builder with database creation permission asks for one in the Superblocks editor. If an existing Aurora PostgreSQL cluster has capacity under your scaling settings, Superblocks queues a task to create an isolated logical database on that cluster. Your data plane picks tasks up from the queue and applies them.

Clark generates the database schema and stores it as `.sql` files in the application code. Once the database is ready, Clark queues a task for the data plane to run the migrations.

Databases follow your data plane layout and the [data tags](/development-lifecycle/build/data-tags) each data plane serves. When an app is deployed, Superblocks can create a separate database for the deploy-time data tag before the deploy finishes.

The diagram below traces one app's database end to end. While a builder is still editing, Clark asks for a database, your data plane creates it, and Clark's generated schema is applied. On deploy, the same sequence can run again against a separate database for the deploy-time data tag. Superblocks only queues work; your data plane claims it and calls AWS.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Builder
    participant Clark as Clark
    participant SB as Superblocks control plane
    participant DP as Data plane lifecycle worker
    participant AWS as Your AWS account

    Builder->>Clark: Ask for a feature that needs persistent data
    Clark->>SB: Request a database for this app
    SB->>SB: Queue a lifecycle task
    DP->>SB: Claim the pending task
    DP->>AWS: tofu apply: create isolated logical database
    AWS-->>DP: Database ready
    DP->>SB: Ready callback
    Clark->>Clark: Generate schema as .sql files in the app code
    Clark->>SB: Queue a migration task
    DP->>AWS: Apply migrations to the edit-time database
    Builder->>SB: Deploy the app
    SB->>DP: Create the deploy-time database, then apply migrations
    DP->>AWS: tofu apply plus migrations
    SB-->>Builder: Deploy proceeds once the deploy-time database is ready
```

## What Superblocks creates in your AWS account

Resources arrive in two waves with very different lifetimes. Setup creates a small, fixed set of control resources that live as long as the feature is enabled. Everything else is created on demand as the pool grows, and those are the resources you manage day to day.

### At setup: control resources

The Terraform module creates these once. They are inexpensive to keep, and removing them disables provisioning.

**Lifecycle worker IAM role**: your data plane assumes this role to manage database infrastructure. Its permissions are narrowly scoped: every mutation (create, modify, delete) is tag-gated, so the role can only act on resources that Superblocks itself tagged at creation time. It cannot touch resources in other VPCs or resources it did not create. See [IAM permissions](#iam-permissions) for the full breakdown.

**Connector IAM role**: one role per data plane. This is the identity your data plane uses to reach app databases at query time. To run a query, the data plane assumes this role and uses the resulting credentials to sign a short-lived RDS IAM auth token, which PostgreSQL accepts in place of a password. Provisioning creates the PostgreSQL role the token authenticates as and grants it `rds_iam`; the PostgreSQL role name follows `sbndb_<profile-token>_<application-token>_runtime` (see [Connector role](#connector-role)), and is unrelated to the connector role's ARN. The trust policy limits who can assume this role to the lifecycle worker role for that data plane. See [Connector role](#connector-role) for how the permissions are narrowed on each connection.

**Enhanced Monitoring IAM role**: one account-level role shared by every App Database cluster and instance. RDS assumes this role to publish operating-system metrics. The lifecycle worker can pass only this role, and only to `rds.amazonaws.com`.

**S3 bucket**: stores OpenTofu state so Superblocks can manage your database resources over time (modify, delete, track drift). Key properties:

* One bucket per account and region
* Versioning enabled, so state can be recovered if corrupted
* Public access blocked
* Encrypted at rest with **SSE-S3** by default (AWS-managed keys, no KMS charges). Optionally bring your own KMS key via `kms_key_arn` to use SSE-KMS. When provided, the lifecycle worker IAM policy scopes KMS access to that specific key ARN only
* Superblocks reads and writes to this bucket; you own and retain it
* Each data plane is IAM-scoped to its own prefix (default `app-db/<AGENT_NAME>`, exported as `agents["<AGENT_NAME>"].key_prefix`). State keys follow `{key_prefix}/{tag}/{resource_key}.tfstate`

### As the pool grows: one set per Aurora cluster

`pool.max_databases` (Fargate) / `pool.maxDatabases` (EKS) caps how many app databases land on one cluster before another is added to the pool. Lowering it 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. Along with the ACU range, this is the main control over what the pool costs as it grows.

Your data plane creates the following each time it adds a cluster to the pool (first when you click **Provision**, and again whenever existing clusters reach `max_databases`):

* **Aurora PostgreSQL cluster and its instances**: named `sb-{hash}`, IAM authentication enabled, public accessibility disabled, storage encrypted at rest, and the master password managed by AWS Secrets Manager so there are no plaintext passwords
* **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**: logs every statement running for at least 5 seconds, samples 10% of statements running between 1 and 5 seconds, and excludes bound parameter values
* **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 below.

Individual app databases are logical databases inside these clusters, each with its own PostgreSQL role. They are not separate AWS resources, so they do not appear in the RDS console as their own entries.

### Observability defaults

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

Enhanced Monitoring is optional — see the `monitoring_interval` comment in [Step 1](/admin/superblocks-on-aws/database-setup#step-1-run-the-terraform-module). 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. Your organization controls access to CloudWatch, pays any associated AWS charges, and owns the dashboards, alarms, subscriptions, and incident-response processes it builds on top.

### 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 identify Superblocks-managed resources and attribute them to Superblocks' AWS Partner Network ID. They do **not** change who pays the bill or which AWS account owns the resources — that remains your account. `aws-apn-id` is a fixed Superblocks constant (`pc:ctelqp437y3cvjkv5rv0z2w4f`); copy it verbatim — it is not per-account.

`app-db-prereqs` enforces both keys on setup resources. The Fargate `app-db` module merges the same pair into runtime module inputs, overwriting conflicting caller values. On EKS, the Helm chart does **not** inject them — set both keys under `databaseLifecycle.physicalModuleTags` (see [Step 1](/admin/superblocks-on-aws/database-setup#step-1-run-the-terraform-module)). Without them, IAM create on runtime Aurora, RDS, subnet groups, and security groups fails with `AccessDenied`. The lifecycle worker IAM policy also denies removing either key from runtime resources.

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.

Find everything the feature manages with a Resource Group or the CLI:

```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 bucket is created unconditionally: one per account and region, shared across all data planes in the same module invocation. Default naming is `sb-app-db-<REGION>-<ACCOUNT_ID>`, where the `sb-app-db` prefix comes from the `s3_name_prefix` variable.

When creating it manually, configure the following properties:

| 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-KMS only when `kms_key_arn` is provided; use the same key ARN supplied to the module                                                                                                                                                                 |
| S3 bucket key                      | Disabled                    | Bucket keys cache a per-bucket KMS data key to reduce KMS API call volume. The module disables this (`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 entirely via the IAM policies above; no explicit bucket policy is required                                                                                                                                                          |

## Security

| What                | Details                                                                                                                 |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Authentication      | IAM token via `rds-db:connect`: no passwords stored or rotated                                                          |
| Network             | The database lives in your private subnets; `publicly_accessible = false` is enforced by IAM policy                     |
| Encryption (S3)     | SSE-S3 by default (AWS-managed keys); optionally SSE-KMS with your own key via `kms_key_arn`                            |
| Encryption (Aurora) | Storage encrypted at rest; TLS in transit                                                                               |
| IAM scope           | All lifecycle worker mutations are tag-gated to resources Superblocks created in your VPC                               |
| State storage       | OpenTofu state lives in your S3 bucket; Superblocks does not retain a copy                                              |
| Master credentials  | Managed by AWS Secrets Manager; the lifecycle worker reads them by ref, so there is no plaintext in Superblocks systems |

### IAM permissions

#### Lifecycle worker role

Your data plane runs as this role to manage database infrastructure. Its policies cover state, RDS provisioning and mutation, EC2 networking, Secrets Manager, and observability.

**State backend policy.** Scoped exclusively to the S3 bucket the Terraform module creates, with no wildcards:

* **S3**: `ListBucket` (prefix-conditioned), `GetObject`, `PutObject`, `DeleteObject` under that agent's `key_prefix` in the state bucket only
* **KMS** (only when `kms_key_arn` is provided): `Encrypt`, `Decrypt`, `GenerateDataKey` on the specific key ARN. Superblocks cannot use any other KMS key in your account.

**RDS provisioning policy.** This is the largest policy. **Every mutation is tag-gated**: the lifecycle worker can only create, modify, or delete resources carrying `AgentName={agent-map-key}`, `ManagedBy=superblocks-app-database-lifecycle`, and `Vpc={your-vpc-id}`. Create actions must supply all three tags as request tags. Delete and modify actions require all three tags to already exist on the target resource. The IAM policy only permits removing tags other than `AgentName`, `ManagedBy`, `Vpc`, `superblocks:owned`, and `aws-apn-id`, so the scoping and ownership tags cannot be stripped. An explicit Deny Sid (`DenyRemoveProtectedTags` / `DenyDeleteProtectedTags`) makes that failure attributable in IAM diagnostics.

Additional constraints enforced in the policy:

* RDS instance ARNs are restricted to the `sb-*` namespace, so Superblocks cannot modify your existing RDS instances
* `rds:PubliclyAccessible` must be `false` on all create actions: the policy rejects any create that asks for a public endpoint
* `rds:ManageMasterUserPassword` must be `true` on standalone RDS creates, so master credentials are always managed by Secrets Manager and never set as a static password. This condition also discriminates standalone RDS creates from Aurora cluster member creates: Aurora member creates do not pass this key (the master password is inherited from the cluster), so `rds:ManageMasterUserPassword` evaluates to false and only the Aurora member statement matches.
* `rds:DatabaseEngine` is pinned to `postgres` (standalone RDS) and `aurora-postgresql` (Aurora cluster) at create time, so the lifecycle worker cannot provision any other engine type
* `rds:StorageEncrypted` must be `true` on standalone RDS and Aurora cluster creates. Storage encryption is enforced by IAM at create time, not only by module configuration.
* Security group mutations are scoped to the same `AgentName`, `ManagedBy`, and `Vpc` tag triple
* EC2 security group creation is additionally scoped to the specific VPC ID via the `ec2:VpcID` condition, so Superblocks cannot create resources in other VPCs in your account
* KMS access for RDS storage encryption is gated on `aws:CalledVia: rds.amazonaws.com`, so Superblocks cannot use your KMS keys directly

Allowed describe and list actions (`DescribeDBInstances`, `DescribeDBSubnetGroups`, `DescribeVpcs`, `DescribeSubnets`, and similar) are read-only and apply to `"Resource": "*"`. This is required by AWS, because describe APIs do not support resource-level restrictions, but no data is retained by Superblocks.

**Master secrets policy.** Scoped to RDS-managed master secrets (`rds!db-*` and `rds!cluster-*` ARN prefixes):

* **Read**: `GetSecretValue` and `DescribeSecret` on RDS-managed secrets tagged with `AgentName`, `ManagedBy`, and `Vpc`: the master credentials for databases Superblocks created
* **Write** (create and tag): only allowed via `aws:CalledVia: rds.amazonaws.com`. Superblocks cannot directly create Secrets Manager secrets; only RDS can, on behalf of Superblocks.
* **IAM**: `CreateServiceLinkedRole` scoped to `rds.amazonaws.com`. This is a one-time action required to create the RDS service-linked role if it does not already exist in your account.

#### Connector role

One connector role is created per data plane. The role grants `rds-db:connect` scoped to the DB users Superblocks provisions for that data plane: one statement per data tag, matching the pattern `sbndb_<profile-token>_*_runtime` in your region. `<profile-token>` is the first 16 hexadecimal characters of the SHA-256 digest of the lowercased data tag (for example, `printf '%s' '<DATA_TAG>' | shasum -a 256 | cut -c1-16`). The connector role can only authenticate as DB users that Superblocks created for the declared data tags. It cannot connect as any other DB user, or to databases in your account that Superblocks did not create.

The trust policy limits who can assume the connector role to the specific lifecycle worker role ARN for that data plane. No other principal can assume it.

#### How a connection is authenticated

Neither the connector 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`, where `<profile-token>` comes from the data tag as described above and `<application-token>` is a 24-hex hash of the application ID) 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 ARN it was configured with, and refuses the connection if they disagree. A compromised control plane cannot redirect the data plane at an IAM role its operator never approved.
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. The credentials it gets back are narrower than the connector role itself.
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 the two policies: the connector role's own policy bounds it to `sbndb_<profile-token>_*_runtime` users for the declared data tags, 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.

## 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`, which is what the example values show.

Placeholders to substitute before use:

| 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 (the `sb-app-db` prefix comes from `s3_name_prefix`)                                                                                                                                                                                                                               |
| `<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>`                      | — (max 15 characters, lowercase alphanumeric, unique per AWS account)                 | [Agent name](/admin/superblocks-on-aws/database-reference#agent-name) for this data plane. Terraform `agents` map key; must also equal Fargate `agent_name` or Helm `superblocks.agentName`, and the `AgentName` tag on every resource the lifecycle worker creates. 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 (the `sb-app-db` prefix comes from `iam_name_prefix`)                                                                                                                                                                                                        |

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 last statement (`StateBucketKms`) handles bucket encryption. **Without a customer KMS key** (the default, SSE-S3), the resource is `"*"` and access is constrained to calls originating from S3, as shown below. **With a customer KMS key** (`kms_key_arn` set), replace `"Resource": "*"` with the specific key ARN and remove the `Condition` block entirely.

    ```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>/*"
        },
        {
          "Sid": "StateBucketKms",
          "Effect": "Allow",
          "Action": [
            "kms:Decrypt",
            "kms:DescribeKey",
            "kms:Encrypt",
            "kms:GenerateDataKey",
            "kms:ReEncryptFrom",
            "kms:ReEncryptTo"
          ],
          "Resource": "*",
          "Condition": {
            "ForAnyValue:StringEquals": {
              "aws:CalledVia": "s3.amazonaws.com"
            }
          }
        }
      ]
    }
    ```
  </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.

    ```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. All mutations are gated on the `AgentName`, `ManagedBy`, and `Vpc` resource 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 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. No other principal can assume it.

    ```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))` — compute it 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. The data plane assumes this role with a session policy allowing `rds-db:connect` on one specific cluster and DB user, so each connection is authorized for exactly the app it is serving. See [How a connection is authenticated](#how-a-connection-is-authenticated).

    ```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` — 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>
