An interactive shell script for deploying and managing a Proxmox LXC container-based security testing lab. Automates the creation of a realistic multi-site enterprise network with simulated traffic patterns for testing security solutions like Zscaler, CASB, DLP, and UEBA systems.
AI Guard prompt tests — a new ai-guard-test.ps1, generated from a new ai_guard_prompt section in lab-traffic.tsv, sends prompts engineered to trip each AI Guard detector category (brand/reputation risk, code, finance advice, intellectual property, legal advice, PII, prompt injection, secrets, toxicity) to ChatGPT, Gemini, and Perplexity. It's prompt-only (no URLs, no file/image uploads) and installed on-demand via the new Install AI Guard Prompt Test Script option in Windows Tools for later manual, interactive runs — see Windows Tools.
Gemini prompt capture via headless Chromium — gemini-prompt.py navigates gemini.google.com in a real browser session that ZIA can inspect. Gemini is now enabled as a genai_provider for devops, sales, developer, and executive profiles.
Expanded GenAI prompt banks — each GenAI-capable profile (devops, developer, sales, executive) now ships 12 role-appropriate prompts, up from 3.
Randomized PII across all DLP tests — fake name, date of birth, and home address are randomized on every run across dlp-genai-prompt.sh, dlp-genai-file.sh, dlp-genai-image.sh, and the Windows threat scripts. The DLP document and image canvas were expanded to include the new fields.
RAM pre-flight check — deployment now checks available RAM per node before cloning, matching the existing disk pre-flight: a per-node needed/available/after table is shown, a warning fires at 85% utilization, and the deploy aborts if RAM is insufficient.
Timezone support for Windows scheduled tasks — setup-scheduled-tasks.ps1 now accepts a -TimeZone parameter (IANA or Windows format) so tasks fire at the correct local time on VMs in non-UTC zones.
Container memory bumps — office-worker raised to 512 MB to accommodate headless Chromium; genai-enabled containers (devops, developer, sales, executive) raised to 768 MB for Cloudflare JS overhead.
Important: This lab was developed with a specific network topology in mind:
- Router Configuration: The lab router has existing IPsec/GRE tunnels to two separate Zscaler data centers
- Location Simulation: Data Center and Branch subnets are treated as separate physical locations in Zscaler
- DNS Resolution: DNS for both subnets is handled by Zscaler Trusted Resolver, specified to containers via DHCP
- Traffic Flow: All container traffic egresses through the Zscaler cloud via the appropriate tunnel
This architecture allows realistic simulation of a multi-site enterprise environment with cloud security controls.
Provides a complete workflow for building a multi-container lab environment that generates realistic web traffic patterns mimicking real-world enterprise scenarios.
- Creates Alpine LXC templates with pre-configured utilities
- Deploys Data Center and Branch network containers with appropriate configurations — an interactive checkbox menu lets you select which profiles to deploy and how many of each; containers distribute across multiple Proxmox cluster nodes with auto-balanced or manual placement; supports linked clones (shared base disk, faster deploy) or full clones depending on storage type and topology
- Generates realistic traffic patterns driven by
lab-traffic.tsv— a tab-separated data file that defines URLs, GenAI providers, prompts, and security test assignments per profile. Profile scripts are generated dynamically at install time from this file. - Simulates GenAI usage by submitting role-appropriate prompts to ChatGPT (via API endpoint) and Gemini (via headless Chromium browser session) — generating prompt capture events visible in Zscaler ZIA logs
- Configures security tests independently of normal traffic — DLP (network, GenAI prompt/file/image OCR), AV/malware, policy violations, UEBA anomalies
- Manages Windows VMs via a dedicated submenu: bulk-tags VMs for discovery, installs TLS inspection certificates (skip-if-current), pushes and version-checks traffic scripts, and configures scheduled tasks with profile selection
A single interactive menu covering the full lab lifecycle.
Menu options:
- Create Template — create an Alpine LXC template
- Deploy Containers — clone and configure lab containers
- Start Containers — start stopped lab-managed containers
- Install Traffic Generator — push traffic profiles to containers; includes an in-script profile viewer and security test toggle (enter
vat the confirmation prompt) - Full Setup Wizard — runs steps 1 → 2 → 3 → 4 in sequence
- Windows Tools — submenu for Windows VM management (see below)
- Show Status — view all containers with running state and traffic gen status at a glance
- Update Container Packages — run
apk update && apk upgradeon all running lab containers in parallel - Update Lab Script — check GitHub for a newer version, show changelog, prompt to confirm, then self-patch the script in place and exit. Also downloads updated
setup-scheduled-tasks.ps1andlab-traffic.tsvalongside the main script (win-traffic.ps1is generated from the TSV at push time and is not downloaded). On every interactive launch, the same check runs automatically before the menu appears. - Stop Containers — stop all running lab-managed containers
- Exit
Interactive menu:
./proxmox-lab.shDirect command invocation (useful for scripting or re-running a single step):
./proxmox-lab.sh create-template
./proxmox-lab.sh deploy
./proxmox-lab.sh start
./proxmox-lab.sh stop
./proxmox-lab.sh install-traffic
./proxmox-lab.sh status
./proxmox-lab.sh wizard
./proxmox-lab.sh update-containers
./proxmox-lab.sh update
./proxmox-lab.sh windowsConfig persistence:
On first run, the script prompts for all values as usual. After completing any command, it offers to save the answers to ~/.proxmox-lab.conf. On every subsequent run those values pre-populate all prompts — press Enter to accept, or type a new value to override. The config file survives script updates. The Full Setup Wizard auto-saves once at the end without prompting at each step.
Key saved values include node selection, network bridge, CT disk storage pool (STORAGE), image storage pool (IMAGE_STORAGE), VLAN IDs, CTID ranges, template ID, clone type (CLONE_TYPE), TLS certificate path, cron schedules, and lab-traffic.tsv path.
- Proxmox VE 8.x or higher
- Root or sudo access
- Network bridge configured (default: vmbr0)
- Local storage available (local-lvm or local-zfs); shared NFS/Ceph storage supported for multi-node clusters
- TLS inspection root CA certificate on the Proxmox host, if your network performs HTTPS inspection (see below)
- Multi-node cluster: node IPs resolvable via
/etc/hostsor present in/etc/pve/corosync.conf(DNS not required) - Windows VM certificate install: QEMU guest agent installed and running inside the Windows VM
Run all four setup steps in sequence:
./proxmox-lab.sh wizardThe wizard walks through all four steps in sequence, prompting for input at each stage, then saves your settings automatically.
Each step can also be run individually:
./proxmox-lab.sh create-template # Step 1 — create Alpine template
./proxmox-lab.sh deploy # Step 2 — select workloads and clone containers
./proxmox-lab.sh start # Step 3 — start containers
./proxmox-lab.sh install-traffic # Step 4 — install traffic profilesDuring create-template (step 3b), the script prompts for the storage pool where the Alpine .tar.xz template image will be downloaded. Only pools with the vztmpl content type are listed. This is separate from the CT disk storage pool (step 3) — on most Proxmox hosts local is the correct choice for image storage. The selection is saved to ~/.proxmox-lab.conf as IMAGE_STORAGE.
If your network performs TLS inspection (e.g., Zscaler), copy your root CA certificate to the Proxmox host before running create-template:
scp /path/to/ZscalerRootCertificate.crt root@<proxmox-host>:/root/During create-template (step 7), the script prompts for the certificate path on the host. If provided, the certificate is installed into the template via Alpine's update-ca-certificates, and every cloned container inherits it automatically. The path is saved to ~/.proxmox-lab.conf for subsequent runs.
Zscaler offers a choice of using their built-in root certificate or uploading a custom one — either works. If you are not using TLS inspection, press Enter to skip.
To verify the certificate is trusted in a container after deployment:
pct exec 200 -- curl -sv https://www.google.com 2>&1 | grep -E "SSL|issuer|subject"All Windows VM management is in the Windows Tools submenu (option 6) or via ./proxmox-lab.sh windows. The submenu has four steps meant to be run in order on first setup, plus an on-demand AI Guard prompt test installer:
Step 1 — Tag Windows VMs
Tags one or more VMs with lab-windows so they appear in all subsequent Windows Tools operations. The script shows all VMs across the cluster, marks VMs with 'win' in the name as candidates, and lets you select by number or all.
Step 2 — Install TLS Certificate
Installs the TLS inspection root CA (same CERT_PATH as for LXC containers) to the Windows Trusted Root store on all selected VMs. Skips VMs where the certificate thumbprint is already present — safe to re-run.
Requires the QEMU guest agent to be installed and running inside each Windows VM.
To verify in Windows: certmgr.msc → Trusted Root Certification Authorities → Certificates.
Step 3 — Install / Update Traffic Generator Script
Generates win-traffic.ps1 dynamically from lab-traffic.tsv and pushes it to C:\ProgramData\proxmox-lab\ on each selected VM. The $SCRIPT_VERSION field in the generated file encodes both the script version and a short TSV content hash — skips VMs that are already up to date, and re-pushes automatically when the TSV changes even without a script version bump.
setup-scheduled-tasks.ps1 is downloaded automatically from GitHub the first time you enter the Windows Tools submenu if not already present alongside proxmox-lab.sh, and updated whenever you run Update Lab Script (option 9).
Step 4 — Configure Scheduled Tasks
Pushes setup-scheduled-tasks.ps1 and runs it on each selected VM. Prompts for which profiles to install (office-worker, sales, developer, executive, threat — default: all) and the VM's local timezone (IANA or Windows format, e.g. America/New_York or Eastern Standard Time). Existing lab tasks are always removed and recreated, so re-running with a different profile set removes orphaned tasks cleanly.
Install AI Guard Prompt Test Script (on-demand)
Generates ai-guard-test.ps1 dynamically from the ai_guard_prompt rows in lab-traffic.tsv and copies it, along with a copy of lab-traffic.tsv itself, to C:\ProgramData\proxmox-lab\ on each selected VM. This step only installs the files — it does not run the script. The prompts are engineered to individually trip AI Guard detector categories (brand/reputation risk, code, finance advice, intellectual property, legal advice, PII, prompt injection, secrets, toxicity) and are prompt-only, with no URLs, file uploads, or OCR involved.
Because reliable detection requires real user context (e.g. Zscaler Client Connector identity) that this lab can currently only provide on a live Windows session, the script is meant to be run manually and interactively on the VM rather than via scheduled task or unattended qm guest exec.
Open PowerShell on the VM (no admin rights required) and launch it with -ExecutionPolicy Bypass, since most machines default to a policy (Restricted or AllSigned, often enforced by GPO) that blocks unsigned local scripts from running at all:
powershell.exe -ExecutionPolicy Bypass -File 'C:\ProgramData\proxmox-lab\ai-guard-test.ps1' -Category all -DelaySeconds 15-ExecutionPolicy Bypass here only applies to this one process — it doesn't change the machine's persistent policy. If you're already in a PowerShell prompt and prefer dot-sourcing instead of spawning a subprocess, set the bypass for the current process first:
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
& 'C:\ProgramData\proxmox-lab\ai-guard-test.ps1' -Category all -DelaySeconds 15-Category accepts any single detector category present in the TSV, or all (default). -DelaySeconds controls the pause between prompt requests (default: 15).
View traffic generation logs:
pct exec 200 -- tail -f /var/log/messages
pct exec 220 -- tail -f /var/log/messagesCheck cron schedules (traffic + security tests):
pct exec 200 -- crontab -lManual traffic generation:
pct exec 200 -- /opt/traffic-gen/traffic-gen.sh fileserverRun all security tests manually:
pct exec 200 -- /opt/traffic-gen/run-security-tests.shRun a single security test manually:
pct exec 224 -- bash /opt/traffic-gen/security-tests/ueba.sh
pct exec 200 -- bash /opt/traffic-gen/security-tests/dlp-network.shCTID ranges and VLAN IDs have no built-in defaults — they are entered at deploy time and saved to ~/.proxmox-lab.conf for subsequent runs.
| Network | Available profiles | Default qty |
|---|---|---|
| Data Center | fileserver, webapp, email, monitoring, devops, database | 1 each (user-selectable) |
| BranchNet | office-worker, sales, developer, executive | 2 for office-worker, 1 for others (user-selectable) |
Profiles and quantities are chosen interactively at deploy time via the workload selection menu. Multiple environments of each type are supported — each gets its own CTID range and VLAN. Containers are named hq<env>-<profile><n> and branch<env>-<profile><n> (e.g., hq1-fileserver1, hq2-fileserver1, branch1-worker2).
| Container Type | Memory | CPU Cores |
|---|---|---|
| Standard (fileserver, webapp, email, database) | 256 MB | 1 |
| Heavy (monitoring) | 512 MB | 1 |
| Office (office-worker) | 512 MB | 1 |
| GenAI-enabled (devops, developer, sales, executive) | 768 MB | 1 |
| Cron | Default Schedule | Active Hours |
|---|---|---|
| Server profiles | Every 15 min | 24/7 |
| Office profiles | Every 5 min | M-F 8am-6pm |
| Security tests | Every 30 min | 24/7 (UEBA script self-limits to after-hours) |
| Profile | Domains/Services | User Agent | GenAI |
|---|---|---|---|
| fileserver | OneDrive, Dropbox, S3 | OneDrive Sync, Dropbox, aws-cli | — |
| webapp | Stripe API, CDN services, OCSP | Stripe-Node, WebServer, OpenSSL, aws-cli | — |
| Office 365, Gmail, SpamHaus, ClamAV | Exchange Server, Postfix, SpamAssassin, ClamAV | — | |
| monitoring | Ubuntu repos, Datadog, New Relic, Docker Hub, GitHub | Debian APT, Datadog Agent, NewRelic, Docker, GitHub Actions | — |
| devops | npm, PyPI, GitHub, Docker Hub | npm, pip, git, GitHub Actions, Docker | ChatGPT, Gemini |
| database | AWS RDS, Azure SQL, S3 | aws-sdk-java, Boto3, azsdk-python | — |
| office-worker | Microsoft 365, Slack, Google Docs, news, personal sites | Windows/Mac browser pool (Chrome, Edge, Firefox) | — |
| sales | LinkedIn, Salesforce, Zoom, HubSpot, Expedia | Mac browser pool (Safari, Chrome) | ChatGPT, Gemini |
| developer | GitHub, StackOverflow, npm, PyPI, AWS Console | Mac/Linux browser pool (Chrome, Firefox) | ChatGPT, Gemini |
| executive | WSJ, Bloomberg, FT, Reuters, Office 365, travel, Zoom | Mac Safari/Chrome pool | ChatGPT, Gemini |
Server profiles emit role-appropriate SDK and tool user agents matching the software that would realistically generate each request. User profiles pick from a persona-specific browser UA pool once per run and use it consistently throughout, so all requests within a session appear to come from the same device.
GenAI-capable profiles (devops, developer, sales, executive) generate two types of GenAI activity:
Browsing — periodic GET requests to GenAI platform homepages, simulating a user navigating to an AI tool.
Prompt submission — two mechanisms are used depending on the platform:
- ChatGPT — POST requests to the web app endpoint (
chatgpt.com/backend-api/f/conversation) with role-appropriate business prompts embedded in the request body. The server returns 401/403 (no valid session token). Zscaler inspects the outbound request body before the response arrives, so prompt capture events fire in ZIA logs regardless of the server response. - Gemini —
gemini-prompt.pydrives a headless Chromium session (via pyppeteer) that navigatesgemini.google.comand submits prompts through the real web UI. ZIA inspects the live browser session, producing prompt capture events identical to a real user visit.--disable-quicis passed to force TCP so the session is visible to ZIA.
Each GenAI-capable profile ships 12 role-appropriate prompts.
Microsoft Copilot is excluded — it uses WebSockets which are incompatible with standard TLS inspection. Claude is excluded due to session-authenticated endpoints that cannot produce realistic synthetic traffic without active sessions.
Security tests are installed separately from normal traffic profiles and run on their own cron schedule. Each test is an independent script — it is "enabled" when its script file is present on the container.
The default test assignments are defined in lab-traffic.tsv and can be toggled per profile before deployment using the built-in profile viewer (enter v at the "Proceed with installation?" prompt during install-traffic).
| Test | Category | What It Generates |
|---|---|---|
eicar |
AV | EICAR test file download |
dlp-network |
DLP | POST with fake SSN + CCN to HTTPS endpoint |
dlp-genai-prompt |
DLP | JSON prompt with PII submitted via browser session across ChatGPT, Gemini, and other platforms |
dlp-genai-file |
DLP | Multipart document upload with PII to AI file API |
dlp-genai-image |
DLP OCR | ImageMagick-rendered PNG with PII to AI vision API |
policy-violation |
Policy | HTTP access to Dropbox, WeTransfer, Mega, Box |
ueba |
UEBA | After-hours O365, Teams, SharePoint access |
How GenAI DLP tests work: The tests POST to real AI API endpoints (OpenAI, Anthropic, Google Gemini) with no valid API key. The server returns 401. Zscaler DLP inspects the outbound request body before the response arrives, so the DLP trigger fires on the request payload regardless. The traffic appears as a real user attempting to use a ChatGPT or Claude API.
Default test assignments by profile:
| Profile | Default security tests |
|---|---|
| fileserver | dlp-network |
| devops | eicar, dlp-genai-prompt, dlp-genai-file, dlp-genai-image |
| developer | eicar, dlp-genai-prompt, dlp-genai-file |
| office-worker | policy-violation, dlp-genai-prompt |
| sales | policy-violation, dlp-genai-prompt, dlp-genai-file |
| executive | ueba, dlp-genai-prompt |
| All others | none |
All traffic data — URLs, GenAI providers, GenAI prompts, security test assignments, and AI Guard prompts — lives in lab-traffic.tsv alongside proxmox-lab.sh on the Proxmox host. The file is auto-downloaded from GitHub on first install-traffic run if not present, and updated alongside the main script by Update Lab Script (option 9).
The file is tab-separated with four columns:
type profile value enabled
url office-worker salesforce.com yes
genai_provider devops chatgpt yes
genai_prompt devops What are the key differences... yes
security_test devops dlp-genai-image no
To customize traffic data, edit lab-traffic.tsv directly on the Proxmox host and re-run install-traffic. Changes take effect on the next install-traffic run — no container restart needed. The enabled column on every row lets you toggle individual URLs, prompts, or security tests on or off without deleting entries.
A separate ai-guard profile holds rows consumed only by ai-guard-test.ps1 (see Windows Tools), not by the container traffic generator:
type profile value enabled
genai_provider ai-guard chatgpt yes
ai_guard_prompt pii Please summarize this SSN: ... yes
ai_guard_prompt secrets Here is an AWS key, explain it: ... yes
ai_guard_prompt rows use the detector category (brand-reputation, code, finance-advice, intellectual-property, legal-advice, pii, prompt-injection, secrets, toxicity) in the profile column instead of a traffic profile name. Edit or add rows the same way as any other TSV row, then re-run Install AI Guard Prompt Test Script from Windows Tools to regenerate and push the updated ai-guard-test.ps1.
At the "Proceed with installation?" prompt during install-traffic, enter v to open the profile viewer:
- Browse any profile — see its URLs, GenAI providers, prompts, and security test state. When opened during
install-traffic, tests show as[INSTALL]/[skip]based on your mode selection rather than raw TSV defaults. - Toggle security tests — flip individual tests on or off; changes write back to
lab-traffic.tsvimmediately and affect future "recommended defaults" installs
This is the recommended way to review and adjust security test posture before deploying to a new environment.
./proxmox-lab.sh deploy
# Select Data Center only, enter 300-310 as CTID range./proxmox-lab.sh install-traffic
# Select "Custom" intensity, enter cron schedules./proxmox-lab.sh install-traffic
# Select "Custom selection"
# Example: 300:fileserver,301:webapp,320:developerTo disable a specific security test on a container without reinstalling everything:
pct exec <CTID> -- rm /opt/traffic-gen/security-tests/eicar.sh# Check container status
pct status <CTID>
# View container config
pct config <CTID>
# Check logs
pct exec <CTID> -- dmesg# Verify cron is running
pct exec <CTID> -- rc-status
# Check cron schedule
pct exec <CTID> -- crontab -l
# Test profile manually (use bash explicitly — container default shell is ash)
pct exec <CTID> -- bash /opt/traffic-gen/traffic-gen.sh <profile># Check network configuration
pct config <CTID> | grep net0
# Test connectivity from container
pct exec <CTID> -- ping -c 3 8.8.8.8
pct exec <CTID> -- curl -I https://google.comThe default shell inside Alpine containers is BusyBox ash. The traffic generator scripts use bash-specific syntax — always enter bash explicitly when sourcing or running scripts manually:
pct exec <CTID> -- bash- Volume is intentionally low (~1-2 requests/hour per domain)
- Will NOT trigger rate limits or blocklists on normal traffic
- Safe for production internet connections
- EICAR test file is industry-standard, non-malicious test payload
- ChatGPT prompt submission uses the real web app endpoint with no valid session token — server returns 401/403, but the outbound request body is inspected by Zscaler as intended. Gemini prompt submission uses a headless Chromium session navigating the live web UI, generating real ZIA prompt capture events
- GenAI DLP tests POST to real AI APIs with no valid key — server returns 401, but the outbound request is inspected by Zscaler as intended
dlp-genai-imageinstalls ImageMagick (~10 MB) on the container when enabled
- Template: ~50-100 MB
- Full clone per container: ~300-400 MB (independent copy of template disk)
- Linked clone per container: ~100-150 MB delta (base disk shared with template)
- Total for 11 containers + template — full clones: ~3.5-4.5 GB; linked clones: ~1.2-1.7 GB
- Storage pool must support
rootdircontent type; must be snapshot-capable (lvmthin,zfspool, Ceph RBD) to use linked clones
- Minimal bandwidth usage (<1 Mbps aggregate)
- No broadcast/multicast traffic
- VLAN isolation supported
Contributions welcome. See CONTRIBUTING.md for guidelines.
MIT License