diff --git a/enos/enos-modules.hcl b/enos/enos-modules.hcl index 965fc96a8b..5e670762d0 100644 --- a/enos/enos-modules.hcl +++ b/enos/enos-modules.hcl @@ -43,6 +43,10 @@ module "aws_bucket" { source = "./modules/aws_bucket" } +module "aws_rdp_server" { + source = "./modules/aws_rdp_server" +} + module "build_crt" { source = "./modules/build_crt" } diff --git a/enos/enos-scenario-e2e-aws-rdp-target.hcl b/enos/enos-scenario-e2e-aws-rdp-target.hcl new file mode 100644 index 0000000000..759bebde37 --- /dev/null +++ b/enos/enos-scenario-e2e-aws-rdp-target.hcl @@ -0,0 +1,84 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +scenario "e2e_aws_rdp_target" { + terraform_cli = terraform_cli.default + terraform = terraform.default + providers = [ + provider.aws.default, + provider.enos.default + ] + + matrix { + rdp_server = ["2016", "2019", "2022", "2025"] + } + + locals { + tags = merge({ + "Project Name" : var.project_name + "Project" : "Enos", + "Environment" : "ci" + }, var.tags) + + } + + step "find_azs" { + module = module.aws_az_finder + + variables { + instance_type = [ + var.worker_instance_type, + var.controller_instance_type + ] + } + } + + step "create_base_infra" { + module = module.aws_vpc_ipv6 + + depends_on = [ + step.find_azs, + ] + + variables { + availability_zones = step.find_azs.availability_zones + common_tags = local.tags + } + } + + step "create_rdp_server" { + module = module.aws_rdp_server + depends_on = [ + step.create_base_infra, + ] + + variables { + vpc_id = step.create_base_infra.vpc_id + server_version = matrix.rdp_server + } + } + + output "rdp_target_admin_username" { + value = step.create_rdp_server.admin_username + } + + output "rdp_target_admin_password" { + value = step.create_rdp_server.password + } + + output "rdp_target_public_dns_address" { + value = step.create_rdp_server.public-dns-address + } + + output "rdp_target_public_ip" { + value = step.create_rdp_server.public_ip + } + + output "rdp_target_private_ip" { + value = step.create_rdp_server.private_ip + } + + output "rdp_target_ipv6" { + value = step.create_rdp_server.ipv6 + } +} \ No newline at end of file diff --git a/enos/modules/aws_rdp_server/main.tf b/enos/modules/aws_rdp_server/main.tf new file mode 100644 index 0000000000..35e4ad8d5d --- /dev/null +++ b/enos/modules/aws_rdp_server/main.tf @@ -0,0 +1,190 @@ +# 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] +} + +// We need a keypair to obtain the local administrator credentials to an AWS Windows based EC2 instance. So we generate it locally here +resource "tls_private_key" "rsa_4096_key" { + algorithm = "RSA" + rsa_bits = 4096 +} + +// Create an AWS keypair using the keypair we just generated +resource "aws_key_pair" "rdp-key" { + key_name = "${var.prefix}-${var.aws_key_pair_name}-${local.username}-${var.vpc_id}" + public_key = tls_private_key.rsa_4096_key.public_key_openssh +} + +// 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 + + 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)], + ]) + } + + ingress { + from_port = 88 + to_port = 88 + 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 = 88 + to_port = 88 + 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 +resource "aws_security_group" "allow_all_internal" { + name = "${var.prefix}-allow-all-internal-${local.username}-${var.vpc_id}" + vpc_id = var.vpc_id + + ingress { + from_port = 0 + to_port = 0 + protocol = "-1" + self = true + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } +} + +// Create a random string to be used in the user_data script +resource "random_string" "DSRMPassword" { + length = 8 + override_special = "." # I've set this explicitly so as to avoid characters such as "$" and "'" being used and requiring unneccesary complexity to our user_data scripts + min_lower = 1 + min_upper = 1 + min_numeric = 1 + min_special = 1 +} + +// 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" { + ami = data.aws_ami.infra.id + instance_type = var.rdp_target_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] + 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 = < + $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 + + EOF + + metadata_options { + http_endpoint = "enabled" + instance_metadata_tags = "enabled" + } + get_password_data = true + + tags = { + Name = "${var.prefix}-rdp-target-${local.username}" + } +} + +locals { + password = rsadecrypt(aws_instance.rdp_target.password_data, tls_private_key.rsa_4096_key.private_key_pem) +} + +// This sleep will create a timer of 10 minutes +resource "time_sleep" "wait_10_minutes" { + depends_on = [aws_instance.rdp_target] + create_duration = "10m" +} \ No newline at end of file diff --git a/enos/modules/aws_rdp_server/outputs.tf b/enos/modules/aws_rdp_server/outputs.tf new file mode 100644 index 0000000000..58a669893f --- /dev/null +++ b/enos/modules/aws_rdp_server/outputs.tf @@ -0,0 +1,28 @@ +# 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_rdp_server/variables.tf b/enos/modules/aws_rdp_server/variables.tf new file mode 100644 index 0000000000..7b0317eb87 --- /dev/null +++ b/enos/modules/aws_rdp_server/variables.tf @@ -0,0 +1,50 @@ +# 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" +} + +variable "server_version" { + type = string + description = "Server version for the windows instance" + # Note that only 2025 and 2022 are supported in aws + default = "2025" +} + +variable "rdp_target_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 "active_directory_domain" { + type = string + description = "The name of the Active Directory domain to be created on the Windows Domain Controller." + default = "mydomain.local" +} + +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