Giving up on IAM. 15 minute tokens aint worth it.
This commit is contained in:
parent
8cf322e671
commit
777fe555a0
11 changed files with 74 additions and 237 deletions
|
|
@ -6,6 +6,16 @@ resource "random_pet" "admin_user" {
|
||||||
count = var.admin_username == null ? 1 : 0
|
count = var.admin_username == null ? 1 : 0
|
||||||
separator = "_"
|
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 {
|
locals {
|
||||||
admin_username = coalesce(var.admin_username, random_pet.admin_user[0].id)
|
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))
|
||||||
}
|
}
|
||||||
|
|
@ -13,8 +13,8 @@ resource "local_file" "debug" {
|
||||||
read = aws_rds_cluster_endpoint.endpoint["read"].endpoint
|
read = aws_rds_cluster_endpoint.endpoint["read"].endpoint
|
||||||
}
|
}
|
||||||
admin = {
|
admin = {
|
||||||
username = local.admin.username
|
username = local.admin_username
|
||||||
password = local.admin.password
|
password = local.admin_password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tenants = {
|
tenants = {
|
||||||
|
|
|
||||||
|
|
@ -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]
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -3,13 +3,17 @@ locals {
|
||||||
for key, tenant in module.tenants : key => {
|
for key, tenant in module.tenants : key => {
|
||||||
username = tenant.username
|
username = tenant.username
|
||||||
database = tenant.database
|
database = tenant.database
|
||||||
access_key = tenant.access_key
|
password = tenant.password
|
||||||
secret_key = tenant.secret_key
|
|
||||||
auth_token = tenant.auth_token
|
|
||||||
connection_string = tenant.connection_string
|
connection_string = tenant.connection_string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
output "tenants" {
|
output "tenants" {
|
||||||
value = local.output_tenants
|
value = local.output_tenants
|
||||||
|
}
|
||||||
|
output "admin" {
|
||||||
|
value = {
|
||||||
|
username = local.admin_username
|
||||||
|
password = local.admin_password
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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" {
|
resource "aws_rds_cluster" "cluster" {
|
||||||
cluster_identifier = local.sanitised_name
|
cluster_identifier = local.sanitised_name
|
||||||
engine_mode = "provisioned"
|
engine_mode = "provisioned"
|
||||||
|
|
@ -31,8 +22,7 @@ resource "aws_rds_cluster" "cluster" {
|
||||||
engine_version = data.aws_rds_engine_version.latest.version
|
engine_version = data.aws_rds_engine_version.latest.version
|
||||||
database_name = local.admin_username
|
database_name = local.admin_username
|
||||||
master_username = local.admin_username
|
master_username = local.admin_username
|
||||||
manage_master_user_password = true
|
master_password = local.admin_password
|
||||||
master_user_secret_kms_key_id = aws_kms_key.master_key.arn
|
|
||||||
storage_encrypted = true
|
storage_encrypted = true
|
||||||
enable_local_write_forwarding = true
|
enable_local_write_forwarding = true
|
||||||
backup_retention_period = var.backup_retention_period_days
|
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" {
|
resource "aws_rds_cluster_instance" "instance" {
|
||||||
cluster_identifier = aws_rds_cluster.cluster.id
|
cluster_identifier = aws_rds_cluster.cluster.id
|
||||||
identifier_prefix = "${local.sanitised_name}-"
|
identifier_prefix = "${local.sanitised_name}-"
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ locals {
|
||||||
host = data.aws_rds_cluster.cluster.endpoint
|
host = data.aws_rds_cluster.cluster.endpoint
|
||||||
port = local.is_mysql ? 3306 : 5432
|
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}"
|
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 = "${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}"
|
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 = {
|
database_environment_variables = {
|
||||||
PGPASSWORD = !local.is_mysql ? var.admin_password : null,
|
PGPASSWORD = !local.is_mysql ? var.admin_password : null,
|
||||||
MYSQL_PWD = 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
|
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" {
|
provisioner "local-exec" {
|
||||||
command = (local.is_mysql
|
command = (local.is_mysql
|
||||||
? "echo \"CREATE USER IF NOT EXISTS '${var.username}' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS' | ${local.mysql_command}"
|
? "echo \"CREATE USER IF NOT EXISTS '${local.username}' IDENTIFIED BY '${local.password}'\" | ${local.mysql_command}"
|
||||||
: "echo \"CREATE USER ${var.username}; GRANT rds_iam TO ${var.username}\" | ${local.postgres_command}"
|
: "echo \"CREATE USER ${local.username} WITH PASSWORD '${local.password}; \" | ${local.postgres_command}"
|
||||||
)
|
)
|
||||||
environment = local.database_environment_variables
|
environment = local.database_environment_variables
|
||||||
}
|
}
|
||||||
provisioner "local-exec" {
|
provisioner "local-exec" {
|
||||||
command = (local.is_mysql
|
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
|
environment = local.database_environment_variables
|
||||||
}
|
}
|
||||||
#provisioner "local-exec" {
|
#provisioner "local-exec" {
|
||||||
# when = destroy
|
# when = destroy
|
||||||
# command = (local.is_mysql
|
# command = (local.is_mysql
|
||||||
# ? "DROP USER '${var.username}'@'%';"
|
# ? "DROP USER '${local.username}'@'%';"
|
||||||
# : "DROP USER ${var.username};"
|
# : "DROP USER ${local.username};"
|
||||||
# )
|
# )
|
||||||
#}
|
#}
|
||||||
#provisioner "local-exec" {
|
#provisioner "local-exec" {
|
||||||
# when = destroy
|
# when = destroy
|
||||||
# command = (local.is_mysql
|
# command = (local.is_mysql
|
||||||
# ? "echo 'DROP DATABASE ${var.database}' | ${local.mysql_command}"
|
# ? "echo 'DROP DATABASE ${local.database}' | ${local.mysql_command}"
|
||||||
# : "echo 'DROP DATABASE ${var.database}' | ${local.postgres_command}"
|
# : "echo 'DROP DATABASE ${local.database}' | ${local.postgres_command}"
|
||||||
# )
|
# )
|
||||||
#}
|
#}
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,16 @@ variable "username" {
|
||||||
type = string
|
type = string
|
||||||
description = "The username for the tenant"
|
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" {
|
variable "database" {
|
||||||
type = string
|
type = string
|
||||||
description = "The database for the tenant"
|
description = "The database for the tenant"
|
||||||
|
|
@ -20,6 +30,7 @@ variable "database" {
|
||||||
locals {
|
locals {
|
||||||
username = lower(var.username)
|
username = lower(var.username)
|
||||||
database = lower(var.database)
|
database = lower(var.database)
|
||||||
|
password = try(random_password.password[0].result, var.password)
|
||||||
}
|
}
|
||||||
variable "app_name" {
|
variable "app_name" {
|
||||||
type = string
|
type = string
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,13 @@
|
||||||
output "username" {
|
output "username" {
|
||||||
value = local.username
|
value = local.username
|
||||||
}
|
}
|
||||||
|
output "password" {
|
||||||
|
value = local.password
|
||||||
|
}
|
||||||
output "database" {
|
output "database" {
|
||||||
value = local.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" {
|
output "connection_string" {
|
||||||
value = join(" ", [
|
value = join(" ", [
|
||||||
"mysql",
|
"mysql",
|
||||||
|
|
@ -20,6 +15,6 @@ output "connection_string" {
|
||||||
"-P", data.aws_rds_cluster.cluster.port,
|
"-P", data.aws_rds_cluster.cluster.port,
|
||||||
"-D", local.database,
|
"-D", local.database,
|
||||||
"-u", local.username,
|
"-u", local.username,
|
||||||
"-p'${data.external.rds_auth_token.result.password}'",
|
"-p'${local.password}'",
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
@ -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]
|
|
||||||
}
|
|
||||||
21
cloud/aws/rds_serverless/tenants.tf
Normal file
21
cloud/aws/rds_serverless/tenants.tf
Normal 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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue