> ## 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 security model

> What you are asked to grant, what it can and cannot do, and how to verify it, for the security team reviewing Superblocks Database before setup

This page is for the security reviewer who has to approve [Superblocks Database](/admin/superblocks-on-aws/database) before anyone on your team runs its setup. You are being asked to run a Terraform module that creates IAM roles in your account, and to let a component in your VPC create and modify Aurora infrastructure in response to work that Superblocks queues. This page covers what that grant is, what it can and cannot do, which controls your account enforces rather than Superblocks, and how to verify it. The [Reference](/admin/superblocks-on-aws/database-reference) page holds the evidence, including the full IAM policy JSON.

## The short version

* **Nothing connects into your network.** Your data plane opens every connection. The control plane never initiates a connection to the data plane and is not in any cluster's security group ingress.
* **The control plane holds none of the feature's credentials.** It cannot assume either IAM role, has no database credentials, and has no route to a cluster. What it stores is lifecycle state and connection metadata: which application has a database under which data tag, the cluster endpoint, and the database and role names. Application data and credentials never reach it.
* **IAM policies you review and own decide which resources can change.** The lifecycle worker role can create, modify, or delete only resources that carry the [tags Superblocks stamps at creation](/admin/superblocks-on-aws/database-reference#how-everything-is-tagged), inside the one VPC you declared. Its trust policy names only your data plane's identity, and you can revoke it at any time. IAM bounds the footprint, not every operation inside it; see [What bounds a task](#what-bounds-a-task).
* **No application password ever exists.** Applications connect with 15-minute RDS IAM tokens, each limited by a per-connection session policy to one PostgreSQL role on one cluster.
* **Clusters cannot be public or unencrypted.** IAM conditions require `publicly_accessible = false`, storage encryption, and an RDS-managed master password on every cluster the worker creates, so a modified module cannot opt out.

## What you are asked to grant

Setup runs one Terraform module, `app-db-prereqs`, once per region in each account. It creates the following. Nothing else is created until an organization admin clicks **Provision**, which creates the first Aurora cluster. From then on the worker acts without a human gate: it creates a database whenever a builder needs one, and another cluster whenever remaining pool capacity falls to the thresholds in your data plane configuration (`max_databases`, and `minAvailableCapacityPercent` on Helm). The number of clusters is bounded by those thresholds and your AWS quotas, not by IAM.

<div className="dense-table">
  | Created by the module                          | What it is for                                                                                                                      | What bounds it                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
  | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | Lifecycle worker IAM role, with seven policies | The identity your data plane uses to provision Aurora clusters, create per-application databases, and read and write OpenTofu state | Trust policy names only your data plane's ECS task or Kubernetes service account. Creates require the three scoping tags (`AgentName=<AGENT_NAME>`, `ManagedBy=superblocks-app-database-lifecycle`, `Vpc=<VPC_ID>`) in the request; modifies and deletes require the same three tags on the existing resource; everything is placed in the declared VPC. The worker also stamps the ownership tags `superblocks:owned=true` and `aws-apn-id`. See [How everything is tagged](/admin/superblocks-on-aws/database-reference#how-everything-is-tagged) and [Raw IAM policy JSON](/admin/superblocks-on-aws/database-reference#raw-iam-policy-json) |
  | Connector IAM role, with one policy            | The identity applications authenticate with at query time                                                                           | Trust policy names only the lifecycle worker role. Its one permission is `rds-db:connect`, limited to `sbndb_<profile-token>_*_runtime` users for the data tags you declared                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
  | Enhanced Monitoring IAM role                   | Lets RDS publish operating-system metrics                                                                                           | The worker may pass this role only, and only to `rds.amazonaws.com`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
  | S3 state bucket                                | Stores OpenTofu state for the resources the worker manages                                                                          | Private, versioned, encrypted. Each data plane can read and write only its own key prefix. See [S3 state bucket properties](/admin/superblocks-on-aws/database-reference#s3-state-bucket-properties)                                                                                                                                                                                                                                                                                                                                                                                                                                            |
</div>

What is not granted matters as much:

* **No cross-account trust.** No trust policy names a Superblocks account, and no Superblocks-operated system outside your account can assume either role. If Superblocks operates your data plane for you (Cloud-Prem with managed data planes), its operators act through the data plane's own identity inside your account, and every role assumption is in your CloudTrail.
* **No IAM administration.** The worker's only IAM permissions are `iam:PassRole` for the monitoring role to RDS and `iam:CreateServiceLinkedRole` for the RDS service-linked role. It cannot create roles, attach policies, or change its own permissions.
* **No tagging escape hatch.** The tagging permissions are conditioned too. `ec2:CreateTags` is allowed only in the request that creates a security group or rule, or on one that already carries the three scoping tags. `rds:AddTagsToResource` is allowed only on RDS resources named `sb-*`, and only with the scoping tags in the request. The worker cannot bring an arbitrary resource of yours into scope by tagging it, and it is explicitly denied removing the scoping tags from anything.
* **No reach outside the VPC.** Every mutating permission is conditioned on the VPC you declared.
* **No secrets of its own.** The worker can read the RDS-managed master secret for clusters it created (`rds!cluster-*`), which RDS creates and rotates. It cannot create, write, or export secrets, and it has no long-lived credentials: the role is assumed through ECS or IRSA, and the organization agent key it uses toward the control plane grants nothing in AWS.

## What runs where

In a [Hybrid](/enterprise/hybrid-architecture/overview) deployment the control plane is Superblocks Cloud, outside your account. In [Cloud-Prem](/enterprise/cloud-prem/aws) it runs inside your AWS account under its own identity, operated by Superblocks. The trust boundary is the same in both: the control plane is not a principal in either role's trust policy, is not in any cluster's security group ingress, and never opens a connection to the data plane.

<Tabs>
  <Tab title="Hybrid">
    ```mermaid theme={null}
    flowchart TB
      subgraph cp [Superblocks Cloud]
        Server["Control plane<br/>Records intent and lifecycle state<br/>Holds none of the feature's credentials"]
      end

      subgraph aws [Your AWS account]
        subgraph vpc [Data plane VPC, private subnets]
          DP["Data plane<br/>Lifecycle worker and query executor"]
          Pool[("Aurora PostgreSQL pool<br/>Private, encrypted, IAM authentication")]
        end
        API["AWS service endpoints<br/>RDS, EC2, and tagging APIs"]
        S3["S3 state bucket<br/>OpenTofu state"]
        SM["Secrets Manager<br/>Aurora master password"]
      end

      cp ~~~ aws
      API ~~~ S3 ~~~ SM
      DP -- "Outbound HTTPS only<br/>agent key" --> Server
      DP -- "TCP 5432, TLS, RDS IAM token<br/>as the connector role" --> Pool
      DP -- "tofu plan and apply<br/>as the lifecycle worker role" --> API
      DP -- "Read and write<br/>as the lifecycle worker role" --> S3
      DP -- "Read during provisioning<br/>as the lifecycle worker role" --> SM
    ```
  </Tab>

  <Tab title="Cloud-Prem">
    ```mermaid theme={null}
    flowchart TB
      subgraph aws [Your AWS account]
        subgraph vpc [Data plane VPC, private subnets]
          DP["Data plane<br/>Lifecycle worker and query executor"]
          Pool[("Aurora PostgreSQL pool<br/>Private, encrypted, IAM authentication")]
        end
        subgraph cp [Superblocks control plane, operated by Superblocks]
          Server["Control plane<br/>Own VPC and own IAM role<br/>Holds none of the feature's credentials"]
        end
        API["AWS service endpoints<br/>RDS, EC2, and tagging APIs"]
        S3["S3 state bucket<br/>OpenTofu state"]
        SM["Secrets Manager<br/>Aurora master password"]
      end

      Server ~~~ S3
      API ~~~ SM
      DP -- "HTTPS, opened by the data plane<br/>agent key" --> Server
      DP -- "TCP 5432, TLS, RDS IAM token<br/>as the connector role" --> Pool
      DP -- "tofu plan and apply<br/>as the lifecycle worker role" --> API
      DP -- "Read and write<br/>as the lifecycle worker role" --> S3
      DP -- "Read during provisioning<br/>as the lifecycle worker role" --> SM
    ```
  </Tab>
</Tabs>

Each arrow points from the component that opens the connection to the component that accepts it. The table below describes each connection: how it is authenticated, and what data it carries.

<div className="dense-table">
  | Connection                                                                                                               | Authentication                                                                                                                                                                   | What it carries                                                                                                                                                                                                                                                                                                           |
  | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | Data plane to control plane, outbound HTTPS from your VPC                                                                | Your organization's agent key, the same key every data plane already uses to register                                                                                            | Capability registration, wake-up hints, task claims, task payloads (the operation, the target database, desired settings, and the application's SQL migration files), and outcome reports. Route by route: [Superblocks Database requests](/enterprise/hybrid-architecture/manage/requests#superblocks-database-requests) |
  | Data plane to AWS service endpoints, from your VPC                                                                       | Lifecycle worker IAM role                                                                                                                                                        | `tofu plan` and `tofu apply` calls to the RDS, EC2 security group, and tagging APIs, bounded by [the worker's IAM policy](/admin/superblocks-on-aws/database-reference#raw-iam-policy-json)                                                                                                                               |
  | Data plane to S3 state bucket, from your VPC                                                                             | Lifecycle worker IAM role, limited to this data plane's key prefix                                                                                                               | OpenTofu state for the resources this data plane manages. Superblocks does not retain a copy                                                                                                                                                                                                                              |
  | Data plane to Secrets Manager, from your VPC, during create and retire tasks                                             | Lifecycle worker IAM role                                                                                                                                                        | Reads the RDS-managed master secret for a cluster the worker created                                                                                                                                                                                                                                                      |
  | Data plane to Aurora as the master user, TCP 5432 with TLS inside your VPC, during create and retire tasks only          | The Aurora master password, read from Secrets Manager and held in memory for the task                                                                                            | The allowlisted module's `CREATE DATABASE` and `CREATE ROLE`, and the corresponding drops when a database is retired. This is the only connection with cluster-wide privilege. It is opened by your data plane, never by Superblocks, and every read of the secret is a `GetSecretValue` event in your CloudTrail         |
  | Data plane to Aurora as an application, TCP 5432 with TLS and full certificate and hostname verification inside your VPC | The connector role, assumed with a session policy allowing `rds-db:connect` for exactly one PostgreSQL role on exactly one cluster, then a 15-minute RDS IAM token for that role | Application queries and, when a migration task runs, that application's SQL migration files. Both run as the application's own PostgreSQL role in the application's own database                                                                                                                                          |
  | Control plane to the data plane, a cluster, or any of the feature's AWS resources                                        | None                                                                                                                                                                             | Nothing. No such connection exists                                                                                                                                                                                                                                                                                        |
</div>

Ingress to a cluster is limited to the security groups and CIDR ranges you pass in `source_security_group_ids` and `allowed_cidr_blocks`, which is normally your data plane's security group and nothing else. See [Physical database options](/admin/superblocks-on-aws/database-reference#physical-database-options).

### How applications are kept apart

Each application gets its own PostgreSQL database, `sbndb_<profile-token>_<application-token>`, and its own PostgreSQL role, `sbndb_<profile-token>_<application-token>_runtime`, on a cluster shared with other applications under the same data tag. The tokens are hashes of the data tag and the application ID, so names are stable and cannot collide. See [Data tag names](/admin/superblocks-on-aws/database-reference#data-tag-names). Edit-time and deploy-time databases are separate databases under different data tags; publishing creates the deploy-time database with the schema only, and no rows are copied.

The lifecycle worker creates the role with `rds_iam` and no password, so PostgreSQL delegates authentication for it to IAM: the only way to log in as that role is an RDS IAM token signed by an identity that IAM allows to `rds-db:connect` as that user. Two policies decide that, and the effective permission is their intersection:

1. **The connector role's own policy** allows `rds-db:connect` only for users matching `sbndb_<profile-token>_*_runtime` for the data tags you declared. It cannot connect as the master user, as a role from another data tag, or as anything that is not an application runtime role.
2. **The per-connection session policy** the data plane attaches when it assumes the connector role narrows that to the single user for the single application being served, on the single cluster that application's database lives on.

Application code never sees a connection string, a token, or a role name; it sees a query interface for its own database. One application's session cannot authenticate as another application's role or as the master user, even though every application on the data plane shares the same connector role. The full sequence is under [Connecting](/admin/superblocks-on-aws/database-reference#connecting).

## How work is dispatched

Every change starts as a task the control plane queues and ends with your data plane deciding whether and how to carry it out. The control plane cannot skip the data plane, because it has nothing to call AWS or the database with. Registering the database capability rides on the same `/api/v1/agents/register` call every data plane already makes; the rest use the [database-lifecycle routes](/enterprise/hybrid-architecture/manage/requests#superblocks-database-requests).

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant SB as Superblocks control plane
    participant DP as Data plane in your AWS account
    participant AWS as AWS APIs and Aurora

    Note over DP,SB: The data plane opens every connection. Nothing connects in.
    DP->>SB: Register capability, engine, and capacity (agent key)
    DP->>SB: Subscribe for wake-ups (server-sent events)
    Note right of SB: The data plane also polls every 30 seconds;<br/>the subscription only shortens the wait
    SB->>SB: A builder, a publish, or an admin queues a task
    SB-->>DP: Wake-up hint: work is available for your organization
    Note right of SB: The hint carries no task payload and no secrets
    DP->>SB: Claim pending tasks for this data plane (agent key)
    SB-->>DP: Task: operation, target, desired settings, SQL migration files
    DP->>DP: tofu plan, then check the plan against allowlists
    DP->>AWS: tofu apply, CREATE DATABASE, or apply migrations
    Note right of DP: IAM permits only tagged sb-* resources in this VPC
    DP->>SB: Report the outcome: state and connection metadata
    Note right of SB: The control plane rejects any report<br/>that contains credential material
```

The data plane always polls the claim route every 30 seconds. The server-sent events subscription only lets it notice new work sooner; if that route errors or is unavailable, polling continues and the subscription is re-probed periodically. Both paths are outbound, both authenticate with the agent key, and neither carries a task: the task is always fetched through the claim request, whichever path woke the data plane up. A proxy that buffers the stream adds latency and nothing else.

### What bounds a task

Separate the controls that live in your account, which hold even if Superblocks is compromised, from the controls Superblocks enforces on its own side, which are defense in depth.

**Enforced in your account, independent of Superblocks:**

* **Direction.** The data plane pulls work over connections it opens. The control plane cannot reach the data plane, call AWS, or connect to a database.
* **IAM on the AWS path.** Creates require the scoping tags in the request; modifies and deletes require them on the existing resource; every mutation is confined to the declared VPC; public endpoints and unencrypted storage are denied by condition. A task that asked for a resource outside these bounds fails with `AccessDenied`.
* **Identity on the database path.** Application queries and schema migrations run over the connector role, which IAM limits to one application role on one cluster per connection, as described under [How applications are kept apart](#how-applications-are-kept-apart). Create and retire tasks are different: they run as the master user, over a session the worker opens with the RDS-managed secret. What bounds those is not IAM but the allowlisted module, which executes the DDL for the named database and its roles and nothing else.
* **Allowlists on the data plane.** Before applying, the worker materializes the OpenTofu configuration from module sources on an allowlist in its own configuration, runs `tofu plan`, and rejects any plan that touches a resource type outside its allowlist. These checks run in your VPC, on configuration you control.
* **Network.** Clusters accept connections only from the security groups and CIDR ranges you configured.

**Enforced by the control plane, as defense in depth:**

* Tasks are routed only to a data plane that registered for the exact data tag the task belongs to, and wildcard data tags are rejected at registration.
* Wake-up hints are empty signals, task payloads contain intent and the application's schema files but never credentials, and outcome reports that contain credential material are rejected.

**If the control plane were compromised.** An attacker who controls the control plane can queue tasks but cannot execute them, and cannot make the data plane do anything outside the bounds above. Within those bounds, this is the honest worst case:

* **AWS path.** The attacker can queue tasks that change Superblocks-managed clusters, which as of the current release includes destructive changes to a cluster the worker already manages, such as deleting or resizing it, and can queue cluster creations, so unexpected spend is part of the worst case. The allowlists prevent new resource types and foreign module sources, and IAM prevents any effect outside the tagged footprint, but neither independently blocks a destructive plan against an in-footprint cluster. The controls that apply there are the ones you set on the clusters: `deletion_protection`, which defaults to `true`, automated backups, and the final snapshot on deletion. Set them in your Terraform or Helm configuration, not in the console: the worker re-applies module-managed settings on later runs, so a console change to a module-managed attribute is reverted the next time a task touches that cluster. See [Physical database options](/admin/superblocks-on-aws/database-reference#physical-database-options).
* **Migrations.** Attacker-chosen SQL runs as one application's `_runtime` role in that application's database, one task at a time. It cannot authenticate as the master user or as another application, but the attacker can queue a migration for every application under the data plane's data tags, so the aggregate is corruption of application data across that data plane.
* **Create and retire.** These open a master-user session, but only the allowlisted module's `CREATE` and `DROP` of the named database and roles run on it, not attacker-supplied SQL. Queued retires for every application would drop every application database on that data plane, which is the same aggregate as above by a different route. Backups are the recovery path.
* **Outcome reports.** The attacker controls both ends in this scenario, so the control plane's credential-material check offers no protection here. It does not need to: outcome reports carry no credentials because none exist.

**If the agent key leaks.** The agent key is already the trust anchor for everything a data plane does, so this feature adds no new class of secret. With the key alone, an attacker could register as a data plane for your organization, claim queued tasks (seeing operations, connection metadata, and migration SQL), and post outcome reports. They could not call AWS, assume either IAM role, or connect to a cluster: none of those trust the key, and the clusters accept traffic only from the security groups you configured. Rotate the key as you would for any other data plane compromise.

## How to verify

### Before setup

* Read the [Raw IAM policy JSON](/admin/superblocks-on-aws/database-reference#raw-iam-policy-json) for both roles. The Terraform module attaches exactly these policies, and that section is written to be handed to an IAM approver as is.
* Read the module source. `app-db-prereqs` is published in the `superblocksteam/superblocks/aws` registry module, and its `terraform plan` output lists every resource it will create before you apply it.
* Read the [outbound requests](/enterprise/hybrid-architecture/manage/requests#superblocks-database-requests) the data plane makes, and confirm your egress path allows them. No inbound rule is required.
* Confirm the inputs you will pass: the VPC, the private subnets, the security groups allowed to reach port 5432, and the data tags. Each becomes a condition in the policies above.

### After setup

Everything the feature manages is discoverable from your account, without involving Superblocks. IAM is global, but the tagging and RDS commands below are regional; run them in every region where you deployed a data plane.

```bash theme={null}
# Confirm who can assume each role: expect only your ECS task or
# Kubernetes service account on the worker, and only the worker on the connector
aws iam get-role --role-name sb-app-db-<AGENT_NAME>-lifecycle-worker-<REGION> \
  --query Role.AssumeRolePolicyDocument
aws iam get-role --role-name superblocks-app-db-connector-<AGENT_NAME> \
  --query Role.AssumeRolePolicyDocument

# Every AWS resource the feature has created in this region;
# add Key=AgentName,Values=<AGENT_NAME> to limit it to one data plane
aws resourcegroupstaggingapi get-resources --region <REGION> \
  --tag-filters Key=ManagedBy,Values=superblocks-app-database-lifecycle

# Confirm no cluster instance is publicly reachable
aws rds describe-db-instances --region <REGION> \
  --query "DBInstances[?starts_with(DBInstanceIdentifier, 'sb-')].[DBInstanceIdentifier, PubliclyAccessible]"
```

In CloudTrail, every AWS call the data plane makes is a session of the lifecycle worker role, every application connection starts with an `AssumeRole` on the connector role, and every master-user session starts with a `GetSecretValue` on the cluster's `rds!cluster-*` secret. Individual PostgreSQL connections and statements are in the per-cluster logs exported to CloudWatch Logs; see [Observing](/admin/superblocks-on-aws/database-reference#observing).

## Next steps

<CardGroup cols={2}>
  <Card title="Reference" icon="book" href="/admin/superblocks-on-aws/database-reference">
    Resource names, IAM scoping, tagging, and the full policy JSON.
  </Card>

  <Card title="Setup" icon="wrench" href="/admin/superblocks-on-aws/database-setup">
    Run the Terraform module and provision the first cluster.
  </Card>
</CardGroup>
