The roles all look right but why can't I login?

This commit is contained in:
Greyscale 2024-12-20 22:58:39 +01:00
parent 7392ccc891
commit 8cf322e671
Signed by: grey
GPG key ID: DDB392AE64B32D89
19 changed files with 416 additions and 254 deletions

View file

@ -12,9 +12,9 @@ resource "local_file" "debug" {
write = aws_rds_cluster_endpoint.endpoint["write"].endpoint,
read = aws_rds_cluster_endpoint.endpoint["read"].endpoint
}
tunnel = {
remote = local.db_tunnel_remote
local = data.ssh_tunnel.db.local
admin = {
username = local.admin.username
password = local.admin.password
}
}
tenants = {

View file

@ -1,68 +0,0 @@
data "aws_iam_policy_document" "ec2_connect_document" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
data "aws_iam_policy_document" "aws_policy_document" {
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 = ["*"]
}
}
data "aws_iam_policy_document" "user_connect_policy_document" {
for_each = var.tenants
statement {
actions = ["rds-db:connect"]
effect = "Allow"
resources = ["arn:aws:rds-db:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:dbuser:${aws_rds_cluster_instance.instance.cluster_identifier}/${each.value.username}"]
}
}
data "aws_iam_policy_document" "read_only_policy" {
for_each = var.tenants
statement {
actions = ["rds-db:connect"]
effect = "Allow"
resources = ["arn:aws:rds-db:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:dbuser:${aws_rds_cluster_instance.instance.cluster_identifier}/${each.value.username}"]
}
}
data "aws_iam_policy_document" "read_write_policy" {
for_each = var.tenants
statement {
actions = ["rds-db:connect"]
effect = "Allow"
resources = ["arn:aws:rds-db:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:dbuser:${aws_rds_cluster_instance.instance.cluster_identifier}/${each.value.username}"]
}
}

View file

@ -1,32 +0,0 @@
resource "aws_iam_policy" "user_connect_policy" {
for_each = var.tenants
name = join("-", ["RDS", var.instance_name, each.value.username, "UserConnectPolicy"])
path = "/RDS/${var.instance_name}/${each.value.username}/"
description = "Allow DB Access to EC2"
policy = data.aws_iam_policy_document.user_connect_policy_document[each.key].json
}
resource "aws_iam_policy" "ec2_connect_policy" {
for_each = var.tenants
name = join("-", ["RDS", var.instance_name, each.value.username, "EC2ConnectPolicy"])
path = "/RDS/${var.instance_name}/${each.value.username}/"
description = "Allow DB Access to EC2"
policy = data.aws_iam_policy_document.aws_policy_document.json
}
# Read only access policy
resource "aws_iam_policy" "read_only_policy" {
for_each = var.tenants
name = join("-", ["RDS", title(var.instance_name), each.value.username, "ReadOnlyPolicy"])
path = "/RDS/${var.instance_name}/${each.value.username}/"
description = "Allow Read DB Access to EC2"
policy = data.aws_iam_policy_document.read_only_policy[each.key].json
}
# Read-Write access policy
resource "aws_iam_policy" "read_write_policy" {
for_each = var.tenants
name = join("-", ["RDS", title(var.instance_name), each.value.username, "ReadWritePolicy"])
policy = data.aws_iam_policy_document.read_write_policy[each.key].json
path = "/RDS/${var.instance_name}/${each.value.username}/"
description = "Allow Write DB access to EC2"
}

View file

@ -1,16 +0,0 @@
resource "aws_iam_role" "rds" {
assume_role_policy = data.aws_iam_policy_document.ec2_connect_document.json
name = join("-", ["RDS", title(var.instance_name), ])
path = "/${join("/", [var.application.name, "RDS", ])}/"
force_detach_policies = true
tags = merge(
try(var.application.application_tag, {}),
{}
)
}
resource "aws_iam_policy_attachment" "rds_to_tenants" {
for_each = var.tenants
name = join("-", ["RDS", title(var.instance_name), title(each.value.username), "SupervisorConnectPolicy"])
roles = [aws_iam_role.rds.name]
policy_arn = aws_iam_policy.user_connect_policy[each.key].arn
}

View file

