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