Skip to content

Commit

Permalink
feat: DBTP-1299 - Cross account database copy (#294)
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnStainsby authored Dec 3, 2024
1 parent b0614f4 commit ac84ca8
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 114 deletions.
5 changes: 3 additions & 2 deletions postgres/database-copy.tf
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
module "database-dump" {
count = length(local.data_dump_tasks)
count = length(local.data_dump_tasks) > 0 ? 1 : 0
source = "./database-dump"

application = var.application
environment = var.environment
database_name = var.name
tasks = local.data_dump_tasks
}


Expand All @@ -16,4 +17,4 @@ module "database-load" {
environment = var.environment
database_name = var.name
task = local.data_load_tasks[count.index]
}
}
3 changes: 1 addition & 2 deletions postgres/database-dump/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,4 @@ locals {
dump_bucket_name = local.task_name

ecr_repository_arn = "arn:aws:ecr-public::763451185160:repository/database-copy"

}
}
36 changes: 28 additions & 8 deletions postgres/database-dump/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ resource "aws_iam_role_policy" "allow_task_creation" {
policy = data.aws_iam_policy_document.allow_task_creation.json
}


data "aws_iam_policy_document" "data_dump" {
policy_id = "data_dump"
statement {
Expand Down Expand Up @@ -163,7 +162,6 @@ resource "aws_ecs_task_definition" "service" {
}
}


resource "aws_s3_bucket" "data_dump_bucket" {
# checkov:skip=CKV_AWS_144: Cross Region Replication not Required
# checkov:skip=CKV2_AWS_62: Requires wider discussion around log/event ingestion before implementing. To be picked up on conclusion of DBTP-974
Expand All @@ -182,22 +180,40 @@ data "aws_iam_policy_document" "data_dump_bucket_policy" {
type = "*"
identifiers = ["*"]
}

actions = [
"s3:*",
]

effect = "Deny"

condition {
test = "Bool"
variable = "aws:SecureTransport"

values = [
"false",
]
}
resources = [
aws_s3_bucket.data_dump_bucket.arn,
"${aws_s3_bucket.data_dump_bucket.arn}/*",
]
}

statement {
effect = "Allow"
principals {
type = "AWS"
identifiers = [
for el in var.tasks :
"arn:aws:iam::${coalesce(el.to_account, data.aws_caller_identity.current.account_id)}:role/${var.application}-${el.to}-${var.database_name}-load-task"
]
}
actions = [
"s3:ListBucket",
"s3:GetObject",
"s3:GetObjectTagging",
"s3:GetObjectVersion",
"s3:GetObjectVersionTagging",
"s3:DeleteObject"
]
resources = [
aws_s3_bucket.data_dump_bucket.arn,
"${aws_s3_bucket.data_dump_bucket.arn}/*",
Expand All @@ -216,13 +232,17 @@ resource "aws_kms_key" "data_dump_kms_key" {
tags = local.tags

policy = jsonencode({
Id = "key-default-1"
Statement = [
{
"Sid" : "Enable IAM User Permissions",
"Effect" : "Allow",
"Principal" : {
"AWS" : "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
"AWS" : flatten([
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:root",
[for el in var.tasks :
"arn:aws:iam::${coalesce(el.to_account, data.aws_caller_identity.current.account_id)}:role/${var.application}-${el.to}-${var.database_name}-load-task"
]
])
},
"Action" : "kms:*",
"Resource" : "*"
Expand Down
88 changes: 81 additions & 7 deletions postgres/database-dump/tests/unit.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ variables {
application = "test-app"
environment = "test-env"
database_name = "test-db"
tasks = [
{
from : "prod"
to : "dev"
}
]
}

mock_provider "aws" {}
Expand Down Expand Up @@ -68,8 +74,12 @@ run "data_dump_unit_test" {
for el in data.aws_iam_policy_document.assume_ecs_task_role.statement[0].principals :
true if el.type == "Service" && [
for identifier in el.identifiers : true if identifier == "ecs-tasks.amazonaws.com"
][0] == true
][0] == true
][
0
] == true
][
0
] == true
error_message = "Principal identifier should be: 'ecs-tasks.amazonaws.com'"
}

Expand Down Expand Up @@ -142,7 +152,7 @@ run "data_dump_unit_test" {

assert {
condition = contains(data.aws_iam_policy_document.data_dump.statement[1].actions, "kms:Decrypt")
error_message = "Permission not found: kms:Encrypt"
error_message = "Permission not found: kms:Decrypt"
}

assert {
Expand Down Expand Up @@ -262,13 +272,37 @@ run "data_dump_unit_test" {
}

assert {
condition = [for el in data.aws_iam_policy_document.data_dump_bucket_policy.statement[0].condition : true if(el.variable == "aws:SecureTransport" && contains(el.values, "false"))] == [true]
condition = [
for el in data.aws_iam_policy_document.data_dump_bucket_policy.statement[0].condition : true
if(el.variable == "aws:SecureTransport" && contains(el.values, "false"))
] == [true]
error_message = "Should be denied if not aws:SecureTransport"
}

# aws_s3_bucket_policy.data_dump_bucket_policy.policy cannot be tested with plan
assert {
condition = [for el in data.aws_iam_policy_document.data_dump_bucket_policy.statement[1].principals : el.type][0] == "AWS"
error_message = "Should be: AWS"
}

assert {
condition = flatten([for el in data.aws_iam_policy_document.data_dump_bucket_policy.statement[1].principals : el.identifiers]) == ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/test-app-dev-test-db-load-task"]
error_message = "Bucket policy principals incorrect"
}

assert {
condition = data.aws_iam_policy_document.data_dump_bucket_policy.statement[1].actions == toset(["s3:ListBucket",
"s3:GetObject",
"s3:GetObjectTagging",
"s3:GetObjectVersion",
"s3:GetObjectVersionTagging",
"s3:DeleteObject"])
error_message = "Unexpected actions"
}

# aws_kms_key.data_dump_kms_key policy cannot be tested with plan
assert {
condition = strcontains(aws_kms_key.data_dump_kms_key.policy, "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root") && !strcontains(aws_kms_key.data_dump_kms_key.policy, "arn:aws:iam::000123456789:role/test-app-dev-test-db-load-task")
error_message = "Unexpected KMS key policy principal"
}

assert {
condition = aws_kms_alias.data_dump_kms_alias.name == "alias/test-app-test-env-test-db-dump"
Expand All @@ -281,7 +315,10 @@ run "data_dump_unit_test" {
}

assert {
condition = [for el in aws_s3_bucket_server_side_encryption_configuration.encryption-config.rule : el.apply_server_side_encryption_by_default[0].sse_algorithm] == ["aws:kms"]
condition = [
for el in aws_s3_bucket_server_side_encryption_configuration.encryption-config.rule :
el.apply_server_side_encryption_by_default[0].sse_algorithm
] == ["aws:kms"]
error_message = "Server side encryption algorithm should be: aws:kms"
}

Expand All @@ -295,3 +332,40 @@ run "data_dump_unit_test" {
error_message = "Public access block has expected conditions"
}
}

run "cross_account_data_dump_unit_test" {
command = plan

variables {
tasks = [
{
from : "prod"
from_account : "123456789000"
to : "dev"
to_account : "000123456789"
}
]
}

assert {
condition = [for el in data.aws_iam_policy_document.data_dump_bucket_policy.statement[1].principals : el.type][0] == "AWS"
error_message = "Should be: AWS"
}
assert {
condition = flatten([for el in data.aws_iam_policy_document.data_dump_bucket_policy.statement[1].principals : el.identifiers]) == ["arn:aws:iam::000123456789:role/test-app-dev-test-db-load-task"]
error_message = "Bucket policy principals incorrect"
}
assert {
condition = data.aws_iam_policy_document.data_dump_bucket_policy.statement[1].actions == toset(["s3:ListBucket",
"s3:GetObject",
"s3:GetObjectTagging",
"s3:GetObjectVersion",
"s3:GetObjectVersionTagging",
"s3:DeleteObject"])
error_message = "Unexpected actions"
}
assert {
condition = strcontains(aws_kms_key.data_dump_kms_key.policy, "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root") && strcontains(aws_kms_key.data_dump_kms_key.policy, "arn:aws:iam::000123456789:role/test-app-dev-test-db-load-task")
error_message = "Unexpected KMS key policy principal"
}
}
9 changes: 9 additions & 0 deletions postgres/database-dump/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ variable "environment" {
variable "database_name" {
type = string
}

variable "tasks" {
type = list(object({
from = string
to = string
from_account = optional(string)
to_account = optional(string)
}))
}
1 change: 0 additions & 1 deletion postgres/database-load/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,4 @@ locals {
dump_bucket_name = local.dump_task_name

ecr_repository_arn = "arn:aws:ecr-public::763451185160:repository/database-copy"

}
24 changes: 6 additions & 18 deletions postgres/database-load/main.tf
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
data "aws_caller_identity" "current" {}

data "aws_kms_key" "data_dump_kms_key" {
key_id = local.dump_kms_key_alias
}

data "aws_s3_bucket" "data_dump_bucket" {
bucket = local.dump_bucket_name
}

data "aws_iam_policy_document" "allow_task_creation" {
statement {
sid = "AllowPullFromEcr"
Expand Down Expand Up @@ -66,10 +58,10 @@ resource "aws_iam_role_policy" "allow_task_creation" {

data "aws_iam_policy_document" "data_load" {
policy_id = "data_load"

statement {
sid = "AllowReadFromS3"
effect = "Allow"

actions = [
"s3:ListBucket",
"s3:GetObject",
Expand All @@ -78,10 +70,9 @@ data "aws_iam_policy_document" "data_load" {
"s3:GetObjectVersionTagging",
"s3:DeleteObject"
]

resources = [
data.aws_s3_bucket.data_dump_bucket.arn,
"${data.aws_s3_bucket.data_dump_bucket.arn}/*"
"arn:aws:s3:::${local.dump_bucket_name}/*",
"arn:aws:s3:::${local.dump_bucket_name}",
]
}

Expand All @@ -102,12 +93,10 @@ data "aws_iam_policy_document" "data_load" {
statement {
sid = "AllowKMSDencryption"
effect = "Allow"

actions = [
"kms:Decrypt",
"kms:Decrypt"
]

resources = [data.aws_kms_key.data_dump_kms_key.arn]
resources = ["arn:aws:kms:eu-west-2:${coalesce(var.task.from_account, data.aws_caller_identity.current.account_id)}:key/*"]
}
}

Expand Down Expand Up @@ -142,7 +131,7 @@ resource "aws_ecs_task_definition" "service" {
},
{
name = "S3_BUCKET_NAME"
value = data.aws_s3_bucket.data_dump_bucket.bucket
value = local.dump_bucket_name
}
],
portMappings = [
Expand All @@ -168,7 +157,6 @@ resource "aws_ecs_task_definition" "service" {
cpu = 1024
memory = 3072


requires_compatibilities = ["FARGATE"]

task_role_arn = aws_iam_role.data_load.arn
Expand Down
Loading

0 comments on commit ac84ca8

Please sign in to comment.