diff --git a/.tflint.hcl b/.tflint.hcl
index fcc2fa8..a4029d7 100644
--- a/.tflint.hcl
+++ b/.tflint.hcl
@@ -1,15 +1,15 @@
config {
- module = true
- force = false
+ module = true
+ force = false
disabled_by_default = false
-# ignore_module = {
-# "terraform-aws-modules/vpc/aws" = true
-# "terraform-aws-modules/security-group/aws" = true
-# }
+ # ignore_module = {
+ # "terraform-aws-modules/vpc/aws" = true
+ # "terraform-aws-modules/security-group/aws" = true
+ # }
-# varfile = ["example1.tfvars", "example2.tfvars"]
-# variables = ["foo=bar", "bar=[\"baz\"]"]
+ # varfile = ["example1.tfvars", "example2.tfvars"]
+ # variables = ["foo=bar", "bar=[\"baz\"]"]
}
rule "aws_instance_invalid_type" {
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b40949e..aab1d5d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -100,4 +100,8 @@
* 1.8.1 -- 2026-05-28
- updated policies/sc-servicecatalog-t1,-t2
- - deny update to provisioned products and properties of provisioned products
\ No newline at end of file
+ - deny update to provisioned products and properties of provisioned products
+
+* 1.9.0 -- 2026-06-04
+ - created policies
+ - policies/sc-dbuser
diff --git a/examples/rds-mfa-test/README.md b/examples/rds-mfa-test/README.md
new file mode 100644
index 0000000..c7e4a07
--- /dev/null
+++ b/examples/rds-mfa-test/README.md
@@ -0,0 +1,271 @@
+# IAM DB Authentication for Aurora PostgreSQL / Amazon RDS PostgreSQL
+
+This guide shows how to enable IAM database authentication and connect using:
+- `psql`
+- `pgAdmin`
+- `DBeaver`
+
+It includes CLI commands and sample CLI output you can compare against your environment.
+
+## References
+
+- AWS Docs (RDS IAM DB Authentication): https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html
+- AWS Docs (Connecting with IAM token): https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Connecting.html
+- AWS Docs (Aurora IAM DB Authentication): https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.html
+- AWS Blog: Using IAM authentication to connect with pgAdmin Amazon Aurora PostgreSQL or Amazon RDS for PostgreSQL: https://aws.amazon.com/blogs/database/using-iam-authentication-to-connect-with-pgadmin-amazon-aurora-postgresql-or-amazon-rds-for-postgresql/
+
+## Prerequisites
+
+1. PostgreSQL engine on Amazon RDS or Aurora PostgreSQL.
+2. TLS/SSL enabled from client to database.
+3. AWS CLI v2 installed and authenticated.
+4. Identity Center (SSO) login completed with MFA.
+5. PostgreSQL user exists for your login name (or mapped username).
+
+Optional but recommended:
+- Download the AWS RDS CA bundle for strict certificate validation:
+ https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
+
+## 1. Enable IAM authentication on the database
+
+Choose one path depending on your deployment type.
+
+### RDS PostgreSQL instance
+
+```bash
+aws rds modify-db-instance \
+ --db-instance-identifier my-postgres-instance \
+ --enable-iam-database-authentication \
+ --apply-immediately
+```
+
+Example output:
+
+```json
+{
+ "DBInstance": {
+ "DBInstanceIdentifier": "my-postgres-instance",
+ "Engine": "postgres",
+ "IAMDatabaseAuthenticationEnabled": true,
+ "DBInstanceStatus": "modifying"
+ }
+}
+```
+
+### Aurora PostgreSQL cluster
+
+```bash
+aws rds modify-db-cluster \
+ --db-cluster-identifier my-aurora-pg-cluster \
+ --enable-iam-database-authentication \
+ --apply-immediately
+```
+
+Example output:
+
+```json
+{
+ "DBCluster": {
+ "DBClusterIdentifier": "my-aurora-pg-cluster",
+ "Engine": "aurora-postgresql",
+ "IAMDatabaseAuthenticationEnabled": true,
+ "Status": "modifying"
+ }
+}
+```
+
+## 2. Create or update IAM policy for connect permission
+
+Grant `rds-db:connect` to the exact DB user ARN.
+
+### Policy example for RDS PostgreSQL
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": ["rds-db:connect"],
+ "Resource": ["arn:aws:rds-db:us-east-1:123456789012:dbuser:db-ABCDEFGHIJKL01234/mydbuser"]
+ }
+ ]
+}
+```
+
+### Policy example for Aurora PostgreSQL
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": ["rds-db:connect"],
+ "Resource": ["arn:aws:rds-db:us-east-1:123456789012:dbuser:cluster-ABCDEFGHIJKL01234/mydbuser"]
+ }
+ ]
+}
+```
+
+Attach the policy to the IAM role/user you assume for database access.
+
+## 3. Grant database user membership to `rds_iam`
+
+Connect as an admin user and run:
+
+```sql
+CREATE USER mydbuser WITH LOGIN;
+GRANT rds_iam TO mydbuser;
+```
+
+Validation query:
+
+```sql
+\du mydbuser
+```
+
+Example output:
+
+```text
+Role name | Attributes | Member of
+----------+------------+-----------------
+mydbuser | | {rds_iam}
+```
+
+## 4. Authenticate to AWS with MFA (Identity Center / SSO)
+
+If using AWS CLI SSO profile:
+
+```bash
+aws sso login --profile prod-db
+```
+
+Example output:
+
+```text
+Attempting to automatically open the SSO authorization page in your default browser.
+Successfully logged into Start URL: https://start.us-gov-east-1.us-gov-home.awsapps.com/directory/d-c2672d0b4e#/
+```
+
+## 5. Generate an IAM auth token
+
+Set variables:
+
+```bash
+export AWS_PROFILE=prod-db
+export AWS_REGION=us-east-1
+export DBHOST=my-postgres-instance.abcdefghijkl.us-east-1.rds.amazonaws.com
+export DBPORT=5432
+export DBUSER=mydbuser
+```
+
+Generate token:
+
+```bash
+export PGPASSWORD="$(aws rds generate-db-auth-token \
+ --hostname "$DBHOST" \
+ --port "$DBPORT" \
+ --region "$AWS_REGION" \
+ --username "$DBUSER")"
+```
+
+Example output:
+
+```text
+No stdout output. Token stored in PGPASSWORD.
+```
+
+Token notes:
+- Token lifetime is about 15 minutes.
+- Generate a new token before each new connection if needed.
+
+## 6. Connect with psql
+
+```bash
+psql "host=$DBHOST port=$DBPORT dbname=postgres user=$DBUSER sslmode=require"
+```
+
+Example output:
+
+```text
+psql (16.4, server 15.6)
+SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384)
+Type "help" for help.
+
+postgres=> select current_user, inet_client_addr();
+ current_user | inet_client_addr
+--------------+-----------------
+ mydbuser | 10.100.42.17
+(1 row)
+```
+
+If you use CA verification, replace `sslmode=require` with:
+
+```bash
+sslmode=verify-full sslrootcert=/path/to/global-bundle.pem
+```
+
+## 7. Connect with pgAdmin
+
+1. Open pgAdmin and create a new server registration.
+2. In Connection tab:
+ - Host name/address: your RDS or Aurora endpoint
+ - Port: `5432`
+ - Maintenance database: `postgres` (or target DB)
+ - Username: `mydbuser`
+ - Password: paste the generated IAM token
+3. In SSL tab:
+ - SSL mode: `require` (or `verify-full` with CA cert)
+ - Root certificate: path to `global-bundle.pem` if using `verify-full`
+4. Save and connect before token expiration.
+
+Expected behavior:
+- Connection succeeds.
+- If token expires, pgAdmin prompts again; generate a new token and reconnect.
+
+## 8. Connect with DBeaver
+
+1. Create a new PostgreSQL connection.
+2. Main settings:
+ - Host: your RDS or Aurora endpoint
+ - Port: `5432`
+ - Database: `postgres` (or target DB)
+ - Username: `mydbuser`
+ - Password: paste generated IAM token
+3. SSL settings:
+ - Enable SSL
+ - Mode: `require` or `verify-full`
+ - CA certificate: `global-bundle.pem` when using verification
+4. Click Test Connection, then Finish.
+
+Expected behavior:
+- Test connection succeeds while token is valid.
+- Regenerate token when prompted after expiration.
+
+## Troubleshooting
+
+### `FATAL: PAM authentication failed`
+
+Check:
+- IAM auth is enabled on instance/cluster.
+- IAM principal has correct `rds-db:connect` ARN.
+- Database user exists and has `rds_iam` membership.
+- Username in token command exactly matches DB user.
+
+### `The security token included in the request is invalid`
+
+Check:
+- AWS profile/session is active.
+- `aws sso login --profile ...` was completed.
+- Region and endpoint are correct.
+
+### SSL or certificate errors
+
+Check:
+- Client uses SSL.
+- Root CA bundle path is valid when using `verify-ca` or `verify-full`.
+
+## Optional screenshots from this example
+
+Screenshots in [images/](images/) can be used to supplement the SSO and token workflow in your runbook.
diff --git a/examples/rds-mfa-test/images/image_1.png b/examples/rds-mfa-test/images/image_1.png
new file mode 100644
index 0000000..30ea7ce
Binary files /dev/null and b/examples/rds-mfa-test/images/image_1.png differ
diff --git a/examples/rds-mfa-test/images/image_2.png b/examples/rds-mfa-test/images/image_2.png
new file mode 100644
index 0000000..0e43b5a
Binary files /dev/null and b/examples/rds-mfa-test/images/image_2.png differ
diff --git a/examples/rds-mfa-test/images/image_3.png b/examples/rds-mfa-test/images/image_3.png
new file mode 100644
index 0000000..fe0770e
Binary files /dev/null and b/examples/rds-mfa-test/images/image_3.png differ
diff --git a/examples/rds-mfa-test/images/image_4.png b/examples/rds-mfa-test/images/image_4.png
new file mode 100644
index 0000000..b218433
Binary files /dev/null and b/examples/rds-mfa-test/images/image_4.png differ
diff --git a/examples/rds-mfa-test/images/image_5.png b/examples/rds-mfa-test/images/image_5.png
new file mode 100644
index 0000000..9d36228
Binary files /dev/null and b/examples/rds-mfa-test/images/image_5.png differ
diff --git a/examples/rds-mfa-test/images/image_6.png b/examples/rds-mfa-test/images/image_6.png
new file mode 100644
index 0000000..4c6e1ce
Binary files /dev/null and b/examples/rds-mfa-test/images/image_6.png differ
diff --git a/policies/sc-dbuser/README.md b/policies/sc-dbuser/README.md
new file mode 100644
index 0000000..33cb3c2
--- /dev/null
+++ b/policies/sc-dbuser/README.md
@@ -0,0 +1,44 @@
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 1.12 |
+| [aws](#requirement\_aws) | >= 6.0 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [aws](#provider\_aws) | >= 6.0 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [aws_arn.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/arn) | data source |
+| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
+| [aws_iam_policy_document.inline](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
+| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [account\_alias](#input\_account\_alias) | AWS Account Alias | `string` | `""` | no |
+| [account\_id](#input\_account\_id) | AWS Account ID (default will pull from current user) | `string` | `""` | no |
+| [override\_prefixes](#input\_override\_prefixes) | Override built-in prefixes by component. This should be used primarily for common infrastructure things | `map(string)` | `{}` | no |
+| [tags](#input\_tags) | AWS Tags to apply to appropriate resources | `map(string)` | `{}` | no |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [customer\_managed\_policy\_names](#output\_customer\_managed\_policy\_names) | Map of policy name to permission boundary of Customer Managed Policy to attach to the permissionset |
+| [inline\_policy](#output\_inline\_policy) | AWS Policy document for the single allowed inline policy (use .json to get policy) |
+| [managed\_policy\_names](#output\_managed\_policy\_names) | Names of AWS Managed Policy to attach to the permissionset |
+| [name](#output\_name) | Permission Set Name for which all settings apply |
+| [relay\_state](#output\_relay\_state) | Relay State to pass along to permissionset |
diff --git a/policies/sc-dbuser/base_arn.tf b/policies/sc-dbuser/base_arn.tf
new file mode 100644
index 0000000..5eba500
--- /dev/null
+++ b/policies/sc-dbuser/base_arn.tf
@@ -0,0 +1,3 @@
+locals {
+ all_account_arn_iam = format("arn:%v:%v::%v:%%v", data.aws_arn.current.partition, "iam", "*")
+}
diff --git a/policies/sc-dbuser/data.tf b/policies/sc-dbuser/data.tf
new file mode 120000
index 0000000..37fff16
--- /dev/null
+++ b/policies/sc-dbuser/data.tf
@@ -0,0 +1 @@
+../../common/data.tf
\ No newline at end of file
diff --git a/policies/sc-dbuser/defaults.tf b/policies/sc-dbuser/defaults.tf
new file mode 120000
index 0000000..1227df3
--- /dev/null
+++ b/policies/sc-dbuser/defaults.tf
@@ -0,0 +1 @@
+../../common/defaults.tf
\ No newline at end of file
diff --git a/policies/sc-dbuser/locals.tf b/policies/sc-dbuser/locals.tf
new file mode 100644
index 0000000..6aa29cd
--- /dev/null
+++ b/policies/sc-dbuser/locals.tf
@@ -0,0 +1,12 @@
+locals {
+ account_id = var.account_id != "" ? var.account_id : data.aws_caller_identity.current.account_id
+ account_environment = data.aws_arn.current.partition == "aws-us-gov" ? "gov" : "ew"
+ region = data.aws_region.current.region
+ region_short = join("", [for c in split("-", local.region) : substr(c, 0, 1)])
+
+ base_tags = {
+ "boc:tf_module_version" = local._module_version
+ "boc:tf_module_name" = local._module_name
+ "boc:created_by" = "terraform"
+ }
+}
diff --git a/policies/sc-dbuser/main.tf b/policies/sc-dbuser/main.tf
new file mode 100644
index 0000000..3c91f4a
--- /dev/null
+++ b/policies/sc-dbuser/main.tf
@@ -0,0 +1,2 @@
+/*
+*/
diff --git a/policies/sc-dbuser/module_name.tf b/policies/sc-dbuser/module_name.tf
new file mode 100644
index 0000000..cd0ea8b
--- /dev/null
+++ b/policies/sc-dbuser/module_name.tf
@@ -0,0 +1,3 @@
+locals {
+ _module_name = "aws-sso/policies/sc-dbuser"
+}
diff --git a/policies/sc-dbuser/outputs.tf b/policies/sc-dbuser/outputs.tf
new file mode 100644
index 0000000..776869b
--- /dev/null
+++ b/policies/sc-dbuser/outputs.tf
@@ -0,0 +1,24 @@
+output "name" {
+ description = "Permission Set Name for which all settings apply"
+ value = local.name
+}
+
+output "managed_policy_names" {
+ description = "Names of AWS Managed Policy to attach to the permissionset"
+ value = local.managed_policy_names
+}
+
+output "customer_managed_policy_names" {
+ description = "Map of policy name to permission boundary of Customer Managed Policy to attach to the permissionset"
+ value = local.customer_managed_policy_names
+}
+
+output "inline_policy" {
+ description = "AWS Policy document for the single allowed inline policy (use .json to get policy)"
+ value = local.inline_policy
+}
+
+output "relay_state" {
+ description = "Relay State to pass along to permissionset"
+ value = local.relay_state
+}
diff --git a/policies/sc-dbuser/policy.tf b/policies/sc-dbuser/policy.tf
new file mode 100644
index 0000000..b605dc7
--- /dev/null
+++ b/policies/sc-dbuser/policy.tf
@@ -0,0 +1,48 @@
+data "aws_iam_policy_document" "inline" {
+ # Read/Describe clusters/instances in RDS
+ statement {
+ sid = "AllowRDSDBRead"
+ effect = "Allow"
+ resources = ["*"]
+ actions = [
+ "rds:DescribeDBInstances",
+ "rds:DescribeDBClusters",
+ ]
+ }
+ # Read/Describe performance insights metrics for clusters/instances in RDS
+ statement {
+ sid = "AllowPerformanceInsightsSelf"
+ effect = "Allow"
+ actions = [
+ "pi:DescribeDimensionKeys",
+ "pi:GetResourceMetrics",
+ "pi:ListAvailableResourceDimensions",
+ "pi:ListAvailableResourceMetrics"
+ ]
+ # PI resource ARN format: arn:aws:pi:region:account:metrics/rds/db-resource-id
+ # Can't embed dbuser here — PI scopes to the DB resource, not user — leave or separate
+ resources = [format("arn:%v:%v:*:*:%v", data.aws_arn.current.partition, "pi", "metrics/rds/*")]
+ }
+ # Deny rds-db:connect if the session tag UserName is not present (i.e. not authenticated via SSO or STS)
+ statement {
+ sid = "DenyRDSConnectIfNoTag"
+ effect = "Deny"
+ actions = ["rds-db:connect"]
+ resources = ["*"]
+ condition {
+ test = "Null"
+ variable = "aws:PrincipalTag/UserName"
+ values = ["true"]
+ }
+ }
+ # Allow rds-db:connect if the session tag UserName matches the DB username in the resource ARN
+ statement {
+ sid = "AllowRDSConnectSelf"
+ effect = "Allow"
+ actions = ["rds-db:connect"]
+ resources = [
+ # Scopes to only the DB username matching their session tag
+ format("arn:%v:%v:*:*:%v:*/$${aws:PrincipalTag/UserName}", data.aws_arn.current.partition, "rds-db", "dbuser")
+ ]
+ }
+}
diff --git a/policies/sc-dbuser/prefixes.tf b/policies/sc-dbuser/prefixes.tf
new file mode 120000
index 0000000..5bc256c
--- /dev/null
+++ b/policies/sc-dbuser/prefixes.tf
@@ -0,0 +1 @@
+../../common/prefixes.tf
\ No newline at end of file
diff --git a/policies/sc-dbuser/settings.tf b/policies/sc-dbuser/settings.tf
new file mode 100644
index 0000000..7e8bede
--- /dev/null
+++ b/policies/sc-dbuser/settings.tf
@@ -0,0 +1,10 @@
+locals {
+ name = "sc-dbuser"
+ description = "System Common DB User"
+ managed_policy_names = [
+ "ReadOnlyAccess",
+ ]
+ customer_managed_policy_names = {}
+ relay_state = null
+ inline_policy = data.aws_iam_policy_document.inline
+}
diff --git a/policies/sc-dbuser/variables.common.tf b/policies/sc-dbuser/variables.common.tf
new file mode 120000
index 0000000..e01226c
--- /dev/null
+++ b/policies/sc-dbuser/variables.common.tf
@@ -0,0 +1 @@
+../../common/variables.common.tf
\ No newline at end of file
diff --git a/policies/sc-dbuser/variables.tf.unused b/policies/sc-dbuser/variables.tf.unused
new file mode 100644
index 0000000..53d6bf1
--- /dev/null
+++ b/policies/sc-dbuser/variables.tf.unused
@@ -0,0 +1,29 @@
+variable "name" {
+ description = "Permission Set Name for which all settings apply"
+ type = string
+ default = null
+}
+
+variable "managed_policy_names" {
+ description = "Names of AWS Managed Policy to attach to the permissionset"
+ type = list(string)
+ default = []
+}
+
+variable "customer_managed_policy_names" {
+ description = "Map of policy name to permission boundary of Customer Managed Policy to attach to the permissionset"
+ type = map(string)
+ default = {}
+}
+
+# variable "inline_policy" {
+# description = "AWS Policy document for the single allowed inline policy"
+# type = string
+# default = null
+# }
+
+variable "relay_state" {
+ description = "Relay State to pass along to permissionset"
+ type = string
+ default = null
+}
diff --git a/policies/sc-dbuser/version.tf b/policies/sc-dbuser/version.tf
new file mode 120000
index 0000000..4950c91
--- /dev/null
+++ b/policies/sc-dbuser/version.tf
@@ -0,0 +1 @@
+../../common/version.tf
\ No newline at end of file
diff --git a/policies/sc-dbuser/versions.tf b/policies/sc-dbuser/versions.tf
new file mode 120000
index 0000000..cbeda73
--- /dev/null
+++ b/policies/sc-dbuser/versions.tf
@@ -0,0 +1 @@
+../../common/versions.tf
\ No newline at end of file