Skip to content
16 changes: 16 additions & 0 deletions databases/rds-postgres-db/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,22 @@ This service requires minimal AWS permissions compared to `rds-postgres-server`.

No RDS, EC2, or S3 permissions are needed.

The `requirements/` Terraform module creates a dedicated IAM role
(`nullplatform-<cluster_name>-rds-postgres-db-role`) holding this policy,
with a trust policy allowing the nullplatform agent role to `sts:AssumeRole`
on it. Pass `cluster_name` (required) and optionally `agent_role_arn`
(defaults to `nullplatform-<cluster_name>-agent-role`) when applying it.
Granting the agent itself permission to assume this role is handled
separately, outside this module.

This role and its policy are shared per **cluster**, not per linked
`rds-postgres-server` instance — the `GetSecretValue` grant is scoped to the
`nullplatform/rds/*` secret-name prefix (every master secret in the cluster
following that naming convention), not to the single secret this particular
service instance's link actually uses. Anything that assumes this role can
read the master password of any `rds-postgres-server` in the cluster, not
just the linked one.

### Runtime Dependencies

These tools are required inside the agent container:
Expand Down
25 changes: 25 additions & 0 deletions databases/rds-postgres-db/requirements/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions databases/rds-postgres-db/requirements/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data "aws_caller_identity" "current" {}
12 changes: 12 additions & 0 deletions databases/rds-postgres-db/requirements/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
locals {
iam_module_name = "requirements-rds-postgres-db"
iam_create = var.iam_create_role

role_name = var.role_name != "" ? var.role_name : "nullplatform-${var.cluster_name}-rds-postgres-db-role"
agent_role_arn = var.agent_role_arn != "" ? var.agent_role_arn : "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/nullplatform-${var.cluster_name}-agent-role"

iam_default_tags = merge(var.iam_resource_tags_json, {
ManagedBy = "rds-postgres-db"
Module = local.iam_module_name
})
}
47 changes: 47 additions & 0 deletions databases/rds-postgres-db/requirements/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
################################################################################
# Permissions role — assumed by the nullplatform agent role (sts:AssumeRole)
################################################################################

resource "aws_iam_role" "nullplatform_rds_postgres_db" {
count = local.iam_create ? 1 : 0

name = local.role_name
description = "Permissions role assumed by the nullplatform agent role for rds-postgres-db in cluster ${var.cluster_name}"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { AWS = concat([local.agent_role_arn], var.additional_agent_role_arns) }
Action = "sts:AssumeRole"
}]
})

tags = local.iam_default_tags
}

################################################################################
# Secrets Manager IAM policy — read-only access to the RDS master password
################################################################################

resource "aws_iam_policy" "nullplatform_rds_postgres_db_secretsmanager_policy" {
count = local.iam_create ? 1 : 0

name = "nullplatform-${var.cluster_name}-rds-postgres-db-secretsmanager-policy"
description = "Policy for reading the RDS master password from Secrets Manager"

policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "secretsmanager:GetSecretValue"
Resource = "arn:aws:secretsmanager:*:${data.aws_caller_identity.current.account_id}:secret:nullplatform/rds/*"
}]
})
}

resource "aws_iam_role_policy_attachment" "rds_postgres_db_secretsmanager" {
count = local.iam_create ? 1 : 0
role = aws_iam_role.nullplatform_rds_postgres_db[0].name
policy_arn = aws_iam_policy.nullplatform_rds_postgres_db_secretsmanager_policy[0].arn
}
19 changes: 19 additions & 0 deletions databases/rds-postgres-db/requirements/output.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
output "permissions_role_arn" {
description = "ARN of the rds-postgres-db permissions role assumed by the nullplatform agent role. Pass to the agent (assume_role_arns)."
value = local.iam_create ? aws_iam_role.nullplatform_rds_postgres_db[0].arn : ""
}

output "permissions_role_name" {
description = "Name of the rds-postgres-db permissions role"
value = local.iam_create ? aws_iam_role.nullplatform_rds_postgres_db[0].name : ""
}

output "permissions_role_id" {
description = "ID of the rds-postgres-db permissions role"
value = local.iam_create ? aws_iam_role.nullplatform_rds_postgres_db[0].id : ""
}

output "secretsmanager_policy_arn" {
description = "ARN of the Secrets Manager read policy"
value = local.iam_create ? aws_iam_policy.nullplatform_rds_postgres_db_secretsmanager_policy[0].arn : ""
}
44 changes: 44 additions & 0 deletions databases/rds-postgres-db/requirements/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
variable "cluster_name" {
description = "Name of the cluster this bootstrap run is for. Used to derive the permissions role name, the policy name, and the default agent role ARN."
type = string
}

