Incomplete auth flow

This commit is contained in:
Greyscale 2024-12-18 02:58:37 +01:00
parent 9ce551d953
commit 83875080c8
Signed by: grey
GPG key ID: DDB392AE64B32D89
12 changed files with 348 additions and 32 deletions

View file

@ -6,7 +6,7 @@ resource "random_pet" "admin_user" {
count = var.admin_username == null ? 1 : 0
separator = "_"
}
locals {
admin_username = coalesce(var.admin_username, random_pet.admin_user[0].id)
admin_token = ""
}

View file

@ -0,0 +1,2 @@
data "aws_region" "current" {}
data "aws_caller_identity" "current" {}

View file

@ -1,12 +1,18 @@
resource "local_file" "debug" {
content = jsonencode({
instance_name = var.instance_name,
tennants = var.tennants,
application_arn = try(var.application.arn, null),
application_name = try(var.application.name, null),
engine_user = var.engine,
engine_actual = data.aws_rds_engine_version.latest.engine
engine_version_actual = data.aws_rds_engine_version.latest.version,
rds = {
instance_name = var.instance_name,
tennants = var.tenants,
application_arn = try(var.application.arn, null),
application_name = try(var.application.name, null),
engine_user = var.engine,
engine_actual = data.aws_rds_engine_version.latest.engine
engine_version_actual = data.aws_rds_engine_version.latest.version,
}
tenants = {
input = var.tenants
output = local.output_tenants
}
})
filename = "${path.root}/.debug/aws/rds/serverless/${var.instance_name}.json"
file_permission = "0600"

View file

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

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

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

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

@ -3,11 +3,14 @@ variable "instance_name" {
description = "The name of the RDS serverless instance"
default = "serverless-multitennant"
}
variable "tennants" {
locals {
sanitised_name = lower(replace(var.instance_name, "[^a-zA-Z0-9]", "-"))
}
variable "tenants" {
type = map(object({
username = string
password = string
database = string
active = optional(bool, true)
}))
default = null
}
@ -74,3 +77,17 @@ variable "skip_final_snapshot" {
default = false
}
variable "enable_performance_insights" {
type = bool
default = false
}
variable "bastion" {
description = "The ssh bastion to use for creating the database"
type = object({
host = string
user = optional(string)
password = optional(string)
private_key = optional(string)
})
}

View file

@ -6,39 +6,118 @@ data "aws_rds_engine_version" "latest" {
values = ["provisioned"]
}
}
resource "aws_kms_key" "db_key" {
description = "RDS ${var.instance_name} Encryption Key"
tags = merge(
try(var.application.application_tag, {}),
{
TerraformSecretType = "RDSMasterEncryptionKey"
}
)
}
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_prefix = "${var.instance_name}-"
engine_mode = "provisioned"
engine = data.aws_rds_engine_version.latest.engine
engine_version = data.aws_rds_engine_version.latest.version
database_name = local.admin_username
master_username = local.admin_username
manage_master_user_password = true
storage_encrypted = true
enable_local_write_forwarding = true
backup_retention_period = var.backup_retention_period_days
skip_final_snapshot = var.skip_final_snapshot
preferred_backup_window = var.backup_window
cluster_identifier = local.sanitised_name
engine_mode = "provisioned"
engine = data.aws_rds_engine_version.latest.engine
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
storage_encrypted = true
enable_local_write_forwarding = true
backup_retention_period = var.backup_retention_period_days
skip_final_snapshot = var.skip_final_snapshot
preferred_backup_window = var.backup_window
iam_database_authentication_enabled = true
kms_key_id = aws_kms_key.db_key.arn
apply_immediately = true
db_subnet_group_name = aws_db_subnet_group.sg.name
serverlessv2_scaling_configuration {
max_capacity = var.scaling.max_capacity
min_capacity = var.scaling.min_capacity
}
lifecycle {
create_before_destroy = false
}
tags = merge(
try(var.application.application_tag, {}),
{
Name = var.instance_name
}
)
}
resource "aws_rds_cluster_instance" "instance" {
cluster_identifier = aws_rds_cluster.cluster.id
identifier_prefix = "${local.sanitised_name}-"
instance_class = "db.serverless"
engine = aws_rds_cluster.cluster.engine
engine_version = aws_rds_cluster.cluster.engine_version
apply_immediately = true
publicly_accessible = false
db_subnet_group_name = aws_rds_cluster.cluster.db_subnet_group_name
performance_insights_enabled = var.enable_performance_insights
performance_insights_retention_period = var.enable_performance_insights ? 7 : null
performance_insights_kms_key_id = var.enable_performance_insights ? aws_kms_key.db_key.arn : null
lifecycle {
create_before_destroy = false
}
tags = merge(
try(var.application.application_tag, {}),
{}
)
}
resource "aws_rds_cluster_instance" "instance" {
cluster_identifier = aws_rds_cluster.cluster.id
instance_class = "db.serverless"
engine = aws_rds_cluster.cluster.engine
engine_version = aws_rds_cluster.cluster.engine_version
apply_immediately = true
publicly_accessible = false
resource "aws_rds_cluster_endpoint" "endpoint" {
depends_on = [aws_rds_cluster_instance.instance]
for_each = { "write" = "ANY", "read" = "READER" }
cluster_endpoint_identifier = join("-", [local.sanitised_name, each.key, "endpoint"])
cluster_identifier = aws_rds_cluster.cluster.id
custom_endpoint_type = each.value
tags = merge(
try(var.application.application_tag, {}),
{}
)
}
resource "null_resource" "database_create" {
for_each = var.tenants
depends_on = [aws_rds_cluster_instance.instance]
triggers = {
cluster_id = aws_rds_cluster.cluster.id
}
connection {
type = "ssh"
host = var.bastion.host
user = var.bastion.user
password = var.bastion.password
private_key = var.bastion.private_key
}
provisioner "remote-exec" {
inline = [
"PGPASSWORD=${local.admin_token} psql -h ${aws_rds_cluster_endpoint.endpoint["write"].endpoint} -U ${local.admin_username} -d ${local.admin_username} -c \"CREATE DATABASE ${each.value.database}\""
]
}
}
output "endpoints" {
value = {
for key, endpoint in aws_rds_cluster_endpoint.endpoint : key => endpoint.endpoint
}
}

View file

@ -11,7 +11,7 @@ data "aws_subnets" "subnets" {
}
}
resource "aws_db_subnet_group" "sg" {
name = "${var.instance_name}-subnet-group"
name = lower(join("-", [var.instance_name, "subnet-group"]))
subnet_ids = data.aws_subnets.subnets.ids
tags = merge(
try(var.application.application_tag, {}),

View file

@ -8,11 +8,19 @@ terraform {
}
random = {
source = "hashicorp/random"
version = "3.6.2"
version = "~> 3.0"
}
local = {
source = "hashicorp/local"
version = "~>2.1"
version = "~> 2.0"
}
postgresql = {
source = "cyrilgdn/postgresql"
version = "~> 1.0"
}
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
}
}