From 71fc7a5ec40544d451cff44eeeca528a3709a69f Mon Sep 17 00:00:00 2001 From: Michael Li Date: Tue, 12 Aug 2025 18:02:18 -0400 Subject: [PATCH] chore(e2e): Add module for rdp member server (#5976) * chore(e2e): Add module for rdp member server * CR: Update Domain mode to be compatible with newer versions of Windows * CR: Add comments to ports * CR: Simplify adding computer to the domain --- enos/enos-modules.hcl | 8 +- enos/enos-scenario-e2e-aws-rdp-target.hcl | 16 +- enos/enos-scenario-e2e-aws-windows.hcl | 119 ++++++++---- .../main.tf | 146 +++++++++++++-- .../aws_rdp_domain_controller/outputs.tf | 48 +++++ .../variables.tf | 39 ++-- enos/modules/aws_rdp_member_server/main.tf | 169 ++++++++++++++++++ enos/modules/aws_rdp_member_server/outputs.tf | 34 ++++ .../aws_rdp_member_server/variables.tf | 73 ++++++++ enos/modules/aws_rdp_server/outputs.tf | 28 --- enos/modules/aws_windows_client/variables.tf | 4 +- enos/modules/test_e2e/main.tf | 118 +++++++----- 12 files changed, 649 insertions(+), 153 deletions(-) rename enos/modules/{aws_rdp_server => aws_rdp_domain_controller}/main.tf (54%) create mode 100644 enos/modules/aws_rdp_domain_controller/outputs.tf rename enos/modules/{aws_rdp_server => aws_rdp_domain_controller}/variables.tf (74%) create mode 100644 enos/modules/aws_rdp_member_server/main.tf create mode 100644 enos/modules/aws_rdp_member_server/outputs.tf create mode 100644 enos/modules/aws_rdp_member_server/variables.tf delete mode 100644 enos/modules/aws_rdp_server/outputs.tf diff --git a/enos/enos-modules.hcl b/enos/enos-modules.hcl index 8cf58d9b21..26d599655c 100644 --- a/enos/enos-modules.hcl +++ b/enos/enos-modules.hcl @@ -43,8 +43,12 @@ module "aws_bucket" { source = "./modules/aws_bucket" } -module "aws_rdp_server" { - source = "./modules/aws_rdp_server" +module "aws_rdp_domain_controller" { + source = "./modules/aws_rdp_domain_controller" +} + +module "aws_rdp_member_server" { + source = "./modules/aws_rdp_member_server" } module "build_crt" { diff --git a/enos/enos-scenario-e2e-aws-rdp-target.hcl b/enos/enos-scenario-e2e-aws-rdp-target.hcl index 166d320ef2..04fbf3f531 100644 --- a/enos/enos-scenario-e2e-aws-rdp-target.hcl +++ b/enos/enos-scenario-e2e-aws-rdp-target.hcl @@ -45,8 +45,8 @@ scenario "e2e_aws_rdp_target" { } } - step "create_rdp_server" { - module = module.aws_rdp_server + step "create_rdp_domain_controller" { + module = module.aws_rdp_domain_controller depends_on = [ step.create_base_infra, ] @@ -59,26 +59,26 @@ scenario "e2e_aws_rdp_target" { } output "rdp_target_admin_username" { - value = step.create_rdp_server.admin_username + value = step.create_rdp_domain_controller.admin_username } output "rdp_target_admin_password" { - value = step.create_rdp_server.password + value = step.create_rdp_domain_controller.password } output "rdp_target_public_dns_address" { - value = step.create_rdp_server.public_dns_address + value = step.create_rdp_domain_controller.public_dns_address } output "rdp_target_public_ip" { - value = step.create_rdp_server.public_ip + value = step.create_rdp_domain_controller.public_ip } output "rdp_target_private_ip" { - value = step.create_rdp_server.private_ip + value = step.create_rdp_domain_controller.private_ip } output "rdp_target_ipv6" { - value = step.create_rdp_server.ipv6 + value = step.create_rdp_domain_controller.ipv6 } } diff --git a/enos/enos-scenario-e2e-aws-windows.hcl b/enos/enos-scenario-e2e-aws-windows.hcl index fb25976362..6e8958d452 100644 --- a/enos/enos-scenario-e2e-aws-windows.hcl +++ b/enos/enos-scenario-e2e-aws-windows.hcl @@ -210,8 +210,8 @@ scenario "e2e_aws_windows" { } } - step "create_rdp_server" { - module = module.aws_rdp_server + step "create_rdp_domain_controller" { + module = module.aws_rdp_domain_controller depends_on = [ step.create_base_infra, ] @@ -222,36 +222,61 @@ scenario "e2e_aws_windows" { } } + step "create_rdp_member_server" { + module = module.aws_rdp_member_server + depends_on = [ + step.create_base_infra, + step.create_rdp_domain_controller, + ] + + variables { + vpc_id = step.create_base_infra.vpc_id + server_version = matrix.rdp_server + active_directory_domain = step.create_rdp_domain_controller.domain_name + domain_controller_aws_keypair_name = step.create_rdp_domain_controller.keypair_name + domain_controller_ip = step.create_rdp_domain_controller.private_ip + domain_admin_password = step.create_rdp_domain_controller.password + domain_controller_private_key = step.create_rdp_domain_controller.ssh_private_key + domain_controller_sec_group_id_list = step.create_rdp_domain_controller.security_group_id_list + } + } + step "run_e2e_test" { module = module.test_e2e depends_on = [ step.create_boundary_cluster, - step.create_rdp_server, + step.create_rdp_domain_controller, + step.create_rdp_member_server, step.create_bucket ] variables { - test_package = "" - debug_no_run = true - alb_boundary_api_addr = step.create_boundary_cluster.alb_boundary_api_addr - auth_method_id = step.create_boundary_cluster.auth_method_id - auth_login_name = step.create_boundary_cluster.auth_login_name - auth_password = step.create_boundary_cluster.auth_password - local_boundary_dir = local.local_boundary_dir - aws_ssh_private_key_path = local.aws_ssh_private_key_path - target_user = "ubuntu" - target_port = "22" - aws_bucket_name = step.create_bucket.bucket_name - aws_region = var.aws_region - max_page_size = step.create_boundary_cluster.max_page_size - worker_tag_collocated = local.collocated_tag - target_rdp_address = step.create_rdp_server.private_ip - target_rdp_user = step.create_rdp_server.admin_username - target_rdp_password = step.create_rdp_server.password - client_ip_public = step.create_windows_client.public_ip - client_username = step.create_windows_client.test_username - client_password = step.create_windows_client.test_password - client_test_dir = step.create_windows_client.test_dir + test_package = "" + debug_no_run = true + alb_boundary_api_addr = step.create_boundary_cluster.alb_boundary_api_addr + auth_method_id = step.create_boundary_cluster.auth_method_id + auth_login_name = step.create_boundary_cluster.auth_login_name + auth_password = step.create_boundary_cluster.auth_password + local_boundary_dir = local.local_boundary_dir + aws_ssh_private_key_path = local.aws_ssh_private_key_path + target_user = "ubuntu" + target_port = "22" + aws_bucket_name = step.create_bucket.bucket_name + aws_region = var.aws_region + max_page_size = step.create_boundary_cluster.max_page_size + worker_tag_collocated = local.collocated_tag + target_rdp_domain_controller_addr = step.create_rdp_domain_controller.private_ip + target_rdp_domain_controller_user = step.create_rdp_domain_controller.admin_username + target_rdp_domain_controller_password = step.create_rdp_domain_controller.password + target_rdp_member_server_addr = step.create_rdp_member_server.private_ip + target_rdp_member_server_domain_hostname = step.create_rdp_member_server.domain_hostname + target_rdp_member_server_user = step.create_rdp_member_server.admin_username + target_rdp_member_server_password = step.create_rdp_member_server.password + target_rdp_domain_name = step.create_rdp_domain_controller.domain_name + client_ip_public = step.create_windows_client.public_ip + client_username = step.create_windows_client.test_username + client_password = step.create_windows_client.test_password + client_test_dir = step.create_windows_client.test_dir } } @@ -263,31 +288,51 @@ scenario "e2e_aws_windows" { value = step.create_boundary_cluster.worker_ips } - output "rdp_target_admin_username" { - value = step.create_rdp_server.admin_username + output "rdp_domain_ssh_key" { + value = step.create_rdp_domain_controller.ssh_private_key } - output "rdp_target_admin_password" { - value = step.create_rdp_server.password + output "rdp_domain_controller_public_ip" { + value = step.create_rdp_domain_controller.public_ip } - output "rdp_target_public_dns_address" { - value = step.create_rdp_server.public_dns_address + output "rdp_domain_controller_private_ip" { + value = step.create_rdp_domain_controller.private_ip } - output "rdp_target_private_ip" { - value = step.create_rdp_server.private_ip + output "rdp_domain_controller_admin_username" { + value = step.create_rdp_domain_controller.admin_username } - output "windows_client_public_ip" { - value = step.create_windows_client.public_ip + output "rdp_domain_controller_admin_password" { + value = step.create_rdp_domain_controller.password + } + + output "rdp_domain" { + value = step.create_rdp_domain_controller.domain_name + } + + output "rdp_member_server_public_ip" { + value = step.create_rdp_member_server.public_ip } - output "windows_client_private_ip" { - value = step.create_windows_client.private_ip + output "rdp_member_server_private_ip" { + value = step.create_rdp_member_server.private_ip + } + + output "rdp_member_server_domain_hostname" { + value = step.create_rdp_member_server.domain_hostname + } + + output "rdp_member_server_admin_password" { + value = step.create_rdp_member_server.password + } + + output "windows_client_public_ip" { + value = step.create_windows_client.public_ip } - output "windows_client_password" { + output "windows_client_admin_password" { value = step.create_windows_client.admin_password } diff --git a/enos/modules/aws_rdp_server/main.tf b/enos/modules/aws_rdp_domain_controller/main.tf similarity index 54% rename from enos/modules/aws_rdp_server/main.tf rename to enos/modules/aws_rdp_domain_controller/main.tf index 35e4ad8d5d..05c7b92dbf 100644 --- a/enos/modules/aws_rdp_server/main.tf +++ b/enos/modules/aws_rdp_domain_controller/main.tf @@ -52,37 +52,49 @@ resource "aws_key_pair" "rdp-key" { } // Create an AWS security group to allow RDP traffic in and out to from IP's on the allowlist. -// We also allow ingress to port 88, where the Kerberos KDC is running. resource "aws_security_group" "rdp_ingress" { name = "${var.prefix}-rdp-ingress-${local.username}-${var.vpc_id}" vpc_id = var.vpc_id + # Allow SSH traffic ingress { - from_port = 3389 - to_port = 3389 + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.current.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + ]) + } + + # Allow DNS (Domain Name System) traffic to resolve hostnames + ingress { + from_port = 53 + to_port = 53 protocol = "tcp" cidr_blocks = flatten([ formatlist("%s/32", data.enos_environment.current.public_ipv4_addresses), join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), ]) ipv6_cidr_blocks = flatten([ - [for ip in coalesce(data.enos_environment.current.public_ipv6_addresses, []) : cidrsubnet("${ip}/64", 0, 0)], + [for ip in coalesce(data.enos_environment.current.public_ipv6_addresses, []) : cidrsubnet("${ip}/64", 0, 0)] ]) } ingress { - from_port = 3389 - to_port = 3389 + from_port = 53 + to_port = 53 protocol = "udp" cidr_blocks = flatten([ formatlist("%s/32", data.enos_environment.current.public_ipv4_addresses), join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), ]) ipv6_cidr_blocks = flatten([ - [for ip in coalesce(data.enos_environment.current.public_ipv6_addresses, []) : cidrsubnet("${ip}/64", 0, 0)], + [for ip in coalesce(data.enos_environment.current.public_ipv6_addresses, []) : cidrsubnet("${ip}/64", 0, 0)] ]) } + # Allow Kerberos authentication traffic ingress { from_port = 88 to_port = 88 @@ -108,6 +120,101 @@ resource "aws_security_group" "rdp_ingress" { [for ip in coalesce(data.enos_environment.current.public_ipv6_addresses, []) : cidrsubnet("${ip}/64", 0, 0)] ]) } + + # Allow RPC (Remote Procedure Calls) traffic + ingress { + from_port = 135 + to_port = 135 + protocol = "tcp" + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.current.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + ]) + ipv6_cidr_blocks = flatten([ + [for ip in coalesce(data.enos_environment.current.public_ipv6_addresses, []) : cidrsubnet("${ip}/64", 0, 0)] + ]) + } + + ingress { + from_port = 135 + to_port = 135 + protocol = "udp" + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.current.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + ]) + ipv6_cidr_blocks = flatten([ + [for ip in coalesce(data.enos_environment.current.public_ipv6_addresses, []) : cidrsubnet("${ip}/64", 0, 0)] + ]) + } + + # Allow LDAP (Lightweight Directory Access Protocol) traffic to query Active Directory + ingress { + from_port = 389 + to_port = 389 + protocol = "tcp" + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.current.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + ]) + ipv6_cidr_blocks = flatten([ + [for ip in coalesce(data.enos_environment.current.public_ipv6_addresses, []) : cidrsubnet("${ip}/64", 0, 0)] + ]) + } + + ingress { + from_port = 389 + to_port = 389 + protocol = "udp" + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.current.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + ]) + ipv6_cidr_blocks = flatten([ + [for ip in coalesce(data.enos_environment.current.public_ipv6_addresses, []) : cidrsubnet("${ip}/64", 0, 0)] + ]) + } + + # Allow Server Message Block (SMB) traffic + ingress { + from_port = 445 + to_port = 445 + protocol = "tcp" + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.current.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + ]) + ipv6_cidr_blocks = flatten([ + [for ip in coalesce(data.enos_environment.current.public_ipv6_addresses, []) : cidrsubnet("${ip}/64", 0, 0)] + ]) + } + + # Allow RDP traffic + ingress { + from_port = 3389 + to_port = 3389 + protocol = "tcp" + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.current.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + ]) + ipv6_cidr_blocks = flatten([ + [for ip in coalesce(data.enos_environment.current.public_ipv6_addresses, []) : cidrsubnet("${ip}/64", 0, 0)], + ]) + } + + ingress { + from_port = 3389 + to_port = 3389 + protocol = "udp" + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.current.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + ]) + ipv6_cidr_blocks = flatten([ + [for ip in coalesce(data.enos_environment.current.public_ipv6_addresses, []) : cidrsubnet("${ip}/64", 0, 0)], + ]) + } } // Create an AWS security group to allow all traffic originating from the default vpc @@ -142,9 +249,9 @@ resource "random_string" "DSRMPassword" { } // Deploy a Windows EC2 instance using the previously created, aws_security_group's, aws_key_pair and use a userdata script to create a set up Active Directory -resource "aws_instance" "rdp_target" { +resource "aws_instance" "domain_controller" { ami = data.aws_ami.infra.id - instance_type = var.rdp_target_instance_type + instance_type = var.instance_type vpc_security_group_ids = [aws_security_group.rdp_ingress.id, aws_security_group.allow_all_internal.id] key_name = aws_key_pair.rdp-key.key_name subnet_id = data.aws_subnets.infra.ids[0] @@ -164,7 +271,9 @@ resource "aws_instance" "rdp_target" { $password = ConvertTo-SecureString ${random_string.DSRMPassword.result} -AsPlainText -Force Add-WindowsFeature -name ad-domain-services -IncludeManagementTools - Install-ADDSForest -CreateDnsDelegation:$false -DomainMode Win2012R2 -DomainName ${var.active_directory_domain} -DomainNetbiosName ${var.active_directory_netbios_name} -ForestMode Win2012R2 -InstallDns:$true -SafeModeAdministratorPassword $password -Force:$true + + # causes the instance to reboot + Install-ADDSForest -CreateDnsDelegation:$false -DomainMode 7 -DomainName ${var.active_directory_domain} -DomainNetbiosName ${var.active_directory_netbios_name} -ForestMode 7 -InstallDns:$true -SafeModeAdministratorPassword $password -Force:$true EOF @@ -175,16 +284,23 @@ resource "aws_instance" "rdp_target" { get_password_data = true tags = { - Name = "${var.prefix}-rdp-target-${local.username}" + Name = "${var.prefix}-domain-controller-${local.username}" } } locals { - password = rsadecrypt(aws_instance.rdp_target.password_data, tls_private_key.rsa_4096_key.private_key_pem) + password = rsadecrypt(aws_instance.domain_controller.password_data, tls_private_key.rsa_4096_key.private_key_pem) +} + +resource "local_sensitive_file" "private_key" { + depends_on = [tls_private_key.rsa_4096_key] + + content = tls_private_key.rsa_4096_key.private_key_pem + filename = "${path.root}/.terraform/tmp/key-domain-controller-${timestamp()}" + file_permission = "0400" } -// This sleep will create a timer of 10 minutes resource "time_sleep" "wait_10_minutes" { - depends_on = [aws_instance.rdp_target] + depends_on = [aws_instance.domain_controller] create_duration = "10m" -} \ No newline at end of file +} diff --git a/enos/modules/aws_rdp_domain_controller/outputs.tf b/enos/modules/aws_rdp_domain_controller/outputs.tf new file mode 100644 index 0000000000..2d77394f50 --- /dev/null +++ b/enos/modules/aws_rdp_domain_controller/outputs.tf @@ -0,0 +1,48 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +output "public_dns_address" { + value = aws_instance.domain_controller.public_dns +} + +output "public_ip" { + value = aws_instance.domain_controller.public_ip +} + +output "private_ip" { + value = aws_instance.domain_controller.private_ip +} + +output "ipv6" { + value = flatten(aws_instance.domain_controller.ipv6_addresses) +} + +output "admin_username" { + description = "The username of the administrator account" + value = "Administrator" +} + +output "password" { + description = "This is the decrypted administrator password for the EC2 instance" + value = nonsensitive(local.password) +} + +output "ssh_private_key" { + description = "Private key to ssh into the windows client" + value = abspath(local_sensitive_file.private_key.filename) +} + +output "security_group_id_list" { + description = "List of security group IDs attached to the RDP server" + value = aws_instance.domain_controller.vpc_security_group_ids +} + +output "keypair_name" { + description = "The name of the keypair used for the instance" + value = aws_key_pair.rdp-key.key_name +} + +output "domain_name" { + description = "The domain name the instance is joined to" + value = var.active_directory_domain +} diff --git a/enos/modules/aws_rdp_server/variables.tf b/enos/modules/aws_rdp_domain_controller/variables.tf similarity index 74% rename from enos/modules/aws_rdp_server/variables.tf rename to enos/modules/aws_rdp_domain_controller/variables.tf index 7b0317eb87..bd4a9188d6 100644 --- a/enos/modules/aws_rdp_server/variables.tf +++ b/enos/modules/aws_rdp_domain_controller/variables.tf @@ -1,50 +1,55 @@ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: BUSL-1.1 -variable "prefix" { - type = string - description = "Prefix used to name various infrastructure components. Alphanumeric characters only." - default = "enos" -} - variable "vpc_id" { type = string description = "Id of VPC to add additional infra resources to." } -variable "aws_key_pair_name" { - type = string - description = "key_name for the aws_key_pair resource" - default = "RDPKey" -} - +# ================================================================= +# ec2 instance configuration +# ================================================================= variable "server_version" { type = string description = "Server version for the windows instance" - # Note that only 2025 and 2022 are supported in aws - default = "2025" + default = "2025" } -variable "rdp_target_instance_type" { +variable "instance_type" { type = string description = "The AWS instance type to use for servers." default = "m7i-flex.xlarge" } +variable "prefix" { + type = string + description = "Prefix used to name various infrastructure components. Alphanumeric characters only." + default = "enos" +} + variable "root_block_device_size" { type = string description = "The volume size of the root block device." default = 128 } +variable "aws_key_pair_name" { + type = string + description = "key_name for the aws_key_pair resource" + default = "RDPKey" +} + +# ================================================================= +# domain information +# ================================================================= variable "active_directory_domain" { type = string description = "The name of the Active Directory domain to be created on the Windows Domain Controller." - default = "mydomain.local" + default = "mydomain.com" } variable "active_directory_netbios_name" { type = string description = "Ostensibly the short-hand for the name of the domain." default = "mydomain" -} \ No newline at end of file +} diff --git a/enos/modules/aws_rdp_member_server/main.tf b/enos/modules/aws_rdp_member_server/main.tf new file mode 100644 index 0000000000..1f631e6fe1 --- /dev/null +++ b/enos/modules/aws_rdp_member_server/main.tf @@ -0,0 +1,169 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +terraform { + required_version = ">= 1.1.2" + + required_providers { + enos = { + source = "registry.terraform.io/hashicorp-forge/enos" + } + } +} + +data "enos_environment" "current" {} + +data "aws_caller_identity" "current" {} + +data "aws_ami" "infra" { + most_recent = true + owners = ["amazon"] + filter { + name = "name" + values = ["Windows_Server-${var.server_version}-English-Full-Base*"] + } +} + +data "aws_vpc" "infra" { + id = var.vpc_id +} + +data "aws_subnets" "infra" { + filter { + name = "vpc-id" + values = [var.vpc_id] + } +} + +locals { + username = split(":", data.aws_caller_identity.current.user_id)[1] + domain_parts = split(".", var.active_directory_domain) + domain_sld = local.domain_parts[0] # second-level domain (example.com --> example) + domain_tld = local.domain_parts[1] # top-level domain (example.com --> com) +} + +resource "aws_instance" "member_server" { + ami = data.aws_ami.infra.id + instance_type = var.instance_type + vpc_security_group_ids = var.domain_controller_sec_group_id_list + key_name = var.domain_controller_aws_keypair_name + subnet_id = data.aws_subnets.infra.ids[0] + ipv6_address_count = 1 + + root_block_device { + volume_type = "gp2" + volume_size = var.root_block_device_size + delete_on_termination = "true" + encrypted = true + } + + user_data_replace_on_change = true + + user_data = < + # Set up SSH so we can remotely manage the instance + ## Install OpenSSH Server and Client + Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 + Set-Service -Name sshd -StartupType 'Automatic' + Start-Service sshd + + Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0 + Set-Service -Name ssh-agent -StartupType Automatic + Start-Service ssh-agent + + ## Set PowerShell as the default SSH shell + New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value (Get-Command powershell.exe).Path -PropertyType String -Force + + ## Configure SSH server to use private key authentication so that scripts don't have to use passwords + ## Save the private key from instance metadata + $ImdsToken = (Invoke-WebRequest -Uri 'http://169.254.169.254/latest/api/token' -Method 'PUT' -Headers @{'X-aws-ec2-metadata-token-ttl-seconds' = 2160} -UseBasicParsing).Content + $ImdsHeaders = @{'X-aws-ec2-metadata-token' = $ImdsToken} + $AuthorizedKey = (Invoke-WebRequest -Uri 'http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key' -Headers $ImdsHeaders -UseBasicParsing).Content + $AuthorizedKeysPath = 'C:\ProgramData\ssh\administrators_authorized_keys' + New-Item -Path $AuthorizedKeysPath -ItemType File -Value $AuthorizedKey -Force + + ## Ensure the SSH agent pulls in the new key. + Set-Service -Name ssh-agent -StartupType "Automatic" + Restart-Service -Name ssh-agent + + ## Open the firewall for SSH connections + New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 + + # Adds member server to the domain + [int]$intix = Get-NetAdapter | % { Process { If ( $_.Status -eq "up" ) { $_.ifIndex } }} +Set-DNSClientServerAddress -interfaceIndex $intix -ServerAddresses ("${var.domain_controller_ip}","127.0.0.1") +$here_string_password = @' +${var.domain_admin_password} +'@ +$password = ConvertTo-SecureString $here_string_password -AsPlainText -Force +$username = "${local.domain_sld}\Administrator" +$credential = New-Object System.Management.Automation.PSCredential($username,$password) + +# check that domain can be reached +$timeout = 300 +$interval = 10 +$elapsed = 0 + +do { + try { + $result = Resolve-DnsName -Name "${var.active_directory_domain}" -Server "${var.domain_controller_ip}" -ErrorAction Stop + if ($result) { + Write-Host "DNS resolved successfully." + break + } + } catch { + Write-Host "DNS not resolved yet. Retrying in $interval seconds..." + Start-Sleep -Seconds $interval + $elapsed += $interval + } + if ($elapsed -ge $timeout) { + Write-Host "DNS resolution failed after 5 minutes. Exiting." + exit 1 + } +} while ($true) + +# add computer to domain +Add-Computer -DomainName "${var.active_directory_domain}" -Credential $credential + +Restart-Computer -Force + + EOF + + metadata_options { + http_endpoint = "enabled" + instance_metadata_tags = "enabled" + } + get_password_data = true + + tags = { + Name = "${var.prefix}-rdp-member-server-${local.username}" + } +} + +locals { + password = rsadecrypt(aws_instance.member_server.password_data, file(var.domain_controller_private_key)) + private_key = abspath(var.domain_controller_private_key) +} + +resource "time_sleep" "wait_2_minutes" { + depends_on = [aws_instance.member_server] + create_duration = "2m" +} + +# wait for the SSH service to be available on the instance. We specifically use +# BatchMode=Yes to prevent SSH from prompting for a password to ensure that we +# can just SSH using the private key +resource "enos_local_exec" "wait_for_ssh" { + depends_on = [time_sleep.wait_2_minutes] + inline = ["timeout 600s bash -c 'until ssh -i ${local.private_key} -o BatchMode=Yes -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no Administrator@${aws_instance.member_server.public_ip} \"echo ready\"; do sleep 10; done'"] +} + +# Retrieve the domain hostname of the member server, which will be used in +# Kerberos +resource "enos_local_exec" "get_hostname" { + depends_on = [ + enos_local_exec.wait_for_ssh, + ] + + inline = ["ssh -i ${local.private_key} -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no Administrator@${aws_instance.member_server.public_ip} '$env:COMPUTERNAME'"] +} diff --git a/enos/modules/aws_rdp_member_server/outputs.tf b/enos/modules/aws_rdp_member_server/outputs.tf new file mode 100644 index 0000000000..b09e4ee836 --- /dev/null +++ b/enos/modules/aws_rdp_member_server/outputs.tf @@ -0,0 +1,34 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +output "public_dns_address" { + description = "This is the public DNS address of our instance" + value = aws_instance.member_server.public_dns +} + +output "public_ip" { + value = aws_instance.member_server.public_ip +} + +output "private_ip" { + value = aws_instance.member_server.private_ip +} + +output "ipv6" { + value = flatten(aws_instance.member_server.ipv6_addresses) +} + +output "admin_username" { + description = "The username of the administrator account" + value = "Administrator" +} + +output "password" { + description = "This is the decrypted administrator password for the EC2 instance" + value = local.password +} + +output "domain_hostname" { + description = "The hostname of the domain controller" + value = trimspace(enos_local_exec.get_hostname.stdout) +} diff --git a/enos/modules/aws_rdp_member_server/variables.tf b/enos/modules/aws_rdp_member_server/variables.tf new file mode 100644 index 0000000000..fbdb7f3807 --- /dev/null +++ b/enos/modules/aws_rdp_member_server/variables.tf @@ -0,0 +1,73 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +variable "vpc_id" { + type = string + description = "Id of VPC to add additional infra resources to." +} + +# ================================================================= +# ec2 instance configuration +# ================================================================= +variable "server_version" { + type = string + description = "Server version for the windows instance" + default = "2025" +} + +variable "instance_type" { + type = string + description = "The AWS instance type to use for servers." + default = "m7i-flex.xlarge" +} + +variable "root_block_device_size" { + type = string + description = "The volume size of the root block device." + default = 128 +} + +variable "prefix" { + type = string + description = "Prefix used to name various infrastructure components. Alphanumeric characters only." + default = "enos" +} + +variable "domain_hostname" { + type = string + description = "Hostname to assign to the member server" + default = "MyWindowsServer" +} + +variable "active_directory_domain" { + type = string + description = "The name of the Active Directory domain to be created on the Windows Domain Controller." +} + +# ================================================================= +# domain controller information +# ================================================================= +variable "domain_controller_aws_keypair_name" { + type = string + description = "The AWS keypair created during creation of the domain controller." +} + +variable "domain_controller_ip" { + type = string + description = "IP Address of an already created Domain Controller and DNS server." +} + +variable "domain_admin_password" { + type = string + description = "The domain administrator password." +} + +variable "domain_controller_private_key" { + type = string + description = "The file path of the private key generated during creation of the domain controller." +} + +variable "domain_controller_sec_group_id_list" { + type = list(any) + description = "ID's of AWS Network Security Groups created during creation of the domain controller." +} diff --git a/enos/modules/aws_rdp_server/outputs.tf b/enos/modules/aws_rdp_server/outputs.tf deleted file mode 100644 index ce930bc9f5..0000000000 --- a/enos/modules/aws_rdp_server/outputs.tf +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: BUSL-1.1 - -output "public_dns_address" { - value = aws_instance.rdp_target.public_dns -} - -output "public_ip" { - value = aws_instance.rdp_target.public_ip -} - -output "private_ip" { - value = aws_instance.rdp_target.private_ip -} - -output "admin_username" { - description = "The username of the administrator account" - value = "Administrator" -} - -output "password" { - description = "This is the decrypted administrator password for the EC2 instance" - value = nonsensitive(local.password) -} - -output "ipv6" { - value = flatten(aws_instance.rdp_target.*.ipv6_addresses) -} \ No newline at end of file diff --git a/enos/modules/aws_windows_client/variables.tf b/enos/modules/aws_windows_client/variables.tf index 08b45df60d..0525ca963a 100644 --- a/enos/modules/aws_windows_client/variables.tf +++ b/enos/modules/aws_windows_client/variables.tf @@ -34,7 +34,7 @@ variable "prefix" { } # ================================================================= -# Paths for source code +# additional resources # ================================================================= variable "boundary_cli_zip_path" { description = "Path to the boundary cli zip file (windows, amd64)" @@ -46,4 +46,4 @@ variable "boundary_src_path" { description = "Path to the boundary source code" type = string default = "" -} \ No newline at end of file +} diff --git a/enos/modules/test_e2e/main.tf b/enos/modules/test_e2e/main.tf index 8fea4ece42..773e860097 100644 --- a/enos/modules/test_e2e/main.tf +++ b/enos/modules/test_e2e/main.tf @@ -71,7 +71,6 @@ variable "vault_addr_public" { type = string default = "" } - variable "vault_addr_private" { description = "Private address to a vault instance" type = string @@ -156,18 +155,44 @@ variable "boundary_license" { type = string default = "" } -variable "target_rdp_user" { - description = "RDP username for target" + +variable "target_rdp_domain_controller_addr" { + description = "Address of RDP domain controller" + type = string + default = "" +} +variable "target_rdp_domain_controller_user" { + description = "Username for RDP domain controller" + type = string + default = "Administrator" +} +variable "target_rdp_domain_controller_password" { + description = "Password for RDP domain controller" + type = string + default = "" +} +variable "target_rdp_member_server_addr" { + description = "Address of RDP member server" + type = string + default = "" +} +variable "target_rdp_member_server_domain_hostname" { + description = "Domain Name of RDP member server" + type = string + default = "" +} +variable "target_rdp_member_server_user" { + description = "Username for RDP member server" type = string default = "Administrator" } -variable "target_rdp_password" { - description = "RDP password for target" +variable "target_rdp_member_server_password" { + description = "Password for RDP member server" type = string default = "" } -variable "target_rdp_address" { - description = "Address of target for RDP" +variable "target_rdp_domain_name" { + description = "Domain name of RDP targets" type = string default = "" } @@ -212,43 +237,48 @@ locals { resource "enos_local_exec" "run_e2e_test" { environment = { - E2E_TESTS = "true" - BOUNDARY_ADDR = var.alb_boundary_api_addr - BOUNDARY_LICENSE = var.boundary_license - E2E_PASSWORD_AUTH_METHOD_ID = var.auth_method_id - E2E_PASSWORD_ADMIN_LOGIN_NAME = var.auth_login_name - E2E_PASSWORD_ADMIN_PASSWORD = var.auth_password - E2E_TARGET_ADDRESS = var.target_address - E2E_TARGET_PORT = var.target_port - E2E_SSH_USER = var.target_user - E2E_SSH_KEY_PATH = local.aws_ssh_private_key_path - E2E_SSH_CA_KEY = "" - VAULT_ADDR = var.vault_addr_public - VAULT_TOKEN = var.vault_root_token - E2E_VAULT_ADDR_PUBLIC = var.vault_addr_public - E2E_VAULT_ADDR_PRIVATE = var.vault_addr_private - E2E_AWS_ACCESS_KEY_ID = var.aws_access_key_id - E2E_AWS_SECRET_ACCESS_KEY = var.aws_secret_access_key - E2E_AWS_HOST_SET_FILTER = var.aws_host_set_filter1 - E2E_AWS_HOST_SET_IPS = local.aws_host_set_ips1 - E2E_AWS_HOST_SET_FILTER2 = var.aws_host_set_filter2 - E2E_AWS_HOST_SET_IPS2 = local.aws_host_set_ips2 - E2E_AWS_REGION = var.aws_region - E2E_AWS_BUCKET_NAME = var.aws_bucket_name - E2E_AWS_ROLE_ARN = var.aws_role_arn - E2E_WORKER_TAG_INGRESS = var.worker_tag_ingress - E2E_WORKER_TAG_ISOLATED = var.worker_tag_isolated - E2E_WORKER_TAG_COLLOCATED = var.worker_tag_collocated - E2E_WORKER_ADDRESS = var.worker_address - E2E_MAX_PAGE_SIZE = var.max_page_size - E2E_IP_VERSION = var.ip_version - E2E_TARGET_RDP_USER = var.target_rdp_user - E2E_TARGET_RDP_PASSWORD = var.target_rdp_password - E2E_TARGET_RDP_ADDRESS = var.target_rdp_address - E2E_CLIENT_IP_PUBLIC = var.client_ip_public - E2E_CLIENT_USERNAME = var.client_username - E2E_CLIENT_PASSWORD = var.client_password - E2E_CLIENT_TEST_DIR = var.client_test_dir + E2E_TESTS = "true" + BOUNDARY_ADDR = var.alb_boundary_api_addr + BOUNDARY_LICENSE = var.boundary_license + E2E_PASSWORD_AUTH_METHOD_ID = var.auth_method_id + E2E_PASSWORD_ADMIN_LOGIN_NAME = var.auth_login_name + E2E_PASSWORD_ADMIN_PASSWORD = var.auth_password + E2E_TARGET_ADDRESS = var.target_address + E2E_TARGET_PORT = var.target_port + E2E_SSH_USER = var.target_user + E2E_SSH_KEY_PATH = local.aws_ssh_private_key_path + E2E_SSH_CA_KEY = "" + VAULT_ADDR = var.vault_addr_public + VAULT_TOKEN = var.vault_root_token + E2E_VAULT_ADDR_PUBLIC = var.vault_addr_public + E2E_VAULT_ADDR_PRIVATE = var.vault_addr_private + E2E_AWS_ACCESS_KEY_ID = var.aws_access_key_id + E2E_AWS_SECRET_ACCESS_KEY = var.aws_secret_access_key + E2E_AWS_HOST_SET_FILTER = var.aws_host_set_filter1 + E2E_AWS_HOST_SET_IPS = local.aws_host_set_ips1 + E2E_AWS_HOST_SET_FILTER2 = var.aws_host_set_filter2 + E2E_AWS_HOST_SET_IPS2 = local.aws_host_set_ips2 + E2E_AWS_REGION = var.aws_region + E2E_AWS_BUCKET_NAME = var.aws_bucket_name + E2E_AWS_ROLE_ARN = var.aws_role_arn + E2E_WORKER_TAG_INGRESS = var.worker_tag_ingress + E2E_WORKER_TAG_ISOLATED = var.worker_tag_isolated + E2E_WORKER_TAG_COLLOCATED = var.worker_tag_collocated + E2E_WORKER_ADDRESS = var.worker_address + E2E_MAX_PAGE_SIZE = var.max_page_size + E2E_IP_VERSION = var.ip_version + E2E_TARGET_RDP_DOMAIN_CONTROLLER_ADDR = var.target_rdp_domain_controller_addr + E2E_TARGET_RDP_DOMAIN_CONTROLLER_USER = var.target_rdp_domain_controller_user + E2E_TARGET_RDP_DOMAIN_CONTROLLER_PASSWORD = var.target_rdp_domain_controller_password + E2E_TARGET_RDP_MEMBER_SERVER_ADDR = var.target_rdp_member_server_addr + E2E_TARGET_RDP_MEMBER_SERVER_DOMAIN_HOSTNAME = var.target_rdp_member_server_domain_hostname + E2E_TARGET_RDP_MEMBER_SERVER_USER = var.target_rdp_member_server_user + E2E_TARGET_RDP_MEMBER_SERVER_PASSWORD = var.target_rdp_member_server_password + E2E_TARGET_RDP_DOMAIN_NAME = var.target_rdp_domain_name + E2E_CLIENT_IP_PUBLIC = var.client_ip_public + E2E_CLIENT_USERNAME = var.client_username + E2E_CLIENT_PASSWORD = var.client_password + E2E_CLIENT_TEST_DIR = var.client_test_dir } inline = var.debug_no_run ? [""] : [