variable "agent_role_arn" {
description = "ARN of the primary nullplatform agent IAM role allowed to assume this permissions role via sts:AssumeRole, and always a trusted principal of the role's trust policy. Defaults (when empty) to the conventional agent role for the cluster: arn:aws:iam::<account>:role/nullplatform-<cluster_name>-agent-role."
type = string
default = ""

validation {
condition = var.agent_role_arn == "" || can(regex("^arn:aws:iam::[0-9]{12}:role/.+", var.agent_role_arn))
error_message = "agent_role_arn must be empty (to use the derived default) or match arn:aws:iam::<account-id>:role/<role-name>"
}
}

variable "additional_agent_role_arns" {
description = "Extra IAM role ARNs allowed to assume this permissions role, appended to agent_role_arn in the trust policy. Defaults to none."
type = list(string)
default = []

validation {
condition = alltrue([for arn in var.additional_agent_role_arns : can(regex("^arn:aws:iam::[0-9]{12}:role/.+", arn))])
error_message = "each additional_agent_role_arns entry must match arn:aws:iam::<account-id>:role/<role-name>"
}
}

variable "role_name" {
description = "Override for the permissions IAM role name. Defaults to nullplatform-{cluster_name}-rds-postgres-db-role."
type = string
default = ""
}

variable "iam_create_role" {
description = "Whether to create the permissions role and its policy. When false, the module produces no resources."
type = bool
default = true
}

