Ir al contenido

Terraform Source Reference

Esta página aún no está disponible en tu idioma.

The following five files are staged from the canonical terraform/aws implementation. The rendered source is the reference; do not copy generated resource identifiers or runtime outputs into persistent configuration.

versions.tf pins Terraform and provider constraints, configures encrypted remote S3 state with native locking, restricts the AWS provider to the approved account, and includes the required explicit provider "xcsh" {} block. The F5 Distributed Cloud provider source is f5-sales-demo/xcsh.

terraform {
required_version = ">= 1.14.0"
backend "s3" {
bucket = "terraform-tfstate-xc"
key = "f5-sales-demo/client-side-defense.tfstate"
region = "us-east-1"
use_lockfile = true
encrypt = true
}
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 6.0, < 7.0"
}
xcsh = {
source = "f5-sales-demo/xcsh"
version = "9.5.1"
}
}
}
provider "aws" {
profile = var.aws_profile
region = var.aws_region
allowed_account_ids = [var.expected_aws_account_id]
default_tags {
tags = var.tags
}
}
provider "xcsh" {}

variables.tf defines the reviewed account, profile, region, namespace, domain, network layout, restricted F5 Distributed Cloud origin CIDRs, and common tags. Its validations prevent accidental reuse against another environment.

variable "expected_aws_account_id" {
description = "AWS account that is authorized to own this stack."
type = string
default = "280469140135"
validation {
condition = var.expected_aws_account_id == "280469140135"
error_message = "This stack is restricted to AWS account 280469140135."
}
}
variable "aws_profile" {
description = "Local AWS shared-configuration profile."
type = string
default = "Users-280469140135"
validation {
condition = var.aws_profile == "Users-280469140135"
error_message = "Use the approved Users-280469140135 profile."
}
}
variable "aws_region" {
description = "AWS region for the application origin."
type = string
default = "us-east-1"
validation {
condition = var.aws_region == "us-east-1"
error_message = "This stack is restricted to us-east-1."
}
}
variable "namespace" {
description = "F5 Distributed Cloud namespace."
type = string
default = "client-side-defense"
validation {
condition = var.namespace == "client-side-defense"
error_message = "This stack is restricted to the client-side-defense namespace."
}
}
variable "domain" {
description = "Public application domain protected by F5 Distributed Cloud."
type = string
default = "client-side-defense.f5-sales-demo.com"
validation {
condition = var.domain == "client-side-defense.f5-sales-demo.com"
error_message = "This stack is restricted to client-side-defense.f5-sales-demo.com."
}
}
variable "vpc_cidr" {
description = "CIDR for the dedicated CSD application VPC."
type = string
default = "10.43.0.0/16"
validation {
condition = var.vpc_cidr == "10.43.0.0/16"
error_message = "The reviewed dedicated VPC CIDR is 10.43.0.0/16."
}
}
variable "public_subnet_cidrs" {
description = "CIDRs for public ALB subnets in us-east-1a and us-east-1b."
type = list(string)
default = ["10.43.0.0/24", "10.43.1.0/24"]
validation {
condition = tolist(var.public_subnet_cidrs) == tolist(["10.43.0.0/24", "10.43.1.0/24"])
error_message = "Public subnet CIDRs must retain the reviewed two-AZ layout."
}
}
variable "private_subnet_cidrs" {
description = "CIDRs for private Fargate subnets in us-east-1a and us-east-1b."
type = list(string)
default = ["10.43.10.0/24", "10.43.11.0/24"]
validation {
condition = tolist(var.private_subnet_cidrs) == tolist(["10.43.10.0/24", "10.43.11.0/24"])
error_message = "Private subnet CIDRs must retain the reviewed two-AZ layout."
}
}
variable "tags" {
description = "Tags applied to supported AWS resources."
type = map(string)
default = {
application = "client-side-defense"
managed-by = "terraform"
repository = "f5-sales-demo/csd"
}
}

