* feat(tests) Add Terraform testing to support Cross-account discovery **Problem** * To support cross-account DHC discovery, we need to add terraform configurations and tests to test against live AWS data. * However, there was nothing in place to support any AssumeRole testing, irrespective of cross-account. **Solution** * Added a new terraform file, iam_assume_role.tf, to contain the new terraform configurations for single-account AssumeRole and cross-role * Added a second terraform file, iam_cross_account.tf, to contain the new terraform configurations for cross-account * Refactored how the tests are organized. The previous static test cases used a system where a separate function was created for each test. This was moved to a dedicated file, e2e_host_static_test.go. * For AssumeRole tests (single account, cross role, and cross account), this system wasn't ideal since there are so many test cases and they are all extremely similar. A table driven system was better that passed in a set of variables to shared test helpers. A dedicated file, e2e_host_assumerole_test.go, was created to contain the testing infrastructure for AssumeRole tests. * e2e_host_test.go remains the main test runner that then calls e2e_host_assumerole_test.go and e2e_host_static_test.go * The terraform tests work by setting environment variables for the AWS account that you want to configure the terraform for. The existing setup worked for single-account and cross-role. However, to support cross-account, support had to be added for a second set of credentials. The second set of environment variables can now be set with the TARGET_ prefix. Some configuration in testing.go had to be added to support creating/destroying multiple terraform setups. **Testing** * The single account tests pass as expected. However, a bug was discovered during this process that will be addressed later. * The cross-role and cross-account tests failed as expected because this featured has not yet been delivered. * It was noticed that some of the existing static tests were flaky. The problem and fix were identified and will be addressed later.
323 lines
9.8 KiB
Terraform
323 lines
9.8 KiB
Terraform
# Copyright IBM Corp. 2021, 2026
|
|
# SPDX-License-Identifier: MPL-2.0
|
|
|
|
# Cross-account Dynamic Host Catalog test infrastructure.
|
|
#
|
|
# This file provisions the IAM resources needed to test two-hop AssumeRole
|
|
# across two separate AWS accounts:
|
|
#
|
|
# Account A (principal account) — the default provider
|
|
# * cross_account_principal: the plugin assumes this role first.
|
|
# It has sts:AssumeRole permission on the target role in Account B.
|
|
#
|
|
# Account B (target account) — the "target" provider alias
|
|
# * cross_account_target: holds ec2:DescribeInstances; its trust policy
|
|
# allows the Account A principal role to assume it. (happy path)
|
|
# * cross_account_target_no_ec2_permission: same trust policy but no
|
|
# ec2:DescribeInstances. (error case)
|
|
# * cross_account_target_no_trust: exists in Account B but its trust
|
|
# policy does not allow the Account A principal. (error case)
|
|
#
|
|
# Prerequisites:
|
|
# TARGET_AWS_ACCESS_KEY_ID, TARGET_AWS_SECRET_ACCESS_KEY, and (for temporary
|
|
# credentials) TARGET_AWS_SESSION_TOKEN must be set to credentials that have
|
|
# sufficient IAM permissions in Account B. The test runner skips this
|
|
# section when the access key ID or secret access key are absent.
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Provider for Account B (target account)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
provider "aws" {
|
|
alias = "target"
|
|
region = var.target_region
|
|
access_key = var.target_access_key_id
|
|
secret_key = var.target_secret_access_key
|
|
token = var.target_session_token
|
|
}
|
|
|
|
data "aws_caller_identity" "target" {
|
|
provider = aws.target
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Variables for Account B credentials and region
|
|
# ---------------------------------------------------------------------------
|
|
|
|
variable "target_access_key_id" {
|
|
description = "AWS access key ID for the target account (Account B)."
|
|
type = string
|
|
sensitive = true
|
|
default = ""
|
|
}
|
|
|
|
variable "target_secret_access_key" {
|
|
description = "AWS secret access key for the target account (Account B)."
|
|
type = string
|
|
sensitive = true
|
|
default = ""
|
|
}
|
|
|
|
variable "target_session_token" {
|
|
description = "AWS session token for the target account (Account B). Required when using temporary (STS) credentials."
|
|
type = string
|
|
sensitive = true
|
|
default = ""
|
|
}
|
|
|
|
variable "target_region" {
|
|
description = "AWS region for the target account (Account B). Defaults to the same region as Account A."
|
|
type = string
|
|
default = ""
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Account A: cross_account_principal role
|
|
# ---------------------------------------------------------------------------
|
|
|
|
resource "random_id" "cross_account_principal_name" {
|
|
prefix = "BoundaryPluginCAPrincipal"
|
|
byte_length = 4
|
|
}
|
|
|
|
resource "aws_iam_role" "cross_account_principal" {
|
|
name = random_id.cross_account_principal_name.dec
|
|
|
|
assume_role_policy = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [
|
|
{
|
|
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
Effect = "Allow"
|
|
Principal = { AWS = data.aws_caller_identity.current.arn }
|
|
},
|
|
]
|
|
})
|
|
|
|
tags = {
|
|
"boundary-demo" = local.hashicorp_email
|
|
}
|
|
}
|
|
|
|
# Allow the principal role to assume the happy-path and no-EC2-permission
|
|
# target roles in Account B.
|
|
resource "aws_iam_policy" "cross_account_principal_assume_targets" {
|
|
name = "${random_id.cross_account_principal_name.dec}-assume-targets"
|
|
|
|
policy = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [
|
|
{
|
|
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
Effect = "Allow"
|
|
Resource = [
|
|
aws_iam_role.cross_account_target.arn,
|
|
aws_iam_role.cross_account_target_no_ec2_permission.arn,
|
|
]
|
|
},
|
|
]
|
|
})
|
|
}
|
|
|
|
resource "aws_iam_role_policy_attachment" "cross_account_principal_assume_targets" {
|
|
role = aws_iam_role.cross_account_principal.name
|
|
policy_arn = aws_iam_policy.cross_account_principal_assume_targets.arn
|
|
}
|
|
|
|
output "cross_account_principal_arn" {
|
|
value = aws_iam_role.cross_account_principal.arn
|
|
}
|
|
|
|
# Error case: caller cannot assume the principal role (first hop denied).
|
|
# Reuses assume_role_no_trust from iam_assume_role.tf — its Deny trust policy
|
|
# also blocks the test runner from assuming it as a principal.
|
|
output "cross_account_principal_no_trust_arn" {
|
|
value = aws_iam_role.assume_role_no_trust.arn
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Account B: happy-path target role
|
|
# ---------------------------------------------------------------------------
|
|
|
|
resource "random_id" "cross_account_target_name" {
|
|
prefix = "BoundaryPluginCATarget"
|
|
byte_length = 4
|
|
}
|
|
|
|
resource "aws_iam_role" "cross_account_target" {
|
|
provider = aws.target
|
|
name = random_id.cross_account_target_name.dec
|
|
|
|
assume_role_policy = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [
|
|
{
|
|
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
Effect = "Allow"
|
|
Principal = { AWS = aws_iam_role.cross_account_principal.arn }
|
|
},
|
|
]
|
|
})
|
|
|
|
tags = {
|
|
"boundary-demo" = local.hashicorp_email
|
|
}
|
|
}
|
|
|
|
resource "aws_iam_policy" "cross_account_target_ec2_describeinstances" {
|
|
provider = aws.target
|
|
name = "${random_id.cross_account_target_name.dec}-ec2"
|
|
|
|
policy = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [
|
|
{
|
|
Action = ["ec2:DescribeInstances"]
|
|
Effect = "Allow"
|
|
Resource = "*"
|
|
},
|
|
]
|
|
})
|
|
}
|
|
|
|
resource "aws_iam_role_policy_attachment" "cross_account_target_ec2_describeinstances" {
|
|
provider = aws.target
|
|
role = aws_iam_role.cross_account_target.name
|
|
policy_arn = aws_iam_policy.cross_account_target_ec2_describeinstances.arn
|
|
}
|
|
|
|
output "cross_account_target_arn" {
|
|
value = aws_iam_role.cross_account_target.arn
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Account B: error case — both hops succeed but target has no ec2:DescribeInstances
|
|
# ---------------------------------------------------------------------------
|
|
|
|
resource "random_id" "cross_account_target_no_ec2_permission_name" {
|
|
prefix = "BoundaryPluginCATargetNoEC2"
|
|
byte_length = 4
|
|
}
|
|
|
|
resource "aws_iam_role" "cross_account_target_no_ec2_permission" {
|
|
provider = aws.target
|
|
name = random_id.cross_account_target_no_ec2_permission_name.dec
|
|
|
|
assume_role_policy = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [
|
|
{
|
|
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
Effect = "Allow"
|
|
Principal = { AWS = aws_iam_role.cross_account_principal.arn }
|
|
},
|
|
]
|
|
})
|
|
|
|
tags = {
|
|
"boundary-demo" = local.hashicorp_email
|
|
}
|
|
}
|
|
|
|
output "cross_account_target_no_ec2_permission_arn" {
|
|
value = aws_iam_role.cross_account_target_no_ec2_permission.arn
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Account B: error case — principal cannot assume this target (second hop denied)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
resource "random_id" "cross_account_target_no_trust_name" {
|
|
prefix = "BoundaryPluginCATargetNoTrust"
|
|
byte_length = 4
|
|
}
|
|
|
|
resource "aws_iam_role" "cross_account_target_no_trust" {
|
|
provider = aws.target
|
|
name = random_id.cross_account_target_no_trust_name.dec
|
|
|
|
assume_role_policy = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [
|
|
{
|
|
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
Effect = "Deny"
|
|
Principal = { AWS = aws_iam_role.cross_account_principal.arn }
|
|
},
|
|
]
|
|
})
|
|
|
|
tags = {
|
|
"boundary-demo" = local.hashicorp_email
|
|
}
|
|
}
|
|
|
|
output "cross_account_target_no_trust_arn" {
|
|
value = aws_iam_role.cross_account_target_no_trust.arn
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Account B: EC2 instance for cross-account host discovery validation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
data "aws_availability_zones" "target_azs" {
|
|
count = var.target_access_key_id != "" ? 1 : 0
|
|
provider = aws.target
|
|
}
|
|
|
|
data "aws_ami" "target_ubuntu" {
|
|
count = var.target_access_key_id != "" ? 1 : 0
|
|
provider = aws.target
|
|
most_recent = true
|
|
|
|
filter {
|
|
name = "name"
|
|
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
|
|
}
|
|
|
|
filter {
|
|
name = "virtualization-type"
|
|
values = ["hvm"]
|
|
}
|
|
|
|
owners = ["099720109477"] # Canonical
|
|
}
|
|
|
|
resource "aws_vpc" "target_vpc" {
|
|
count = var.target_access_key_id != "" ? 1 : 0
|
|
provider = aws.target
|
|
cidr_block = "10.1.0.0/16"
|
|
assign_generated_ipv6_cidr_block = true
|
|
}
|
|
|
|
resource "aws_subnet" "target_subnet" {
|
|
count = var.target_access_key_id != "" ? 1 : 0
|
|
provider = aws.target
|
|
vpc_id = aws_vpc.target_vpc[0].id
|
|
cidr_block = aws_vpc.target_vpc[0].cidr_block
|
|
ipv6_cidr_block = cidrsubnet(aws_vpc.target_vpc[0].ipv6_cidr_block, 8, 0)
|
|
availability_zone = data.aws_availability_zones.target_azs[0].names[0]
|
|
map_public_ip_on_launch = true
|
|
assign_ipv6_address_on_creation = true
|
|
}
|
|
|
|
resource "aws_instance" "target_instances" {
|
|
count = var.target_access_key_id != "" ? length(local.instance_tags) : 0
|
|
provider = aws.target
|
|
ami = data.aws_ami.target_ubuntu[0].id
|
|
instance_type = "t3.nano"
|
|
subnet_id = aws_subnet.target_subnet[0].id
|
|
|
|
tags = local.instance_tags[count.index]
|
|
}
|
|
|
|
output "target_instance_ids" {
|
|
value = aws_instance.target_instances.*.id
|
|
}
|
|
|
|
output "target_instance_tags" {
|
|
value = {
|
|
for _, r in aws_instance.target_instances : r.id => r.tags
|
|
}
|
|
}
|