variable "iam_resource_tags_json" {
description = "Tags to apply to IAM resources created by this module."
type = map(string)
default = {}
}
8 changes: 8 additions & 0 deletions databases/rds-postgres-db/requirements/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
8 changes: 7 additions & 1 deletion databases/rds-postgres-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,13 @@ The agent executing this service needs the following IAM permissions (see `requi
- **S3**: Full lifecycle on the `np-service-<SERVICE_ID>` bucket
- **IAM**: `CreateServiceLinkedRole` (for RDS)

The `requirements/` Terraform module can be used to create and attach the necessary IAM policies to an existing role.
The `requirements/` Terraform module creates a dedicated IAM role
(`nullplatform-<cluster_name>-rds-postgres-server-role`) holding these
policies, with a trust policy allowing the nullplatform agent role to
`sts:AssumeRole` on it. Pass `cluster_name` (required) and optionally
`agent_role_arn` (defaults to `nullplatform-<cluster_name>-agent-role`) when
applying it. Granting the agent itself permission to assume this role is
handled separately, outside this module.

### Runtime Dependencies

Expand Down
25 changes: 25 additions & 0 deletions databases/rds-postgres-server/requirements/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions databases/rds-postgres-server/requirements/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data "aws_caller_identity" "current" {}
12 changes: 12 additions & 0 deletions databases/rds-postgres-server/requirements/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
locals {
iam_module_name = "requirements-rds-postgres-server"
iam_create = var.iam_create_role

role_name = var.role_name != "" ? var.role_name : "nullplatform-${var.cluster_name}-rds-postgres-server-role"
agent_role_arn = var.agent_role_arn != "" ? var.agent_role_arn : "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/nullplatform-${var.cluster_name}-agent-role"

iam_default_tags = merge(var.iam_resource_tags_json, {
ManagedBy = "rds-postgres-server"
Module = local.iam_module_name
})
}
64 changes: 47 additions & 17 deletions databases/rds-postgres-server/requirements/main.tf
Original file line number Diff line number Diff line change
@@ -1,29 +1,51 @@
################################################################################
# Policy attachments (only when role_name is provided)
# Permissions role — assumed by the nullplatform agent role (sts:AssumeRole)
################################################################################

resource "aws_iam_role" "nullplatform_rds_postgres_server" {
count = local.iam_create ? 1 : 0

name = local.role_name
description = "Permissions role assumed by the nullplatform agent role for rds-postgres-server in cluster ${var.cluster_name}"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { AWS = concat([local.agent_role_arn], var.additional_agent_role_arns) }
Action = "sts:AssumeRole"
}]
})

tags = local.iam_default_tags
}

################################################################################
# Policy attachments
################################################################################

resource "aws_iam_role_policy_attachment" "rds" {
count = var.role_name != null ? 1 : 0
role = var.role_name
policy_arn = aws_iam_policy.nullplatform_rds_policy.arn
count = local.iam_create ? 1 : 0
role = aws_iam_role.nullplatform_rds_postgres_server[0].name
policy_arn = aws_iam_policy.nullplatform_rds_policy[0].arn
}

resource "aws_iam_role_policy_attachment" "rds_sg" {
count = var.role_name != null ? 1 : 0
role = var.role_name
policy_arn = aws_iam_policy.nullplatform_rds_sg_policy.arn
count = local.iam_create ? 1 : 0
role = aws_iam_role.nullplatform_rds_postgres_server[0].name
policy_arn = aws_iam_policy.nullplatform_rds_sg_policy[0].arn
}

resource "aws_iam_role_policy_attachment" "rds_secretsmanager" {
count = var.role_name != null ? 1 : 0
role = var.role_name
policy_arn = aws_iam_policy.nullplatform_rds_secretsmanager_policy.arn
count = local.iam_create ? 1 : 0
role = aws_iam_role.nullplatform_rds_postgres_server[0].name
policy_arn = aws_iam_policy.nullplatform_rds_secretsmanager_policy[0].arn
}

resource "aws_iam_role_policy_attachment" "rds_s3" {
count = var.role_name != null ? 1 : 0
role = var.role_name
policy_arn = aws_iam_policy.nullplatform_rds_s3_policy.arn
count = local.iam_create ? 1 : 0
role = aws_iam_role.nullplatform_rds_postgres_server[0].name
policy_arn = aws_iam_policy.nullplatform_rds_s3_policy[0].arn
}

################################################################################
Expand All @@ -32,7 +54,9 @@ resource "aws_iam_role_policy_attachment" "rds_s3" {

# Grant permissions to manage RDS instances and subnet groups
resource "aws_iam_policy" "nullplatform_rds_policy" {
name = "nullplatform_${var.name}_rds_policy"
count = local.iam_create ? 1 : 0

name = "nullplatform-${var.cluster_name}-rds-policy"
description = "Policy for managing RDS instances and subnet groups"

policy = jsonencode({
Expand Down Expand Up @@ -71,7 +95,9 @@ resource "aws_iam_policy" "nullplatform_rds_policy" {

# Grant permissions to manage EC2 security groups for RDS
resource "aws_iam_policy" "nullplatform_rds_sg_policy" {
name = "nullplatform_${var.name}_rds_sg_policy"
count = local.iam_create ? 1 : 0

name = "nullplatform-${var.cluster_name}-rds-sg-policy"
description = "Policy for managing EC2 security groups for RDS"

policy = jsonencode({
Expand Down Expand Up @@ -106,7 +132,9 @@ resource "aws_iam_policy" "nullplatform_rds_sg_policy" {

# Grant permissions to manage the per-link S3 bucket used to store tofu state
resource "aws_iam_policy" "nullplatform_rds_s3_policy" {
name = "nullplatform_${var.name}_rds_s3_policy"
count = local.iam_create ? 1 : 0

name = "nullplatform-${var.cluster_name}-rds-s3-policy"
description = "Policy for managing per-service S3 tfstate buckets (np-service-*)"

policy = jsonencode({
Expand Down Expand Up @@ -141,7 +169,9 @@ resource "aws_iam_policy" "nullplatform_rds_s3_policy" {

# Grant permissions to manage Secrets Manager secrets for RDS master password
resource "aws_iam_policy" "nullplatform_rds_secretsmanager_policy" {
name = "nullplatform_${var.name}_rds_secretsmanager_policy"
count = local.iam_create ? 1 : 0

name = "nullplatform-${var.cluster_name}-rds-secretsmanager-policy"
description = "Policy for managing Secrets Manager secrets for RDS master password"

policy = jsonencode({
Expand Down
21 changes: 18 additions & 3 deletions databases/rds-postgres-server/requirements/output.tf
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
output "rds_policy_arn" {
description = "ARN of the RDS management policy"
value = aws_iam_policy.nullplatform_rds_policy.arn
value = local.iam_create ? aws_iam_policy.nullplatform_rds_policy[0].arn : ""
}

output "rds_sg_policy_arn" {
description = "ARN of the EC2 security group policy"
value = aws_iam_policy.nullplatform_rds_sg_policy.arn
value = local.iam_create ? aws_iam_policy.nullplatform_rds_sg_policy[0].arn : ""
}

output "rds_secretsmanager_policy_arn" {
description = "ARN of the Secrets Manager policy"
value = aws_iam_policy.nullplatform_rds_secretsmanager_policy.arn
value = local.iam_create ? aws_iam_policy.nullplatform_rds_secretsmanager_policy[0].arn : ""
}

output "permissions_role_arn" {
description = "ARN of the rds-postgres-server permissions role assumed by the nullplatform agent role. Pass to the agent (assume_role_arns)."
value = local.iam_create ? aws_iam_role.nullplatform_rds_postgres_server[0].arn : ""
}

output "permissions_role_name" {
description = "Name of the rds-postgres-server permissions role"
value = local.iam_create ? aws_iam_role.nullplatform_rds_postgres_server[0].name : ""
}

output "permissions_role_id" {
description = "ID of the rds-postgres-server permissions role"
value = local.iam_create ? aws_iam_role.nullplatform_rds_postgres_server[0].id : ""
}
Loading
Loading