main.tf contains the entitlement precondition, AWS networking and logging resources, the vendored origin module, and the F5 Distributed Cloud resources. The AWS origin pool uses the module’s ALB hostname through public_name; the HTTPS load balancer redirects HTTP, uses the public default VIP, references one default-route pool, and enables all-pages CSD injection.

data "aws_caller_identity" "current" {}
data "aws_partition" "current" {}
data "xcsh_addon_service_activation_status" "csd" {
addon_service = "f5xc-client-side-defense-standard"
}
locals {
name = "csd-juice-shop"
availability_zones = ["us-east-1a", "us-east-1b"]
alb_logs_bucket_prefix = "f5-sales-demo-csd-alb-logs-"
alb_access_logs_prefix = "juice-shop"
# Authoritative machine-readable source: https://docs.cloud.f5.com/docs-v2/downloads/platform/reference/network-cloud-ref/ips-domains.json
# Revalidate this complete global Regional Edge TCP IPv4 set before every apply using docs/en/terraform/operations.mdx.
regional_edge_origin_cidrs = toset([
# Americas
"5.182.215.0/25",
"84.54.61.0/25",
"23.158.32.0/25",
"84.54.62.0/25",
"185.94.143.0/25",
"185.94.142.0/24",
"159.60.190.0/24",
"159.60.168.0/24",
"159.60.180.0/24",
"159.60.174.0/24",
"159.60.175.0/24",
"159.60.176.0/24",
"159.60.177.0/24",
"159.60.179.0/24",
"159.60.181.0/24",
"159.60.183.0/24",
# Europe
"5.182.213.0/24",
"5.182.212.0/25",
"5.182.214.0/25",
"84.54.60.0/25",
"185.56.154.0/25",
"159.60.160.0/24",
"159.60.161.0/24",
"159.60.162.0/24",
"159.60.163.0/24",
"159.60.188.0/24",
"159.60.182.0/24",
"159.60.178.0/24",
# Asia
"103.135.56.0/24",
"103.135.57.0/25",
"103.135.59.0/25",
"103.135.58.0/24",
"159.60.189.0/24",
"159.60.166.0/24",
"159.60.164.0/24",
"159.60.170.0/24",
"159.60.172.0/24",
"159.60.191.0/24",
"159.60.184.0/24",
"159.60.185.0/24",
"159.60.186.0/24",
])
}
resource "terraform_data" "require_csd" {
lifecycle {
precondition {
condition = data.xcsh_addon_service_activation_status.csd.state == "AS_SUBSCRIBED"
error_message = "Client-Side Defense Standard must be subscribed before this stack can be planned or applied."
}
}
}
resource "aws_vpc" "csd" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = { Name = "${local.name}-vpc" }
lifecycle {
precondition {
condition = data.aws_caller_identity.current.account_id == var.expected_aws_account_id
error_message = "Refusing to operate outside the approved AWS account."
}
}
}
resource "aws_default_security_group" "csd" {
vpc_id = aws_vpc.csd.id
tags = { Name = "${local.name}-default-deny-all" }
}
resource "aws_internet_gateway" "csd" {
vpc_id = aws_vpc.csd.id
tags = { Name = "${local.name}-igw" }
}
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.csd.id
availability_zone = local.availability_zones[count.index]
cidr_block = var.public_subnet_cidrs[count.index]
map_public_ip_on_launch = false
tags = { Name = "${local.name}-public-${count.index + 1}" }
}
resource "aws_subnet" "private" {
count = 2
vpc_id = aws_vpc.csd.id
availability_zone = local.availability_zones[count.index]
cidr_block = var.private_subnet_cidrs[count.index]
tags = { Name = "${local.name}-private-${count.index + 1}" }
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.csd.id
tags = { Name = "${local.name}-public" }
}
resource "aws_route" "public_internet" {
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.csd.id
}
resource "aws_route_table_association" "public" {
count = 2
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# One NAT Gateway limits demo cost. It is intentionally a single-AZ egress dependency;
# production resilience would require one NAT Gateway and route table per AZ.
resource "aws_eip" "nat" {
domain = "vpc"
tags = { Name = "${local.name}-nat" }
depends_on = [aws_internet_gateway.csd]
}
resource "aws_nat_gateway" "csd" {
count = 1
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public[0].id
tags = { Name = "${local.name}-nat" }
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.csd.id
tags = { Name = "${local.name}-private" }
}
resource "aws_route" "private_egress" {
route_table_id = aws_route_table.private.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.csd[0].id
}
resource "aws_route_table_association" "private" {
count = 2
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private.id
}
resource "aws_kms_key" "logs" {
description = "Encrypt CSD Juice Shop CloudWatch logs"
deletion_window_in_days = 30
enable_key_rotation = true
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AccountAdministration"
Effect = "Allow"
Principal = { AWS = "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:root" }
Action = "kms:*"
Resource = "*"
},
{
Sid = "CloudWatchLogsUse"
Effect = "Allow"
Principal = { Service = "logs.${var.aws_region}.amazonaws.com" }
Action = [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey*",
"kms:ReEncrypt*",
"kms:DescribeKey"
]
Resource = "*"
Condition = {
ArnLike = {
"kms:EncryptionContext:aws:logs:arn" = [
"arn:${data.aws_partition.current.partition}:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/ecs/${local.name}*",
"arn:${data.aws_partition.current.partition}:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/vpc/${local.name}*",
]
}
}
}
]
})
tags = { Name = "${local.name}-logs" }
}
resource "aws_kms_alias" "logs" {
name = "alias/${local.name}-logs"
target_key_id = aws_kms_key.logs.key_id
}
resource "aws_cloudwatch_log_group" "vpc_flow" {
name = "/aws/vpc/${local.name}"
retention_in_days = 365
kms_key_id = aws_kms_key.logs.arn
tags = { Name = "${local.name}-vpc-flow" }
}
resource "aws_iam_role" "vpc_flow" {
name = "${local.name}-vpc-flow"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "vpc-flow-logs.amazonaws.com" }
Action = "sts:AssumeRole"
Condition = {
StringEquals = { "aws:SourceAccount" = data.aws_caller_identity.current.account_id }
ArnLike = {
"aws:SourceArn" = "arn:${data.aws_partition.current.partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:vpc-flow-log/*"
}
}
}]
})
}
resource "aws_iam_role_policy" "vpc_flow" {
name = "${local.name}-vpc-flow-delivery"
role = aws_iam_role.vpc_flow.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams",
]
Resource = "${aws_cloudwatch_log_group.vpc_flow.arn}:*"
},
{
Effect = "Allow"
Action = "logs:DescribeLogGroups"
Resource = "*"
},
]
})
}
resource "aws_flow_log" "csd" {
vpc_id = aws_vpc.csd.id
traffic_type = "ALL"
log_destination_type = "cloud-watch-logs"
log_destination = aws_cloudwatch_log_group.vpc_flow.arn
iam_role_arn = aws_iam_role.vpc_flow.arn
tags = { Name = "${local.name}-vpc-flow" }
}
resource "aws_s3_bucket" "alb_logs" {
# checkov:skip=CKV2_AWS_62:Dedicated ALB access-log sink has no event consumer; notifications would add an unused delivery path.
# checkov:skip=CKV_AWS_18:Server access logging to this same dedicated log sink would recurse; no separate durable audit bucket is part of this ephemeral demo stack.
# checkov:skip=CKV_AWS_144:Cross-region replication conflicts with same-state ephemeral teardown and is not required for this disposable ALB log sink.
# checkov:skip=CKV_AWS_145:ALB access-log delivery supports only Amazon S3-managed encryption keys (SSE-S3), not SSE-KMS.
bucket_prefix = local.alb_logs_bucket_prefix
force_destroy = true
}
resource "aws_s3_bucket_versioning" "alb_logs" {
bucket = aws_s3_bucket.alb_logs.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_public_access_block" "alb_logs" {
bucket = aws_s3_bucket.alb_logs.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "alb_logs" {
bucket = aws_s3_bucket.alb_logs.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_lifecycle_configuration" "alb_logs" {
bucket = aws_s3_bucket.alb_logs.id
rule {
id = "expire-alb-logs"
status = "Enabled"
filter {}
expiration {
days = 90
}
noncurrent_version_expiration {
noncurrent_days = 90
}
abort_incomplete_multipart_upload {
days_after_initiation = 7
}
}
}
resource "aws_s3_bucket_policy" "alb_logs" {
bucket = aws_s3_bucket.alb_logs.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowELBLogDelivery"
Effect = "Allow"
Principal = { Service = "logdelivery.elasticloadbalancing.amazonaws.com" }
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.alb_logs.arn}/${local.alb_access_logs_prefix}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"
Condition = {
StringEquals = {
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
}
}
},
{
Sid = "AllowELBGetBucketAcl"
Effect = "Allow"
Principal = { Service = "logdelivery.elasticloadbalancing.amazonaws.com" }
Action = "s3:GetBucketAcl"
Resource = aws_s3_bucket.alb_logs.arn
Condition = {
StringEquals = {
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
}
}
}
]
})
}
# Vendored from f5-sales-demo/origin-server commit d6384bb0621c4c1eceb38d55a6b63e7b9cc7083a.
module "origin" {
source = "./vendor/aws-juice-shop"
name = local.name
vpc_id = aws_vpc.csd.id
alb_subnet_ids = aws_subnet.public[*].id
task_subnet_ids = aws_subnet.private[*].id
public_exposure = true
enable_page_tamper_endpoint = true
allowed_ingress_cidrs = local.regional_edge_origin_cidrs
cloudwatch_logs_kms_key_arn = aws_kms_key.logs.arn
alb_access_logs_bucket = aws_s3_bucket.alb_logs.id
alb_access_logs_prefix = local.alb_access_logs_prefix
tags = var.tags
depends_on = [
aws_route.public_internet,
aws_route.private_egress,
aws_route_table_association.public,
aws_route_table_association.private,
aws_s3_bucket_policy.alb_logs,
aws_s3_bucket_server_side_encryption_configuration.alb_logs,
]
}
resource "xcsh_namespace" "csd" {
name = var.namespace
}
resource "xcsh_protected_domain" "csd" {
name = "client-side-defense"
namespace = var.namespace
protected_domain = "f5-sales-demo.com"
depends_on = [xcsh_namespace.csd, terraform_data.require_csd]
}
resource "xcsh_origin_pool" "origin" {
name = "csd-juice-shop"
namespace = var.namespace
port = 80
origin_servers {
public_name {
dns_name = module.origin.origin_hostname
}
}
no_tls = {}
same_as_endpoint_port = {}
depends_on = [xcsh_namespace.csd, terraform_data.require_csd]
}
resource "xcsh_http_loadbalancer" "csd" {
name = "client-side-defense"
namespace = var.namespace
domains = [var.domain]
https_auto_cert {
http_redirect = true
}
advertise_on_public_default_vip = {}
default_route_pools {
pool {
name = xcsh_origin_pool.origin.name
namespace = var.namespace
}
weight = 1
priority = 1
}
client_side_defense {
policy {
js_insert_all_pages = {}
}
}
depends_on = [xcsh_namespace.csd, xcsh_protected_domain.csd, terraform_data.require_csd]
}

