Giving up on IAM. 15 minute tokens aint worth it.

This commit is contained in:
Greyscale 2024-12-21 00:22:06 +01:00
parent 8cf322e671
commit 777fe555a0
Signed by: grey
GPG key ID: DDB392AE64B32D89
11 changed files with 74 additions and 237 deletions

View file

@ -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))
}

View file

@ -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 = {

View file

@ -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]
}

View file

@ -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
}
)
}

View file

@ -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
}
}

View file

@ -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}-"

View file

@ -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}"
# )
#}
}

View file

@ -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

View file

@ -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}'",
])
}

View file

@ -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]
}

View file

@ -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
}
)
}