From 777fe555a0d9b9217edc5f93bf477ff8b479941a Mon Sep 17 00:00:00 2001 From: Matthew Baggett <matthew@baggett.me> Date: Sat, 21 Dec 2024 00:22:06 +0100 Subject: [PATCH] Giving up on IAM. 15 minute tokens aint worth it. --- cloud/aws/rds_serverless/admin_user.tf | 10 ++ cloud/aws/rds_serverless/debug.tf | 4 +- cloud/aws/rds_serverless/iam.system.tf | 61 ------------ cloud/aws/rds_serverless/iam.tenants.tf | 22 ----- cloud/aws/rds_serverless/outputs.tf | 10 +- cloud/aws/rds_serverless/rds.tf | 23 +---- cloud/aws/rds_serverless/tenant/db.tf | 27 ++++-- cloud/aws/rds_serverless/tenant/input.tf | 11 +++ cloud/aws/rds_serverless/tenant/output.tf | 15 +-- cloud/aws/rds_serverless/tenant/tenant.tf | 107 ---------------------- cloud/aws/rds_serverless/tenants.tf | 21 +++++ 11 files changed, 74 insertions(+), 237 deletions(-) delete mode 100644 cloud/aws/rds_serverless/iam.system.tf delete mode 100644 cloud/aws/rds_serverless/iam.tenants.tf delete mode 100644 cloud/aws/rds_serverless/tenant/tenant.tf create mode 100644 cloud/aws/rds_serverless/tenants.tf diff --git a/cloud/aws/rds_serverless/admin_user.tf b/cloud/aws/rds_serverless/admin_user.tf index edba2f2..6cdabae 100644 --- a/cloud/aws/rds_serverless/admin_user.tf +++ b/cloud/aws/rds_serverless/admin_user.tf @@ -6,6 +6,16 @@ resource "random_pet" "admin_user" { count = var.admin_username == null ? 1 : 0 separator = "_" } +variable "admin_password" { + type = string + default = null +} +resource "random_password" "admin_pass" { + count = var.admin_username == null ? 1 : 0 + special = false + length = 32 +} locals { admin_username = coalesce(var.admin_username, random_pet.admin_user[0].id) + admin_password = nonsensitive(coalesce(var.admin_password, random_password.admin_pass[0].result)) } \ No newline at end of file diff --git a/cloud/aws/rds_serverless/debug.tf b/cloud/aws/rds_serverless/debug.tf index 62c3852..94a3c52 100644 --- a/cloud/aws/rds_serverless/debug.tf +++ b/cloud/aws/rds_serverless/debug.tf @@ -13,8 +13,8 @@ resource "local_file" "debug" { read = aws_rds_cluster_endpoint.endpoint["read"].endpoint } admin = { - username = local.admin.username - password = local.admin.password + username = local.admin_username + password = local.admin_password } } tenants = { diff --git a/cloud/aws/rds_serverless/iam.system.tf b/cloud/aws/rds_serverless/iam.system.tf deleted file mode 100644 index 53132b5..0000000 --- a/cloud/aws/rds_serverless/iam.system.tf +++ /dev/null @@ -1,61 +0,0 @@ -data "aws_iam_policy_document" "assume_role_policy" { - statement { - actions = ["sts:AssumeRole"] - principals { - type = "Service" - identifiers = ["ec2.amazonaws.com"] - } - } -} -data "aws_iam_policy_document" "rds_instance_policy" { - statement { - actions = ["iam:GetGroup"] - effect = "Allow" - resources = ["*"] - } - statement { - actions = ["iam:GetSSHPublicKey", "iam:ListSSHPublicKeys"] - effect = "Allow" - resources = ["arn:aws:iam::*:user/*"] - } - statement { - actions = ["ec2:*"] - effect = "Allow" - resources = ["*"] - } - statement { - actions = ["rds:*"] - effect = "Allow" - resources = ["*"] - } - statement { - actions = ["secretsmanager:*"] - effect = "Allow" - resources = ["*"] - } -} -resource "aws_iam_policy" "rds_instance_policy" { - policy = data.aws_iam_policy_document.rds_instance_policy.json - name = join("", [local.app_name, "RDS", "InstancePolicy"]) - path = "/${join("/", [local.app_name, "RDS", "InstancePolicy"])}/" - tags = merge( - try(var.application.application_tag, {}), - {} - ) -} -resource "aws_iam_role" "rds_instance_policy" { - assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json - name = join("", [local.app_name, "RDS", ]) - path = "/${join("/", [local.app_name, "RDS", ])}/" - force_detach_policies = true - tags = merge( - try(var.application.application_tag, {}), - {} - ) -} - -resource "aws_iam_policy_attachment" "rds_instance_policy" { - name = aws_iam_policy.rds_instance_policy.name - policy_arn = aws_iam_policy.rds_instance_policy.arn - roles = [aws_iam_role.rds_instance_policy.name] -} \ No newline at end of file diff --git a/cloud/aws/rds_serverless/iam.tenants.tf b/cloud/aws/rds_serverless/iam.tenants.tf deleted file mode 100644 index 1be88f9..0000000 --- a/cloud/aws/rds_serverless/iam.tenants.tf +++ /dev/null @@ -1,22 +0,0 @@ -module "tenants" { - depends_on = [aws_rds_cluster.cluster, aws_rds_cluster_instance.instance] - for_each = var.tenants - source = "./tenant" - username = each.value.username - database = each.value.database - app_name = local.app_name - vpc_id = data.aws_vpc.current.id - aws_profile = var.aws_profile - cluster_id = aws_rds_cluster.cluster.id - super_user_iam_role_name = aws_iam_role.rds_instance_policy.name - engine = aws_rds_cluster.cluster.engine - admin_username = local.admin.username - admin_password = local.admin.password - tags = merge( - try(var.application.application_tag, {}), - { - "TerraformRDSClusterName" = var.instance_name - "TerraformRDSTenantName" = each.value.username - } - ) -} \ No newline at end of file diff --git a/cloud/aws/rds_serverless/outputs.tf b/cloud/aws/rds_serverless/outputs.tf index 25c3c16..76c012e 100644 --- a/cloud/aws/rds_serverless/outputs.tf +++ b/cloud/aws/rds_serverless/outputs.tf @@ -3,13 +3,17 @@ locals { for key, tenant in module.tenants : key => { username = tenant.username database = tenant.database - access_key = tenant.access_key - secret_key = tenant.secret_key - auth_token = tenant.auth_token + password = tenant.password connection_string = tenant.connection_string } } } output "tenants" { value = local.output_tenants +} +output "admin" { + value = { + username = local.admin_username + password = local.admin_password + } } \ No newline at end of file diff --git a/cloud/aws/rds_serverless/rds.tf b/cloud/aws/rds_serverless/rds.tf index 25a3e85..81613f9 100644 --- a/cloud/aws/rds_serverless/rds.tf +++ b/cloud/aws/rds_serverless/rds.tf @@ -15,15 +15,6 @@ resource "aws_kms_key" "db_key" { } ) } -resource "aws_kms_key" "master_key" { - description = "RDS ${var.instance_name} Master Account Key" - tags = merge( - try(var.application.application_tag, {}), - { - TerraformSecretType = "RDSMasterAccountKey" - } - ) -} resource "aws_rds_cluster" "cluster" { cluster_identifier = local.sanitised_name engine_mode = "provisioned" @@ -31,8 +22,7 @@ resource "aws_rds_cluster" "cluster" { engine_version = data.aws_rds_engine_version.latest.version database_name = local.admin_username master_username = local.admin_username - manage_master_user_password = true - master_user_secret_kms_key_id = aws_kms_key.master_key.arn + master_password = local.admin_password storage_encrypted = true enable_local_write_forwarding = true backup_retention_period = var.backup_retention_period_days @@ -61,17 +51,6 @@ resource "aws_rds_cluster" "cluster" { ) } -data "aws_secretsmanager_secret" "admin" { - arn = join("", aws_rds_cluster.cluster.master_user_secret.*.secret_arn) -} -data "aws_secretsmanager_secret_version" "admin" { - secret_id = data.aws_secretsmanager_secret.admin.id - version_stage = "AWSCURRENT" -} -locals { - admin = nonsensitive(jsondecode(data.aws_secretsmanager_secret_version.admin.secret_string)) -} - resource "aws_rds_cluster_instance" "instance" { cluster_identifier = aws_rds_cluster.cluster.id identifier_prefix = "${local.sanitised_name}-" diff --git a/cloud/aws/rds_serverless/tenant/db.tf b/cloud/aws/rds_serverless/tenant/db.tf index 3068e46..edf7de9 100644 --- a/cloud/aws/rds_serverless/tenant/db.tf +++ b/cloud/aws/rds_serverless/tenant/db.tf @@ -4,8 +4,8 @@ locals { host = data.aws_rds_cluster.cluster.endpoint port = local.is_mysql ? 3306 : 5432 } - mysql_command = "${var.mysql_binary} -h ${data.ssh_tunnel.db.local.host} -P ${data.ssh_tunnel.db.local.port} -u ${var.admin_username}" - postgres_command = "${var.postgres_binary} -h ${data.ssh_tunnel.db.local.host} -p ${data.ssh_tunnel.db.local.port} -U ${var.admin_username} -d ${var.admin_username}" + mysql_command = try("${var.mysql_binary} -h ${data.ssh_tunnel.db.local.host} -P ${data.ssh_tunnel.db.local.port} -u ${var.admin_username}", "") + postgres_command = try("${var.postgres_binary} -h ${data.ssh_tunnel.db.local.host} -p ${data.ssh_tunnel.db.local.port} -U ${var.admin_username} -d ${var.admin_username}", "") database_environment_variables = { PGPASSWORD = !local.is_mysql ? var.admin_password : null, MYSQL_PWD = local.is_mysql ? var.admin_password : null, @@ -40,32 +40,39 @@ resource "terraform_data" "db" { ) environment = local.database_environment_variables } + #provisioner "local-exec" { + # command = (local.is_mysql + # ? "echo \"CREATE USER IF NOT EXISTS '${var.username}' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS' | ${local.mysql_command}" + # : "echo \"CREATE USER ${var.username}; GRANT rds_iam TO ${var.username}\" | ${local.postgres_command}" + # ) + # environment = local.database_environment_variables + #} provisioner "local-exec" { command = (local.is_mysql - ? "echo \"CREATE USER IF NOT EXISTS '${var.username}' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS' | ${local.mysql_command}" - : "echo \"CREATE USER ${var.username}; GRANT rds_iam TO ${var.username}\" | ${local.postgres_command}" + ? "echo \"CREATE USER IF NOT EXISTS '${local.username}' IDENTIFIED BY '${local.password}'\" | ${local.mysql_command}" + : "echo \"CREATE USER ${local.username} WITH PASSWORD '${local.password}; \" | ${local.postgres_command}" ) environment = local.database_environment_variables } provisioner "local-exec" { command = (local.is_mysql - ? "GRANT ALL PRIVILEGES ON ${var.database}.* TO '${var.username}'@'%'\"" - : "" + ? "echo \"GRANT ALL PRIVILEGES ON ${local.database}.* TO '${local.username}'@'%'\" | ${local.mysql_command}" + : "echo \"ALTER DATABASE ${local.database} OWNER TO ${local.username}\" | ${local.postgres_command}" ) environment = local.database_environment_variables } #provisioner "local-exec" { # when = destroy # command = (local.is_mysql - # ? "DROP USER '${var.username}'@'%';" - # : "DROP USER ${var.username};" + # ? "DROP USER '${local.username}'@'%';" + # : "DROP USER ${local.username};" # ) #} #provisioner "local-exec" { # when = destroy # command = (local.is_mysql - # ? "echo 'DROP DATABASE ${var.database}' | ${local.mysql_command}" - # : "echo 'DROP DATABASE ${var.database}' | ${local.postgres_command}" + # ? "echo 'DROP DATABASE ${local.database}' | ${local.mysql_command}" + # : "echo 'DROP DATABASE ${local.database}' | ${local.postgres_command}" # ) #} } \ No newline at end of file diff --git a/cloud/aws/rds_serverless/tenant/input.tf b/cloud/aws/rds_serverless/tenant/input.tf index 48064c4..21dd935 100644 --- a/cloud/aws/rds_serverless/tenant/input.tf +++ b/cloud/aws/rds_serverless/tenant/input.tf @@ -13,6 +13,16 @@ variable "username" { type = string description = "The username for the tenant" } +variable "password" { + type = string + description = "The password for the tenant" + default = null +} +resource "random_password" "password" { + count = var.password == null ? 1 : 0 + special = false + length = 32 +} variable "database" { type = string description = "The database for the tenant" @@ -20,6 +30,7 @@ variable "database" { locals { username = lower(var.username) database = lower(var.database) + password = try(random_password.password[0].result, var.password) } variable "app_name" { type = string diff --git a/cloud/aws/rds_serverless/tenant/output.tf b/cloud/aws/rds_serverless/tenant/output.tf index 57f8fe1..9df173e 100644 --- a/cloud/aws/rds_serverless/tenant/output.tf +++ b/cloud/aws/rds_serverless/tenant/output.tf @@ -1,18 +1,13 @@ output "username" { value = local.username } +output "password" { + value = local.password +} output "database" { value = local.database } -output "access_key" { - value = aws_iam_access_key.tenants.id -} -output "secret_key" { - value = aws_iam_access_key.tenants.secret -} -output "auth_token" { - value = data.external.rds_auth_token.result.password -} + output "connection_string" { value = join(" ", [ "mysql", @@ -20,6 +15,6 @@ output "connection_string" { "-P", data.aws_rds_cluster.cluster.port, "-D", local.database, "-u", local.username, - "-p'${data.external.rds_auth_token.result.password}'", + "-p'${local.password}'", ]) } \ No newline at end of file diff --git a/cloud/aws/rds_serverless/tenant/tenant.tf b/cloud/aws/rds_serverless/tenant/tenant.tf deleted file mode 100644 index 2ced46e..0000000 --- a/cloud/aws/rds_serverless/tenant/tenant.tf +++ /dev/null @@ -1,107 +0,0 @@ -data "aws_arn" "tenant" { - arn = join(":", [ - "arn", "aws", "rds-db", - data.aws_region.current.name, - data.aws_caller_identity.current.account_id, - "dbuser", - "${lower(data.aws_rds_cluster.cluster.cluster_resource_id)}/${local.username}" - ]) -} -data "aws_iam_policy_document" "assume_role_policy" { - statement { - actions = ["sts:AssumeRole"] - principals { - type = "Service" - identifiers = ["ec2.amazonaws.com"] - } - } -} -data "aws_iam_policy_document" "tenant_user_policy_document" { - statement { - actions = ["rds-db:*"] - effect = "Allow" - resources = [data.aws_arn.tenant.arn] - } -} - -# Create IAM and key for each tenant -resource "aws_iam_user" "tenant" { - name = join("", ["RDS", var.app_name, title(local.username)]) - tags = var.tags - path = "/${join("/", [var.app_name, "RDS", title(local.username), ])}/" -} - -data "external" "rds_auth_token" { - program = [ - "bash", "-c", replace( - <<-EOF - aws - --profile ${var.aws_profile} - rds - generate-db-auth-token - --hostname ${data.aws_rds_cluster.cluster.endpoint} - --port ${data.aws_rds_cluster.cluster.port} - --region ${replace(data.aws_region.current.name, "/[[:lower:]]$/", "")} - --username ${local.username} - | jq --raw-input '{ password: . }' - EOF - , "\n", " ") - ] -} - -# Here's that key we were just talking about -resource "aws_iam_access_key" "tenants" { - user = aws_iam_user.tenant.name - status = var.is_active ? "Active" : "Inactive" -} - -# Make a role for the IAM user to assume -resource "aws_iam_role" "rds_instance_policy_per_user" { - assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json - name = join("", [var.app_name, "RDS", "Tenant", title(local.username), ]) - path = "/${join("/", [var.app_name, "RDS", "Tenant", title(local.username), ])}/" - tags = var.tags -} - -# Create a policy per-tenant -resource "aws_iam_policy" "tenant_policy" { - name = join("", [var.app_name, "RDS", title(local.username), "TenantUserPolicy"]) - path = "/RDS/${var.app_name}/${local.username}/" - description = "Allow user ${local.username} Data Access to RDS database ${local.database}" - policy = data.aws_iam_policy_document.tenant_user_policy_document.json - tags = var.tags -} - -# find the superuser role if supplied -data "aws_iam_role" "superuser" { - count = var.super_user_iam_role_name != null ? 1 : 0 - name = var.super_user_iam_role_name -} -# Attach the tenant policy role to the tenants role -resource "aws_iam_policy_attachment" "tenant_policy_attachment" { - name = join("", [var.app_name, "RDS", title(local.username), "TenantUserPolicyAttachment"]) - policy_arn = aws_iam_policy.tenant_policy.arn - roles = compact([ - aws_iam_role.rds_instance_policy_per_user.name, - try(data.aws_iam_role.superuser[0].name, null) - ]) -} - -# Create the tenants own group -resource "aws_iam_group" "tenant" { - name = join("", [var.app_name, "RDS", title(local.username), "TenantGroup"]) - path = "/${join("/", [var.app_name, "RDS", title(local.username), ])}/" -} - -# Attach the tenant role to the tenant group -resource "aws_iam_group_policy_attachment" "tenant_policy_attachment" { - group = aws_iam_group.tenant.name - policy_arn = aws_iam_policy.tenant_policy.arn -} - -# Attach the tenant user to the tenant group -resource "aws_iam_group_membership" "tenant" { - name = join("", [var.app_name, "RDS", title(local.username), "TenantGroupMembership"]) - group = aws_iam_group.tenant.name - users = [aws_iam_user.tenant.name] -} \ No newline at end of file diff --git a/cloud/aws/rds_serverless/tenants.tf b/cloud/aws/rds_serverless/tenants.tf new file mode 100644 index 0000000..b2a632b --- /dev/null +++ b/cloud/aws/rds_serverless/tenants.tf @@ -0,0 +1,21 @@ +module "tenants" { + depends_on = [aws_rds_cluster.cluster, aws_rds_cluster_instance.instance] + for_each = var.tenants + source = "./tenant" + username = each.value.username + database = each.value.database + app_name = local.app_name + vpc_id = data.aws_vpc.current.id + aws_profile = var.aws_profile + cluster_id = aws_rds_cluster.cluster.id + engine = aws_rds_cluster.cluster.engine + admin_username = local.admin_username + admin_password = local.admin_password + tags = merge( + try(var.application.application_tag, {}), + { + "TerraformRDSClusterName" = var.instance_name + "TerraformRDSTenantName" = each.value.username + } + ) +} \ No newline at end of file