Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: DBTP-1299 - Cross account database copy #294

Merged
merged 10 commits into from
Dec 3, 2024
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