@ -1,51 +0,0 @@
resource "aws_iam_role" "ec2_access" {
for_each = var.tenants
name = join("-", ["RDS", title(var.instance_name), title(each.value.username), "EC2Access"])
path = aws_iam_role.rds.path
force_detach_policies = true
assume_role_policy = data.aws_iam_policy_document.ec2_connect_document.json
tags = merge(
try(var.application.application_tag, {}),
{}
)
}
resource "aws_iam_role" "user_access" {
for_each = var.tenants
name = join("-", ["RDS", title(var.instance_name), title(each.value.username), "UserAccess"])
path = aws_iam_role.rds.path
force_detach_policies = true
assume_role_policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [{
"Effect" : "Allow",
"Principal" : {
"AWS" : "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
},
"Action" : ["sts:AssumeRole"]
}]
})
tags = merge(
try(var.application.application_tag, {}),
{}
)
}
resource "aws_iam_policy_attachment" "policy" {
for_each = var.tenants
name = join("-", ["RDS", title(var.instance_name), title(each.value.username), "Policy"])
policy_arn = aws_iam_policy.read_only_policy[each.key].arn
roles = [
aws_iam_role.ec2_access[each.key].name,
aws_iam_role.user_access[each.key].name
]
}
resource "aws_iam_group" "user_access" {
for_each = var.tenants
name = join("-", ["RDS", title(var.instance_name), title(each.value.username), "UserAccess"])
}
resource "aws_iam_group_policy" "user_access" {
for_each = var.tenants
name = join("-", ["RDS", title(var.instance_name), title(each.value.username), "ConnectPolicy"])
policy = data.aws_iam_policy_document.user_connect_policy_document[each.key].json
group = aws_iam_group.user_access[each.key].name
}

View file

@ -0,0 +1,61 @@
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

@ -0,0 +1,22 @@
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

@ -1,37 +0,0 @@
resource "aws_iam_user" "tenants" {
for_each = var.tenants
name = join("-", [var.instance_name, each.value.username])
tags = merge(
try(var.application.application_tag, {}),
{}
)
}
resource "aws_iam_access_key" "tenants" {
for_each = var.tenants
user = aws_iam_user.tenants[each.key].name
status = each.value.active ? "Active" : "Inactive"
}
resource "aws_iam_group_membership" "tenants" {
for_each = var.tenants
group = aws_iam_group.user_access[each.key].name
name = join("-", ["RDS", var.instance_name, "ReadWrite"])
users = [for tenant in aws_iam_user.tenants : tenant.name]
}
locals {
output_tenants = {
for tenant in var.tenants : tenant.username => {
username = tenant.username,
database = tenant.database,
access_key = aws_iam_access_key.tenants[tenant.username].id,
secret_key = aws_iam_access_key.tenants[tenant.username].secret
}
}
}
output "tenants" {
value = local.output_tenants
precondition {
condition = length(aws_iam_user.tenants) > 0
error_message = "No tenants found"
}
}

View file