outputs.tf is the supported interface for runtime identifiers and validation inputs. Use terraform output -raw application_url for protected-path checks and retrieve other values with terraform output; do not persist generated IDs, ARNs, VIPs, bucket suffixes, certificate dates, task IDs, or plan digests in documentation.

output "aws_account_id" {
description = "AWS account containing the CSD origin stack."
value = data.aws_caller_identity.current.account_id
}
output "aws_region" {
description = "AWS region containing the CSD origin stack."
value = var.aws_region
}
output "vpc_id" {
description = "Dedicated application VPC identifier."
value = aws_vpc.csd.id
}
output "origin_hostname" {
description = "Public ALB hostname consumed by the F5 Distributed Cloud origin pool."
value = module.origin.origin_hostname
}
output "origin_url" {
description = "Direct HTTP origin URL for restricted operational diagnostics."
value = module.origin.origin_url
}
output "application_url" {
description = "F5 Distributed Cloud protected application URL."
value = "https://${var.domain}"
}
output "page_tamper_path" {
description = "Dedicated CSD Page Tamper payment path."
value = module.origin.page_tamper_path
}
output "page_tamper_url" {
description = "Direct HTTP origin URL for the dedicated CSD Page Tamper payment endpoint."
value = module.origin.page_tamper_url
}
output "page_tamper_target_group_arn" {
description = "Dedicated CSD Page Tamper target group ARN used by readiness checks."
value = module.origin.page_tamper_target_group_arn
}
output "xc_namespace" {
description = "F5 Distributed Cloud namespace containing application resources."
value = var.namespace
}
output "xc_protected_domain_name" {
description = "Managed F5 Distributed Cloud protected-domain resource name."
value = xcsh_protected_domain.csd.name
}
output "xc_origin_pool_name" {
description = "Managed F5 Distributed Cloud origin-pool resource name."
value = xcsh_origin_pool.origin.name
}
output "xc_http_loadbalancer_name" {
description = "Managed F5 Distributed Cloud HTTP load balancer resource name."
value = xcsh_http_loadbalancer.csd.name
}
output "alb_access_logs_bucket" {
description = "S3 bucket receiving Application Load Balancer access logs."
value = aws_s3_bucket.alb_logs.id
}
output "alb_arn" {
description = "Application Load Balancer ARN."
value = module.origin.load_balancer_arn
}
output "target_group_arn" {
description = "Application target group ARN."
value = module.origin.target_group_arn
}
output "listener_port" {
description = "Public origin HTTP listener port."
value = module.origin.listener_port
}
output "health_check_port" {
description = "Application target health-check port."
value = 3000
}
output "public_subnet_ids" {
description = "Selected public ALB subnet IDs."
value = module.origin.alb_subnet_ids
}
output "private_subnet_ids" {
description = "Selected private Fargate subnet IDs."
value = module.origin.task_subnet_ids
}
output "cloudwatch_logs_kms_key_arn" {
description = "KMS key ARN used to encrypt application CloudWatch logs."
value = aws_kms_key.logs.arn
}
output "alb_access_logs_prefix" {
description = "S3 object prefix used for ALB access logs."
value = local.alb_access_logs_prefix
}