@ -5,6 +5,8 @@ variable "instance_name" {
}
locals {
sanitised_name = lower(replace(var.instance_name, "[^a-zA-Z0-9]", "-"))
titled_name = replace(title(join(" ", split("-", local.sanitised_name))), " ", "")
app_name = try(var.application.name, local.titled_name)
}
variable "tenants" {
type = map(object({
@ -24,6 +26,11 @@ variable "application" {
})
default = null
}
variable "aws_profile" {
type = string
description = "AWS profile to use for generating RDS auth token"
default = null
}
variable "engine" {
type = string
@ -82,14 +89,3 @@ variable "enable_performance_insights" {
default = false
}
variable "mysql_binary" {
type = string
description = "The path to the mysql binary"
default = "mariadb"
}
variable "postgres_binary" {
type = string
description = "The path to the postgres binary"
default = "psql"
}

View file

@ -0,0 +1,15 @@
locals {
output_tenants = {
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
connection_string = tenant.connection_string
}
}
}
output "tenants" {
value = local.output_tenants
}

View file

@ -109,36 +109,6 @@ resource "aws_rds_cluster_endpoint" "endpoint" {
)
}
locals {
db_tunnel_remote = {
host = aws_rds_cluster_endpoint.endpoint["write"].endpoint
port = var.engine == "aurora-postgres" ? 5432 : (var.engine == "aurora-mysql" ? 3306 : null)
}
}
data "ssh_tunnel" "db" {
connection_name = "db-${var.engine}"
remote = local.db_tunnel_remote
}
resource "null_resource" "db" {
for_each = var.tenants
depends_on = [aws_rds_cluster_instance.instance]
provisioner "local-exec" {
command = "echo 'Connecting to \"${local.db_tunnel_remote.host}:${local.db_tunnel_remote.port}\" as \"${local.admin.username}\" via \"${data.ssh_tunnel.db.connection_name}\"'"
}
provisioner "local-exec" {
command = (var.engine == "aurora-mysql"
? "echo 'CREATE DATABASE ${each.value.database}' | ${var.mysql_binary} -h ${data.ssh_tunnel.db.local.host} -P ${data.ssh_tunnel.db.local.port} -u ${local.admin.username} ${local.admin.username}"
: "echo 'CREATE DATABASE ${each.value.database}' | ${var.postgres_binary} -h ${data.ssh_tunnel.db.local.host} -p ${data.ssh_tunnel.db.local.port} -U ${local.admin.username} -d ${local.admin.username}"
)
environment = {
PGPASSWORD = var.engine == "aurora-postgres" ? local.admin.password : null,
MYSQL_PWD = var.engine == "aurora-mysql" ? local.admin.password : null,
}
}
triggers = {
cluster_id = aws_rds_cluster.cluster.id
}
}
output "endpoints" {
value = {
for key, endpoint in aws_rds_cluster_endpoint.endpoint : key => endpoint.endpoint

View file

@ -18,7 +18,7 @@ resource "aws_security_group_rule" "sgr" {
security_group_id = aws_security_group.rds.id
type = "ingress"
protocol = "tcp"
from_port = local.db_tunnel_remote.port
to_port = local.db_tunnel_remote.port
from_port = var.engine == "aurora-postgres" ? 5432 : 3306
to_port = var.engine == "aurora-postgres" ? 5432 : 3306
source_security_group_id = var.source_security_group_id
}

View file

@ -0,0 +1,5 @@
data "aws_region" "current" {}
data "aws_caller_identity" "current" {}
data "aws_vpc" "current" {
id = var.vpc_id
}

View file

@ -0,0 +1,71 @@
locals {
is_mysql = var.engine == "aurora-mysql"
db_tunnel_remote = {
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}"
database_environment_variables = {
PGPASSWORD = !local.is_mysql ? var.admin_password : null,
MYSQL_PWD = local.is_mysql ? var.admin_password : null,
}
}
resource "local_file" "debug" {
filename = "${path.root}/.debug/aws/rds/serverless/${data.aws_rds_cluster.cluster.cluster_identifier}/${local.username}.json"
content = jsonencode({
db_tunnel_remote = local.db_tunnel_remote,
mysql_command = local.mysql_command,
postgres_command = local.postgres_command,
database_environment_variables = local.database_environment_variables,
})
file_permission = "0600"
}
data "ssh_tunnel" "db" {
connection_name = "db-${var.engine}"
remote = local.db_tunnel_remote
}
resource "terraform_data" "db" {
triggers_replace = {
engine = data.aws_rds_cluster.cluster.engine,
cluster_id = data.aws_rds_cluster.cluster.id
}
provisioner "local-exec" {
command = "echo 'Connecting to \"${local.db_tunnel_remote.host}:${local.db_tunnel_remote.port}\" as \"${var.admin_username}\" via \"${data.ssh_tunnel.db.connection_name}\"'"
}
provisioner "local-exec" {
command = (local.is_mysql
? "echo 'CREATE DATABASE IF NOT EXISTS ${var.database}' | ${local.mysql_command}"
: "echo 'CREATE DATABASE ${var.database}' | ${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}"
)
environment = local.database_environment_variables
}
provisioner "local-exec" {
command = (local.is_mysql
? "GRANT ALL PRIVILEGES ON ${var.database}.* TO '${var.username}'@'%'\""
: ""
)
environment = local.database_environment_variables
}
#provisioner "local-exec" {
# when = destroy
# command = (local.is_mysql
# ? "DROP USER '${var.username}'@'%';"
# : "DROP USER ${var.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}"
# )
#}
}

View file

@ -0,0 +1,72 @@
variable "vpc_id" {
type = string
description = "VPC ID"
}
variable "cluster_id" {
type = string
description = "The cluster identifier"
}
data "aws_rds_cluster" "cluster" {
cluster_identifier = var.cluster_id
}
variable "username" {
type = string
description = "The username for the tenant"
}
variable "database" {
type = string
description = "The database for the tenant"
}
locals {
username = lower(var.username)
database = lower(var.database)
}
variable "app_name" {
type = string
description = "The application name"
}
variable "tags" {
type = map(string)
description = "Tags to apply to resources"
default = {}
}
variable "aws_profile" {
type = string
description = "AWS profile to use for generating RDS auth token"
default = null
}
variable "is_active" {
type = bool
default = true
}
variable "super_user_iam_role_name" {
type = string
default = null
}
variable "engine" {
type = string
description = "The engine type of the RDS cluster"
validation {
error_message = "Engine must be one of 'aurora-postgres' or 'aurora-mysql'"
condition = var.engine == "aurora-postgres" || var.engine == "aurora-mysql"
}
}
variable "mysql_binary" {
type = string
description = "The path to the mysql binary"
default = "mariadb"
}
variable "postgres_binary" {
type = string
description = "The path to the postgres binary"
default = "psql"
}
variable "admin_username" {
type = string
description = "The admin user for the database"
}
variable "admin_password" {
type = string
description = "The admin password for the database"
}

View file

@ -0,0 +1,25 @@
output "username" {
value = local.username
}
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",
"-h", data.aws_rds_cluster.cluster.endpoint,
"-P", data.aws_rds_cluster.cluster.port,
"-D", local.database,
"-u", local.username,
"-p'${data.external.rds_auth_token.result.password}'",
])
}

View file

@ -0,0 +1,107 @@
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,26 @@
terraform {
required_version = "~> 1.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
local = {
source = "hashicorp/local"
version = "~> 2.0"
}
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
ssh = {
source = "matthewbaggett/ssh"
version = "~> 0.1"
}
}
}

View file

@ -14,10 +14,6 @@ terraform {
source = "hashicorp/local"
version = "~> 2.0"
}
postgresql = {
source = "cyrilgdn/postgresql"
version = "~> 1.0"
}
null = {
source = "hashicorp/null"
version = "~> 3.0"