tests/stack.tftest.hcl uses mocked providers to verify configuration contracts: identity restrictions, topology, logging controls, namespace and entitlement ordering, origin-pool wiring, HTTPS redirect, public advertisement, all-pages CSD injection, and negative cases for the wrong AWS account or missing entitlement.

Mocked assertions do not validate deployed resources, runtime health, traffic flow, log delivery, or CSD telemetry. After apply, use application_url and run the executable Diagnostics & Verification procedures.

mock_provider "aws" {
mock_resource "aws_cloudwatch_log_group" {
defaults = {
arn = "arn:aws:logs:us-east-1:280469140135:log-group:/aws/vpc/csd-juice-shop"
}
}
mock_resource "aws_iam_role" {
defaults = {
arn = "arn:aws:iam::280469140135:role/csd-juice-shop-vpc-flow"
}
}
}
mock_provider "xcsh" {}
override_data {
target = data.aws_caller_identity.current
values = {
account_id = "280469140135"
}
}
override_data {
target = data.xcsh_addon_service_activation_status.csd
values = {
state = "AS_SUBSCRIBED"
}
}
override_module {
target = module.origin
outputs = {
origin_hostname = "origin.example.com"
origin_url = "http://origin.example.com"
listener_port = 80
load_balancer_arn = "arn:aws:elasticloadbalancing:us-east-1:280469140135:loadbalancer/app/csd/0000000000000000"
target_group_arn = "arn:aws:elasticloadbalancing:us-east-1:280469140135:targetgroup/csd/0000000000000000"
alb_subnet_ids = ["subnet-public-a", "subnet-public-b"]
task_subnet_ids = ["subnet-private-a", "subnet-private-b"]
page_tamper_path = "/csd-page-tamper/payment"
page_tamper_url = "http://origin.example.com/csd-page-tamper/payment"
page_tamper_target_group_arn = "arn:aws:elasticloadbalancing:us-east-1:280469140135:targetgroup/csd-page-tamper/0000000000000000"
}
}
run "stack_contract" {
command = apply
assert {
condition = length(regexall("allowed_account_ids\\s*=\\s*\\[\\s*var\\.expected_aws_account_id\\s*\\]", file("${path.module}/versions.tf"))) == 1
error_message = "The AWS provider must enforce the approved account from expected_aws_account_id at provider initialization."
}
assert {
condition = length(regexall("(?s)module\\s+\"origin\"\\s*\\{.*?depends_on\\s*=\\s*\\[\\s*aws_route\\.public_internet,\\s*aws_route\\.private_egress,\\s*aws_route_table_association\\.public,\\s*aws_route_table_association\\.private,\\s*aws_s3_bucket_policy\\.alb_logs,\\s*aws_s3_bucket_server_side_encryption_configuration\\.alb_logs,\\s*\\]", file("${path.module}/main.tf"))) == 1
error_message = "The origin module must wait for both default routes, both route-table association sets, and the existing log-bucket dependencies."
}
assert {
condition = length(regexall("(?s)module\\s+\"origin\"\\s*\\{.*?enable_page_tamper_endpoint\\s*=\\s*true.*?depends_on", file("${path.module}/main.tf"))) == 1
error_message = "The CSD stack must permanently enable the inert-by-default Page Tamper endpoint."
}
assert {
condition = output.page_tamper_path == "/csd-page-tamper/payment" && output.page_tamper_url == "http://origin.example.com/csd-page-tamper/payment"
error_message = "The CSD stack must re-export the dedicated Page Tamper path and direct origin URL."
}
assert {
condition = output.page_tamper_target_group_arn == module.origin.page_tamper_target_group_arn
error_message = "The stack must re-export the dedicated Page Tamper target-group ARN for readiness checks."
}
assert {
condition = aws_vpc.csd.cidr_block == "10.43.0.0/16"
error_message = "The CSD stack must use the reviewed non-overlapping VPC CIDR."
}
assert {
condition = toset(var.public_subnet_cidrs) == toset(["10.43.0.0/24", "10.43.1.0/24"]) && toset(var.private_subnet_cidrs) == toset(["10.43.10.0/24", "10.43.11.0/24"])
error_message = "The CSD subnets must use the reviewed non-overlapping two-AZ layout."
}
assert {
condition = local.regional_edge_origin_cidrs == toset([
# Americas
"5.182.215.0/25",
"84.54.61.0/25",
"23.158.32.0/25",
"84.54.62.0/25",
"185.94.143.0/25",
"185.94.142.0/24",
"159.60.190.0/24",
"159.60.168.0/24",
"159.60.180.0/24",
"159.60.174.0/24",
"159.60.175.0/24",
"159.60.176.0/24",
"159.60.177.0/24",
"159.60.179.0/24",
"159.60.181.0/24",
"159.60.183.0/24",
# Europe
"5.182.213.0/24",
"5.182.212.0/25",
"5.182.214.0/25",
"84.54.60.0/25",
"185.56.154.0/25",
"159.60.160.0/24",
"159.60.161.0/24",
"159.60.162.0/24",
"159.60.163.0/24",
"159.60.188.0/24",
"159.60.182.0/24",
"159.60.178.0/24",
# Asia
"103.135.56.0/24",
"103.135.57.0/25",
"103.135.59.0/25",
"103.135.58.0/24",
"159.60.189.0/24",
"159.60.166.0/24",
"159.60.164.0/24",
"159.60.170.0/24",
"159.60.172.0/24",
"159.60.191.0/24",
"159.60.184.0/24",
"159.60.185.0/24",
"159.60.186.0/24",
]) && length(local.regional_edge_origin_cidrs) == 41 && !contains(local.regional_edge_origin_cidrs, "0.0.0.0/0")
error_message = "The public origin must allow exactly the 41 documented global Regional Edge TCP IPv4 CIDRs."
}
assert {
condition = aws_s3_bucket.alb_logs.bucket_prefix == "f5-sales-demo-csd-alb-logs-" && aws_s3_bucket.alb_logs.force_destroy
error_message = "The ALB log bucket must use a generated name and allow complete demo teardown."
}
assert {
condition = aws_s3_bucket_versioning.alb_logs.versioning_configuration[0].status == "Enabled"
error_message = "The ALB access-log bucket must retain recoverable object versions."
}
assert {
condition = anytrue([
for statement in jsondecode(aws_iam_role_policy.vpc_flow.policy).Statement :
statement.Effect == "Allow" && try(toset(statement.Action), toset([statement.Action])) == toset(["logs:DescribeLogGroups"]) && statement.Resource == "*"
])
error_message = "The VPC flow-log role must allow DescribeLogGroups in a separate wildcard-scoped statement."
}
assert {
condition = anytrue([
for statement in jsondecode(aws_iam_role_policy.vpc_flow.policy).Statement :
statement.Effect == "Allow" && contains(try(toset(statement.Action), toset([statement.Action])), "logs:PutLogEvents") && statement.Resource == "${aws_cloudwatch_log_group.vpc_flow.arn}:*" && !contains(try(toset(statement.Action), toset([statement.Action])), "logs:DescribeLogGroups")
])
error_message = "Restrictable VPC flow-log write actions must remain scoped to the log-group ARN and separate from DescribeLogGroups."
}
assert {
condition = one(aws_s3_bucket_lifecycle_configuration.alb_logs.rule).expiration[0].days == 90 && one(aws_s3_bucket_lifecycle_configuration.alb_logs.rule).noncurrent_version_expiration[0].noncurrent_days == 90 && one(aws_s3_bucket_lifecycle_configuration.alb_logs.rule).abort_incomplete_multipart_upload[0].days_after_initiation == 7
error_message = "The ALB log lifecycle must expire current and noncurrent versions after 90 days while preserving multipart cleanup."
}
assert {
condition = aws_flow_log.csd.traffic_type == "ALL" && aws_flow_log.csd.log_destination_type == "cloud-watch-logs"
error_message = "The dedicated VPC must publish all flow records to CloudWatch Logs."
}
assert {
condition = aws_cloudwatch_log_group.vpc_flow.name == "/aws/vpc/csd-juice-shop" && aws_cloudwatch_log_group.vpc_flow.retention_in_days == 365
error_message = "VPC flow logging must use the dedicated one-year-retention CloudWatch log group."
}
assert {
condition = strcontains(aws_kms_key.logs.policy, "log-group:/aws/vpc/csd-juice-shop*")
error_message = "The CloudWatch Logs KMS policy must authorize the VPC flow-log namespace."
}
assert {
condition = length(aws_default_security_group.csd.ingress) == 0 && length(aws_default_security_group.csd.egress) == 0
error_message = "The VPC default security group must be explicitly managed with no ingress or egress rules."
}
assert {
condition = output.alb_arn == module.origin.load_balancer_arn && output.target_group_arn == module.origin.target_group_arn
error_message = "The stack must re-export the ALB and target group ARNs."
}
assert {
condition = output.listener_port == 80 && output.health_check_port == 3000
error_message = "The stack must re-export the origin listener and target health-check ports."
}
assert {
condition = output.public_subnet_ids == module.origin.alb_subnet_ids && output.private_subnet_ids == module.origin.task_subnet_ids
error_message = "The stack must re-export the selected public and private subnet IDs."
}
assert {
condition = output.alb_access_logs_prefix == "juice-shop"
error_message = "The stack must re-export the ALB access-log prefix."
}
assert {
condition = length(aws_subnet.public) == 2 && length(aws_subnet.private) == 2
error_message = "The dedicated VPC must have two public and two private subnets."
}
assert {
condition = length(aws_nat_gateway.csd) == 1
error_message = "The stack must use the documented single NAT Gateway topology."
}
assert {
condition = xcsh_namespace.csd.name == var.namespace
error_message = "The stack must manage the F5 Distributed Cloud namespace used by all application resources."
}
assert {
condition = length(regexall("(?s)resource\\s+\"xcsh_protected_domain\"\\s+\"csd\"\\s*\\{.*?depends_on\\s*=\\s*\\[\\s*xcsh_namespace\\.csd,\\s*terraform_data\\.require_csd\\s*\\]", file("${path.module}/main.tf"))) == 1
error_message = "The protected domain must wait for namespace creation and the CSD entitlement gate."
}
assert {
condition = length(regexall("(?s)resource\\s+\"xcsh_origin_pool\"\\s+\"origin\"\\s*\\{.*?depends_on\\s*=\\s*\\[\\s*xcsh_namespace\\.csd,\\s*terraform_data\\.require_csd\\s*\\]", file("${path.module}/main.tf"))) == 1
error_message = "The origin pool must wait for namespace creation and the CSD entitlement gate."
}
assert {
condition = length(regexall("(?s)resource\\s+\"xcsh_http_loadbalancer\"\\s+\"csd\"\\s*\\{.*?depends_on\\s*=\\s*\\[\\s*xcsh_namespace\\.csd,\\s*xcsh_protected_domain\\.csd,\\s*terraform_data\\.require_csd\\s*\\]", file("${path.module}/main.tf"))) == 1
error_message = "The HTTP load balancer must wait for the namespace, protected domain, and CSD entitlement gate; its pool reference remains an implicit dependency."
}
assert {
condition = xcsh_protected_domain.csd.namespace == "client-side-defense" && xcsh_protected_domain.csd.protected_domain == "f5-sales-demo.com"
error_message = "The protected-domain prerequisite must use the guarded namespace and registrable parent domain."
}
assert {
condition = xcsh_origin_pool.origin.port == 80 && one(xcsh_origin_pool.origin.origin_servers).public_name.dns_name == module.origin.origin_hostname
error_message = "The origin pool must target the module ALB hostname over HTTP port 80."
}
assert {
condition = toset(xcsh_http_loadbalancer.csd.domains) == toset(["client-side-defense.f5-sales-demo.com"])
error_message = "The load balancer must use the guarded CSD domain."
}
assert {
condition = xcsh_http_loadbalancer.csd.https_auto_cert.http_redirect == true
error_message = "The load balancer must use auto-cert with HTTP redirect."
}
assert {
condition = xcsh_http_loadbalancer.csd.advertise_on_public_default_vip != null
error_message = "The load balancer must advertise on the public default VIP."
}
assert {
condition = xcsh_http_loadbalancer.csd.client_side_defense.policy.js_insert_all_pages != null
error_message = "Client-Side Defense JavaScript injection must cover all pages."
}
assert {
condition = one(xcsh_http_loadbalancer.csd.default_route_pools).pool.name == xcsh_origin_pool.origin.name
error_message = "The default route must reference the managed origin pool."
}
}
run "reject_wrong_aws_account" {
command = plan
override_data {
target = data.aws_caller_identity.current
values = {
account_id = "111111111111"
}
}
expect_failures = [aws_vpc.csd]
}
run "reject_missing_csd_entitlement" {
command = plan
override_data {
target = data.xcsh_addon_service_activation_status.csd
values = {
state = "AS_NOT_SUBSCRIBED"
}
}
expect_failures = [terraform_data.require_csd]
}

The vendored module itself remains canonical under terraform/aws/vendor/aws-juice-shop; it is intentionally not duplicated on this page. Its pinned provenance is documented in the architecture overview.