Demo
This guide walks you through a complete Client-Side Defense exercise on F5 Distributed Cloud using the API — structured as four phases that an AI assistant or human operator can execute end-to-end. Each step includes a ready-to-run curl command with placeholder values you can customize using the form at the top of the page, a .env file, or any automation tool.
Exercise Phases
Section titled “Exercise Phases”| Phase | Goal | Steps |
|---|---|---|
| Phase 1 — Build | Deploy and validate the full CSD infrastructure | Steps 1–7 |
| Phase 2 — Attack | Generate simulated attack traffic and confirm CSD detected it | Steps 8–9 |
| Phase 3 — Mitigate | Before/after mitigation proof — run attack, apply mitigations, re-run attack, compare | Steps 1–6 |
| Phase 4 — Teardown | Remove all deployment objects after explicit confirmation | Teardown |
Pre-flight Check
Section titled “Pre-flight Check”Before starting Phase 1, verify the environment is clean. Run these API checks to determine if leftover objects exist from a prior run:
# Check all Phase 1 objects and compute environment statusHTTP_LB=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-http")HTTPS_LB=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-https")ORIGIN=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools/xF5XC_ORIGIN_POOLx")HC=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks/xF5XC_HC_NAMEx")PD_COUNT=$(curl -s -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains" \ | jq '[.items // [] | .[] | select(.metadata.name != null)] | length')MD_COUNT=$(curl -s -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/mitigated_domains" \ | jq '[.items // [] | .[] | select(.metadata.name != null)] | length')
# If HTTPS LB exists, fetch body to detect skeleton stateHTTPS_IS_SKELETON="false"if [ "$HTTPS_LB" = "200" ]; then HTTPS_LB_BODY=$(curl -s \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-https") HTTPS_IS_SKELETON=$(echo "$HTTPS_LB_BODY" | jq ' ((.spec.default_route_pools // []) | length == 0) and (.spec.client_side_defense == null) ')fi
# Compute deterministic environment statusjq -n \ --argjson http_lb "$HTTP_LB" \ --argjson https_lb "$HTTPS_LB" \ --argjson origin "$ORIGIN" \ --argjson hc "$HC" \ --argjson pd "$PD_COUNT" \ --argjson md "$MD_COUNT" \ --argjson https_skeleton "$HTTPS_IS_SKELETON" \'{ objects: [ { name: "http_lb", http_code: $http_lb, exists: ($http_lb == 200) }, { name: "https_lb", http_code: $https_lb, exists: ($https_lb == 200), is_skeleton: $https_skeleton }, { name: "origin_pool", http_code: $origin, exists: ($origin == 200) }, { name: "healthcheck", http_code: $hc, exists: ($hc == 200) }, { name: "protected_domains", count: $pd, exists: ($pd > 0) }, { name: "mitigated_domains", count: $md, exists: ($md > 0) } ], any_infra_exists: ($http_lb == 200 or ($https_lb == 200 and ($https_skeleton | not)) or $origin == 200 or $hc == 200), any_csd_exists: ($pd > 0 or $md > 0), status: ( if ($http_lb == 404 and $https_lb == 404 and $origin == 404 and $hc == 404 and $pd == 0 and $md == 0) then "CLEAN" elif ($https_lb == 200 and $https_skeleton and $http_lb == 404 and $origin == 404 and $hc == 404 and $pd == 0 and $md == 0) then "HTTPS_SKELETON" elif ($http_lb == 200 and $origin == 200) then "ALL_EXIST" elif ($http_lb == 200 or ($https_lb == 200 and ($https_skeleton | not)) or $origin == 200 or $hc == 200) then "TEARDOWN_NEEDED" elif ($md > 0 and $http_lb == 404 and ($https_lb == 404 or ($https_lb == 200 and $https_skeleton)) and $origin == 404 and $hc == 404) then "MITIGATIONS_ONLY" else "TEARDOWN_NEEDED" end ), action: ( if ($http_lb == 404 and $https_lb == 404 and $origin == 404 and $hc == 404 and $pd == 0 and $md == 0) then "Proceed to Phase 1" elif ($https_lb == 200 and $https_skeleton and $http_lb == 404 and $origin == 404 and $hc == 404 and $pd == 0 and $md == 0) then "Proceed to Phase 1 (HTTPS LB skeleton will be restored via PUT)" elif ($http_lb == 200 and $origin == 200) then "All Phase 1 objects exist — verify health, optionally skip to Phase 2" elif ($http_lb == 200 or ($https_lb == 200 and ($https_skeleton | not)) or $origin == 200 or $hc == 200) then "Run Phase 4 Teardown first, then re-check" elif ($md > 0 and $http_lb == 404 and ($https_lb == 404 or ($https_lb == 200 and $https_skeleton)) and $origin == 404 and $hc == 404) then "Delete mitigated domains inline, then proceed" else "Run Phase 4 Teardown first, then re-check" end )}'Skip-Ahead Verification
Section titled “Skip-Ahead Verification”When all Phase 1 objects exist (200) and you plan to skip to Phase 2,
run the Phase 1 Step 7 verification commands to confirm infrastructure
health before skipping. Use the exact commands from
Phase 1 — Step 7: Verify:
- DNS resolution:
dig +short xF5XC_DOMAINNAMEx A - HTTP LB state:
GET .../http_loadbalancers/xF5XC_LB_NAMEx-httppiped tojq '{state: .spec.state}'— must showVIRTUAL_HOST_READY - CSD JS configuration:
GET .../js_configuration— must containscriptTag - CSD status:
GET .../statuspiped tojq '{configured: .isConfigured, enabled: .isEnabled}'— both must betrue
All required checks (DNS-1, LB-1, CSD-1, CSD-2) must PASS before skipping to Phase 2. If any check fails, execute Phase 1 starting from the failed step.
Readiness Verification Matrix
Section titled “Readiness Verification Matrix”The pre-flight check above verifies the environment is clean. The readiness matrix below verifies the environment is capable — that all prerequisites, quotas, connectivity, and platform services are in place for a successful demo. Run this matrix before every meeting as part of the Prepare stage.
Each check has a test ID, a tier (T0–T5), a PASS/FAIL/WARN criteria, and a remediation path. Tiers are sequential — a FAIL in an earlier tier blocks later tiers from running.
Tier Summary
Section titled “Tier Summary”| Tier | Category | Blocks Demo? | Purpose |
|---|---|---|---|
| T0 | Connectivity & Auth | Yes | Can we reach the platform and authenticate? |
| T1 | Quotas & Capacity | Yes (if at limit) | Is there room to create demo objects? |
| T2 | Platform Prerequisites | Yes | Are tenant-level services configured? |
| T3 | Origin Health | Warn | Is the backend application responding? |
| T4 | Environment Clean | Auto-remediate | Are there leftover objects from a prior run? |
| T5 | Certificate Readiness | Informational | Will HTTPS work, or should we plan for HTTP-only? |
T0: Connectivity & Auth
Section titled “T0: Connectivity & Auth”These checks confirm the execution host can reach the F5 XC API and the credentials are valid.
PF-T0-1: API Connectivity
Section titled “PF-T0-1: API Connectivity”HTTP_CODE=$(curl -s -o /dev/null -w '%\{http_code\}' --connect-timeout 10 --max-time 15 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/web/namespaces")echo "{\"http_code\": $HTTP_CODE}" | jq '{ check: "PF-T0-1", http_code: .http_code, status: ( if .http_code == 200 then "PASS" elif .http_code == 401 then "FAIL" else "FAIL" end ), detail: ( if .http_code == 200 then "API reachable, token valid" elif .http_code == 401 then "Token expired or invalid — regenerate under Administration > Credentials > API Credentials" elif .http_code == 0 then "Network unreachable — check connectivity, VPN, or TLS compatibility (try --tlsv1.2 --tls-max 1.2)" else "Unexpected HTTP \(.http_code)" end )}'PF-T0-2: Namespace Access
Section titled “PF-T0-2: Namespace Access”HTTP_CODE=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers")echo "{\"http_code\": $HTTP_CODE}" | jq '{ check: "PF-T0-2", http_code: .http_code, status: ( if .http_code == 200 then "PASS" elif .http_code == 404 then "WARN" else "FAIL" end ), detail: ( if .http_code == 200 then "Token has namespace access" elif .http_code == 403 then "Token lacks permissions for namespace — check role bindings" elif .http_code == 404 then "Namespace does not exist — will be created in Phase 1 Step 0" else "Unexpected HTTP \(.http_code)" end )}'PF-T0-3: CSD API Access
Section titled “PF-T0-3: CSD API Access”HTTP_CODE=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/status")echo "{\"http_code\": $HTTP_CODE}" | jq '{ check: "PF-T0-3", http_code: .http_code, status: ( if .http_code == 200 then "PASS" elif .http_code == 404 then "WARN" else "FAIL" end ), detail: ( if .http_code == 200 then "Token has CSD/Shape API permissions" elif .http_code == 403 then "Token lacks CSD role binding — contact tenant administrator" elif .http_code == 404 then "Namespace does not exist — CSD access will be verified after namespace creation in Phase 1" else "Unexpected HTTP \(.http_code)" end )}'PF-T0-4: RBAC Permission Matrix
Section titled “PF-T0-4: RBAC Permission Matrix”Non-destructive probes test read and write permissions for every object type the demo needs. Read is tested via GET on list endpoints. Write is tested via DELETE on known-nonexistent objects — the API returns 403 if RBAC denies the operation, or 404 if the operation is allowed but the object doesn’t exist. This zero-side-effect technique avoids creating temporary probe objects.
PROBE_NAME="rbac-probe-nonexistent"
# Read probesNS_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/web/namespaces/xF5XC_NAMESPACEx")HC_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks")OP_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools")LB_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers")CSD_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/status")PD_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains")MD_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/mitigated_domains")
# Write probes (non-destructive: DELETE/cascade_delete on non-existent objects)NS_W=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -X POST -H "Authorization: APIToken xF5XC_API_TOKENx" \ -H "Content-Type: application/json" \ -d "{\"name\":\"$PROBE_NAME\"}" \ "xF5XC_API_URLx/api/web/namespaces/$PROBE_NAME/cascade_delete")HC_W=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks/$PROBE_NAME")OP_W=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools/$PROBE_NAME")LB_W=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/$PROBE_NAME")PD_W=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains/$PROBE_NAME.example.com")MD_W=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/mitigated_domains/$PROBE_NAME.example.com")
# Compute deterministic permission matrixjq -n \ --argjson ns_r "$NS_R" --argjson ns_w "$NS_W" \ --argjson hc_r "$HC_R" --argjson hc_w "$HC_W" \ --argjson op_r "$OP_R" --argjson op_w "$OP_W" \ --argjson lb_r "$LB_R" --argjson lb_w "$LB_W" \ --argjson csd_r "$CSD_R" \ --argjson pd_r "$PD_R" --argjson pd_w "$PD_W" \ --argjson md_r "$MD_R" --argjson md_w "$MD_W" \'{ check: "PF-T0-4", permissions: [ { object: "namespace", read: ($ns_r != 403), write: ($ns_w != 403), required: false, note: "conditional — only if ns must be created" }, { object: "healthcheck", read: ($hc_r != 403), write: ($hc_w != 403), required: false, note: "optional for CSD" }, { object: "origin_pool", read: ($op_r != 403), write: ($op_w != 403), required: true, note: "" }, { object: "http_loadbalancer", read: ($lb_r != 403), write: ($lb_w != 403), required: true, note: "" }, { object: "csd_status", read: ($csd_r != 403), write: true, required: true, note: "read-only check" }, { object: "protected_domain", read: ($pd_r != 403), write: ($pd_w != 403), required: true, note: "" }, { object: "mitigated_domain", read: ($md_r != 403), write: ($md_w != 403), required: false, note: "Phase 3 only" } ], status: ( if [ ($op_r == 403), ($op_w == 403), ($lb_r == 403), ($lb_w == 403), ($csd_r == 403), ($pd_r == 403), ($pd_w == 403) ] | any then "FAIL" elif ($ns_w == 403 or $hc_w == 403 or $md_w == 403) then "WARN" else "PASS" end ), detail: ( [ (if ($op_r == 403 or $op_w == 403) then "Origin pool: permission denied" else null end), (if ($lb_r == 403 or $lb_w == 403) then "Load balancer: permission denied" else null end), (if $csd_r == 403 then "CSD API: permission denied — CSD may not be enabled for this namespace" else null end), (if ($pd_r == 403 or $pd_w == 403) then "Protected domain: permission denied" else null end), (if $ns_w == 403 then "Namespace: write denied — namespace must already exist (cannot create)" else null end), (if $hc_w == 403 then "Healthcheck: write denied — will skip healthcheck creation" else null end), (if $md_w == 403 then "Mitigated domain: write denied — Phase 3 mitigation will be skipped" else null end) ] | map(select(. != null)) | join("; ") )}'T1: Quotas & Capacity
Section titled “T1: Quotas & Capacity”These checks query the tenant’s Quota Usage API to determine limits, current usage, and remaining capacity for each object kind the demo needs. This replaces probe-and-delete testing with a single read-only API call that reports exact numbers.
PF-T1-0: Quota Usage Gate
Section titled “PF-T1-0: Quota Usage Gate”Query the tenant-wide quota usage endpoint and compute a deterministic PASS/WARN/FAIL status for every object kind the demo needs. This endpoint requires the system namespace. A single API call checks all platform-level quotas at once.
The gate defines a demo_needs array that specifies how many of each object kind the demo will consume, whether the kind is required, and the minimum needed for the demo to proceed. The jq filter compares remaining against needed and computes the status field deterministically — no operator interpretation required.
# Step 1: Fetch quota datacurl -s \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/web/namespaces/system/quota/usage?namespace=system" \ > /tmp/quota.json
# Step 2: Compute gate statusjq ' . as $data | [ { kind: "healthcheck", needed: 1, required: false, min_proceed: 0 }, { kind: "origin_pool", needed: 1, required: true, min_proceed: 1 }, { kind: "endpoint", needed: 1, required: true, min_proceed: 1 }, { kind: "http_loadbalancer", needed: 2, required: true, min_proceed: 1 } ] | map( . as $req | $data.objects[$req.kind] as $obj | $obj.limit.maximum as $limit | $obj.usage.current as $usage | (if $limit == -1 then null else ($limit - $usage) end) as $remaining | { kind: $req.kind, limit: (if $limit == -1 then "unlimited" else $limit end), usage: $usage, remaining: (if $remaining == null then "unlimited" else $remaining end), needed: $req.needed, status: ( if $remaining == null then "PASS" elif $remaining >= $req.needed then "PASS" elif $remaining >= $req.min_proceed then "WARN" else (if $req.required then "FAIL" else "WARN" end) end ) } ) | { checks: ., gate: (if any(.[]; .status == "FAIL") then "FAIL" elif any(.[]; .status == "WARN") then "WARN" else "PASS" end) }' /tmp/quota.jsonGate output — the gate field is the single deterministic verdict:
PASS— all object kinds haveremaining >= needed. Demo can proceed.WARN— at least one kind has reduced capacity but the minimum to proceed is met (e.g., only 1 LB slot available instead of 2, or healthcheck quota exhausted). Demo can proceed with limitations.FAIL— at least one required kind hasremaining < min_proceed. Demo cannot proceed until quota is freed.
Example output (WARN — endpoint at capacity, healthcheck nearly full):
{ "checks": [ { "kind": "healthcheck", "limit": 150, "usage": 149, "remaining": 1, "needed": 1, "status": "PASS" }, { "kind": "origin_pool", "limit": "unlimited", "usage": 420, "remaining": "unlimited", "needed": 1, "status": "PASS" }, { "kind": "endpoint", "limit": 500, "usage": 500, "remaining": 0, "needed": 1, "status": "FAIL" }, { "kind": "http_loadbalancer", "limit": "unlimited", "usage": 116, "remaining": "unlimited", "needed": 2, "status": "PASS" } ], "gate": "FAIL"}gate value | Action |
|---|---|
| PASS | Proceed to PF-T1-4 (protected domain check), then T2 |
| WARN | Note limitations in the readiness report, proceed with reduced capability |
| FAIL | Stop — report which kinds are exhausted and remediation steps below |
Remediation by kind:
| Kind | Remediation |
|---|---|
healthcheck | Delete unused healthchecks to free capacity. Demo proceeds without healthcheck (CSD does not require one). |
origin_pool | Delete unused origin pools or contact your administrator to increase the tenant limit. |
endpoint | Delete unused origin pools in other namespaces to free endpoint capacity (endpoints are sub-objects of origin pools), or contact your administrator. |
http_loadbalancer | Delete unused load balancers or contact your administrator. If only 1 slot is available, the HTTP LB (primary) will be created but the HTTPS LB (secondary) will be skipped. |
PF-T1-4: Protected Domain Quota
Section titled “PF-T1-4: Protected Domain Quota”CSD protected domains do not appear in the platform Quota Usage API. Use a probe-based check: create and immediately delete a probe protected domain.
# Create probe and capture both HTTP code and response bodyPROBE_BODY=$(curl -s -w '\n%\{http_code\}' -X POST \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ -H "Content-Type: application/json" \ -d '{ "metadata": { "name": "preflight-probe.example.com", "namespace": "xF5XC_NAMESPACEx" }, "spec": { "protected_domain": "example.com" } }' \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains")PROBE_HTTP=$(echo "$PROBE_BODY" | tail -1)PROBE_JSON=$(echo "$PROBE_BODY" | sed '$d')
# Compute statusecho "$PROBE_JSON" | jq --argjson http "$PROBE_HTTP" '{ check: "PF-T1-4", http_code: $http, status: ( if $http == 409 then "PASS" elif (.code // 0) == 8 then "FAIL" elif .metadata.name then "PASS" else "FAIL" end ), detail: ( if $http == 409 then "example.com already registered — quota not exhausted" elif (.code // 0) == 8 then "Protected domain quota exhausted — delete unused protected domains" elif .metadata.name then "Probe created — quota available" else "Unexpected response" end )}'
# Cleanup probe (404 is expected if 409 occurred)curl -s -X DELETE \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains/preflight-probe.example.com" \ > /dev/nullFallback: Probe-Based Quota Checks
Section titled “Fallback: Probe-Based Quota Checks”If PF-T1-0 fails (the Quota Usage API returns 403, 404, or an unexpected format), fall back to probe-and-delete checks for healthcheck, origin pool, endpoint, and load balancer quotas. These checks create a temporary object and immediately delete it — if creation returns error code 8 with “exhausted limits”, the quota is full.
Each fallback probe uses the same pattern: create a temporary object, compute a deterministic status from the response, then delete the probe. The status field is PASS if the object was created (.metadata.name present), WARN or FAIL if error code 8 (exhausted limits), depending on whether the kind is required.
Healthcheck probe (WARN if exhausted — healthchecks are optional for CSD):
RESULT=$(curl -s -X POST \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ -H "Content-Type: application/json" \ -d '{ "metadata": { "name": "preflight-quota-probe", "namespace": "xF5XC_NAMESPACEx" }, "spec": { "http_health_check": { "use_origin_server_name": {}, "path": "/", "use_http2": false }, "timeout": 3, "interval": 15, "unhealthy_threshold": 1, "healthy_threshold": 3 } }' \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks")echo "$RESULT" | jq '{ check: "fallback-healthcheck", status: (if .metadata.name then "PASS" elif (.code // 0) == 8 then "WARN" else "FAIL" end), detail: (if .metadata.name then "Quota available" elif (.code // 0) == 8 then "Quota full — healthcheck optional, demo proceeds" else "Unexpected: \(.message // "unknown error")" end)}'curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks/preflight-quota-probe" \ > /dev/nullOrigin pool & endpoint probe (FAIL if exhausted — both are required):
RESULT=$(curl -s -X POST \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ -H "Content-Type: application/json" \ -d '{ "metadata": { "name": "preflight-origin-probe", "namespace": "xF5XC_NAMESPACEx" }, "spec": { "origin_servers": [{ "public_ip": { "ip": "192.0.2.1" }, "labels": {} }], "no_tls": {}, "port": 80, "same_as_endpoint_port": {}, "healthcheck": [], "loadbalancer_algorithm": "LB_OVERRIDE", "endpoint_selection": "LOCAL_PREFERRED" } }' \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools")echo "$RESULT" | jq '{ check: "fallback-origin-pool", status: (if .metadata.name then "PASS" elif (.code // 0) == 8 then "FAIL" else "FAIL" end), detail: (if .metadata.name then "Quota available" elif (.code // 0) == 8 then "Quota exhausted — \(.message // "limit reached")" else "Unexpected: \(.message // "unknown error")" end)}'curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools/preflight-origin-probe" \ > /dev/nullHTTP load balancer probe (FAIL if exhausted — LBs are required):
RESULT=$(curl -s -X POST \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ -H "Content-Type: application/json" \ -d '{ "metadata": { "name": "preflight-lb-probe", "namespace": "xF5XC_NAMESPACEx", "disable": false }, "spec": { "domains": ["preflight-probe.example.com"], "http": { "dns_volterra_managed": false, "port": 80 }, "advertise_on_public_default_vip": {}, "default_route_pools": [], "disable_rate_limit": {}, "no_service_policies": {}, "round_robin": {}, "disable_waf": {}, "no_challenge": {}, "disable_bot_defense": {}, "disable_api_definition": {}, "disable_api_discovery": {}, "disable_ip_reputation": {}, "disable_malicious_user_detection": {}, "single_lb_app": { "disable_discovery": {}, "disable_ddos_detection": {}, "disable_malicious_user_detection": {} }, "disable_trust_client_ip_headers": {}, "user_id_client_ip": {}, "disable_threat_mesh": {}, "l7_ddos_action_default": {}, "system_default_timeouts": {}, "default_sensitive_data_policy": {}, "disable_malware_protection": {}, "disable_api_testing": {} } }' \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers")echo "$RESULT" | jq '{ check: "fallback-http-lb", status: (if .metadata.name then "PASS" elif (.code // 0) == 8 then "FAIL" else "FAIL" end), detail: (if .metadata.name then "Quota available" elif (.code // 0) == 8 then "Quota exhausted — \(.message // "limit reached")" else "Unexpected: \(.message // "unknown error")" end)}'curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/preflight-lb-probe" \ > /dev/nullT2: Platform Prerequisites
Section titled “T2: Platform Prerequisites”These checks verify tenant-level services that the demo depends on.
PF-T2-1: CSD Tenant Status
Section titled “PF-T2-1: CSD Tenant Status”curl -s \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/status" \ | jq '{ check: "PF-T2-1", configured: .isConfigured, enabled: .isEnabled, status: (if .isConfigured and .isEnabled then "PASS" else "FAIL" end), detail: ( if .isConfigured and .isEnabled then "CSD is active" elif (.isConfigured | not) then "CSD not enabled at tenant level — contact F5 XC administrator" else "CSD configured but not active — contact administrator" end ) }'PF-T2-2: DNS Zone Exists
Section titled “PF-T2-2: DNS Zone Exists”HTTP_CODE=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/dns/namespaces/system/dns_zones/xF5XC_ROOT_DOMAINx")echo "{\"http_code\": $HTTP_CODE}" | jq '{ check: "PF-T2-2", http_code: .http_code, status: ( if .http_code == 200 then "PASS" elif .http_code == 404 then "WARN" elif .http_code == 403 then "WARN" else "FAIL" end ), detail: ( if .http_code == 200 then "DNS zone exists in F5 XC" elif .http_code == 404 then "No F5 XC DNS zone — external DNS may be in use" elif .http_code == 403 then "Token lacks DNS zone read access (system namespace)" else "Unexpected HTTP \(.http_code)" end )}'PF-T2-3: DNS Managed Records Enabled
Section titled “PF-T2-3: DNS Managed Records Enabled”Only run if PF-T2-2 returned 200 (F5 XC DNS zone exists).
Check current state:
curl -s \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/dns/namespaces/system/dns_zones/xF5XC_ROOT_DOMAINx" \ | jq '{ check: "PF-T2-3", managed_records: (.spec.primary.allow_http_lb_managed_records // false), status: (if .spec.primary.allow_http_lb_managed_records == true then "PASS" else "WARN" end), detail: (if .spec.primary.allow_http_lb_managed_records == true then "LB-managed DNS records enabled" else "Managed records not enabled — auto-remediation may be needed" end) }'Auto-remediate if needed: If the status is WARN AND PF-T2-4 shows F5 XC nameservers (ns1.f5clouddns.com, ns2.f5clouddns.com), auto-enable managed records using GET+PUT:
# Get current zone configZONE_CONFIG=$(curl -s \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/dns/namespaces/system/dns_zones/xF5XC_ROOT_DOMAINx")
# Update with managed records enabledecho "$ZONE_CONFIG" \ | jq '.spec.primary.allow_http_lb_managed_records = true' \ | curl -s -X PUT \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ -H "Content-Type: application/json" \ -d @- \ "xF5XC_API_URLx/api/config/dns/namespaces/system/dns_zones/xF5XC_ROOT_DOMAINx" \ | jq .Then re-check to confirm the update took effect:
curl -s \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/dns/namespaces/system/dns_zones/xF5XC_ROOT_DOMAINx" \ | jq '.spec.primary.allow_http_lb_managed_records'| Result | DNS Authority | Status | Remediation |
|---|---|---|---|
true | Any | PASS | LB-managed DNS records will be auto-created |
false/null | F5 XC | Auto-remediate | Enable via GET+PUT, verify, report result |
false/null | External | INFO | Managed records not applicable for external DNS — Phase 1 Step 4 will use Option B (manual record creation) |
| Auto-remediation failed | F5 XC | FAIL | Token may lack system namespace write access — contact tenant administrator |
PF-T2-4: DNS Nameserver Authority
Section titled “PF-T2-4: DNS Nameserver Authority”NS_RECORDS=$(dig +short NS xF5XC_ROOT_DOMAINx)echo "$NS_RECORDS" | jq -Rs '{ check: "PF-T2-4", nameservers: (split("\n") | map(select(length > 0))), status: ( if (split("\n") | map(select(length > 0)) | length) == 0 then "FAIL" elif test("f5clouddns\\.com") then "PASS" else "INFO" end ), detail: ( if (split("\n") | map(select(length > 0)) | length) == 0 then "No NS records — DNS is broken for this domain" elif test("f5clouddns\\.com") then "F5 XC is authoritative — automatic DNS management available" else "External DNS provider — Phase 1 Step 4 will use Option B (manual record creation)" end )}'T3: Origin Health
Section titled “T3: Origin Health”These checks verify the backend application is reachable.
Skip condition check: Compute whether the origin IP is an RFC 5737 TEST-NET address before running connectivity tests:
echo "xF5XC_ORIGIN_IPx" | jq -Rs '{ check: "PF-T3-skip", origin_ip: (rtrimstr("\n")), is_test_net: (rtrimstr("\n") | test("^192\\.0\\.2\\.|^198\\.51\\.100\\.|^203\\.0\\.113\\.")), status: (if (rtrimstr("\n") | test("^192\\.0\\.2\\.|^198\\.51\\.100\\.|^203\\.0\\.113\\.")) then "SKIP" else "CONTINUE" end), detail: (if (rtrimstr("\n") | test("^192\\.0\\.2\\.|^198\\.51\\.100\\.|^203\\.0\\.113\\.")) then "RFC 5737 TEST-NET address — not routable, connectivity testing skipped" else "Routable IP — proceed with connectivity tests" end)}'If status is SKIP, record PF-T3-1 and PF-T3-2 as SKIP and move to T4. Otherwise, run the checks below.
PF-T3-1: Origin Server Connectivity
Section titled “PF-T3-1: Origin Server Connectivity”HTTP_CODE=$(curl -s -o /dev/null -w '%\{http_code\}' --connect-timeout 10 --max-time 15 \ "http://xF5XC_ORIGIN_IPx:xF5XC_ORIGIN_PORTx/")echo "{\"http_code\": $HTTP_CODE}" | jq '{ check: "PF-T3-1", http_code: .http_code, status: (if .http_code >= 200 and .http_code < 600 then "PASS" elif .http_code == 0 then "WARN" else "WARN" end), detail: ( if .http_code >= 200 and .http_code < 600 then "Origin responding with HTTP \(.http_code)" elif .http_code == 0 then "Origin unreachable from this network — LB may use a different path" else "Unexpected response code \(.http_code)" end )}'PF-T3-2: Origin Serves HTML Content
Section titled “PF-T3-2: Origin Serves HTML Content”Only run if PF-T3-1 returned a valid HTTP status:
curl -s --max-time 10 "http://xF5XC_ORIGIN_IPx:xF5XC_ORIGIN_PORTx/" \ | grep -qi '</html>' && echo "PASS: HTML content" || echo "WARN: No HTML detected"| Result | Status | Remediation |
|---|---|---|
PASS: HTML content | PASS | Origin serves HTML pages (required for CSD JS injection) |
WARN: No HTML detected | WARN | Origin may be an API-only service or returning non-HTML — CSD JS injection requires HTML page responses |
T4: Environment Clean
Section titled “T4: Environment Clean”These checks verify no named F5 XC configuration objects remain from a prior demo run — HTTP load balancers, HTTPS load balancers, origin pools, healthchecks, protected domains, and mitigated domains. T4 is about object-level cleanup: whether API objects that would conflict with Phase 1 creation still exist. It does not test IP addresses, network connectivity, or origin health (those concerns belong to T3).
Run the six pre-flight commands from the Pre-flight Check section above. Apply the Decision Logic to determine next steps. If objects exist, auto-teardown is performed during Prepare stage (no confirmation needed).
Also check for and delete stale probe objects from prior interrupted pre-flight runs. These probes are only created when the Quota Usage API is unavailable and the fallback probe-based checks were used, or for the protected domain probe (PF-T1-4) which always uses probe-based checking:
# Stale probe cleanup (delete in any order — probes have no dependencies)curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks/preflight-quota-probe" \ > /dev/null
curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/preflight-lb-probe" \ > /dev/null
curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools/preflight-origin-probe" \ > /dev/null
curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains/preflight-probe.example.com" \ > /dev/nullEach DELETE returns 200 (empty {}) if the object existed, or 404 if it did not — both are expected.
T5: Certificate Readiness
Section titled “T5: Certificate Readiness”These checks assess whether HTTPS will work or the demo should plan for HTTP-only.
PF-T5-1: Recent Certificate Issuance History
Section titled “PF-T5-1: Recent Certificate Issuance History”Check whether a Let’s Encrypt certificate was recently issued for the demo domain. Frequent create/destroy cycles can exhaust the weekly rate limit (5 duplicate certificates per week per domain).
PF-T5-2: Existing HTTPS LB Certificate State
Section titled “PF-T5-2: Existing HTTPS LB Certificate State”Only run if an HTTPS LB exists from a prior run (PF-T4 found objects):
CERT_BODY=$(curl -s -w '\n%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-https")CERT_HTTP=$(echo "$CERT_BODY" | tail -1)CERT_JSON=$(echo "$CERT_BODY" | sed '$d')
if [ "$CERT_HTTP" = "404" ]; then echo '{"check":"PF-T5-2","cert_state":null,"status":"SKIP","detail":"No HTTPS LB — certificate state assessed after Phase 1 Step 3"}'else echo "$CERT_JSON" | jq '{ check: "PF-T5-2", cert_state: .spec.cert_state, status: ( if .spec.cert_state == "CertificateValid" then "PASS" elif .spec.cert_state == "AutoCertDomainRateLimited" then "INFO" elif (.spec.cert_state | test("Pending|Started")) then "INFO" else "INFO" end ), detail: ( if .spec.cert_state == "CertificateValid" then "Certificate healthy — HTTPS will work" elif .spec.cert_state == "AutoCertDomainRateLimited" then "Let'\''s Encrypt rate limit hit — plan for HTTP-only demo" elif (.spec.cert_state | test("Pending|Started")) then "Certificate provisioning in progress" else "Certificate state: \(.spec.cert_state // "unknown")" end ) }'fiReadiness Report Format
Section titled “Readiness Report Format”After running all tiers, present a consolidated readiness report:
## Demo Readiness: READY / NOT READY / READY WITH WARNINGS
### T0: Connectivity & Auth| Check | Result | Status ||---|---|---|| PF-T0-1: API Connectivity | 200 | PASS || PF-T0-2: Namespace Access | 200 | PASS || PF-T0-3: CSD API Access | 200 | PASS |
### T1: Quotas & Capacity| Check | Kind | Limit | Usage | Remaining | Needed | Status ||---|---|---|---|---|---|---|| PF-T1-0: Quota Usage Gate | `healthcheck` | 150 | 148 | 2 | 1 | PASS || PF-T1-0: Quota Usage Gate | `origin_pool` | unlimited | 420 | unlimited | 1 | PASS || PF-T1-0: Quota Usage Gate | `endpoint` | 500 | 498 | 2 | 1 | PASS || PF-T1-0: Quota Usage Gate | `http_loadbalancer` | unlimited | 116 | unlimited | 2 | PASS || PF-T1-0: Quota Usage Gate | **gate** | — | — | — | — | **PASS** || PF-T1-4: Protected Domain | — | — | — | — | 1 | PASS (probe) |
### T2: Platform Prerequisites| Check | Result | Status ||---|---|---|| PF-T2-1: CSD Tenant Status | configured + enabled | PASS || PF-T2-2: DNS Zone Exists | 200 | PASS || PF-T2-3: DNS Managed Records | true | PASS || PF-T2-4: DNS Nameserver Authority | f5clouddns.com | PASS |
### T3: Origin Health| Check | Result | Status ||---|---|---|| PF-T3-1: Origin Connectivity | TEST-NET address (192.0.2.1) | SKIP || PF-T3-2: HTML Content | TEST-NET address | SKIP |
### T4: Environment Clean| Check | Result | Status ||---|---|---|| HTTP LB | 404 | PASS || HTTPS LB | 404 | PASS || Origin Pool | 404 | PASS || Healthcheck | 404 | PASS || Protected Domains | 0 | PASS || Mitigated Domains | 0 | PASS |
### T5: Certificate Readiness| Check | Result | Status ||---|---|---|| PF-T5-2: Cert State | SKIP (no HTTPS LB) | INFO |
### Warnings- (list any WARN or INFO items with context)Overall status rules:
| Condition | Status |
|---|---|
| All T0–T4 checks PASS | READY |
| All T0–T4 checks PASS but T3 or T5 have WARN/INFO | READY WITH WARNINGS |
| Any T0, T1, or T2 check is FAIL | NOT READY — resolve before proceeding |
| T4 has leftover objects | Auto-remediate (teardown), then re-check |
AI Assistant Execution Protocol
Section titled “AI Assistant Execution Protocol”This section defines a deterministic workflow for AI assistants (Claude Code, Copilot, etc.) executing the API automation steps. Following this protocol eliminates guesswork — every decision point has a defined resolution path.
Variable Resolution Protocol
Section titled “Variable Resolution Protocol”Resolve each variable in this exact order. Stop at the first source that provides a non-placeholder value:
- Check
.envfile — look for.envin the repository root. If it exists, parse allKEY=VALUEpairs. - Check shell environment — run
env | grep F5XC_to find any values already exported in the current session. - Identify missing values — compare resolved values against the required/optional table below. A value is “missing” if it is absent, empty, or still set to a placeholder default (e.g.,
example-api-token,example-tenant,example-namespace,app.example.com,user@example.com). - Prompt the human operator — for each missing required variable, ask the operator to provide the value. Do not proceed until all required variables are resolved.
- Apply defaults — for each missing optional variable, use the default from the table below without prompting.
Empty string = missing: A variable exported with an empty value (e.g.,
F5XC_HC_NAME="") is treated the same as an unset variable — apply the default. Use shell parameter expansion (${F5XC_HC_NAME:-csd-hc}) to apply defaults in one step.
- Display confirmation — show the final resolved variable table to the operator and wait for approval before executing any API calls.
Prepare stage override: During Stage 1 Prepare, skip the wait in step 6. Display the resolved variable table for the record, then proceed immediately. Steps 1–5 still apply — if any required variable is missing after checking
.envand shell, stop and report the missing variables.
Required vs Optional Variables
Section titled “Required vs Optional Variables”| Variable | Required | Default | Placeholder (treat as missing) |
|---|---|---|---|
F5XC_API_TOKEN | Yes | — | example-api-token |
F5XC_API_URL | Yes | — | https://example-tenant.console.ves.volterra.io |
F5XC_NAMESPACE | Yes | — | example-namespace |
F5XC_DOMAINNAME | Yes | — | app.example.com |
F5XC_ROOT_DOMAIN | Yes | — | example.com |
F5XC_LB_NAME | Yes | — | example-lb-name, example-lb |
F5XC_EMAIL | Yes | — | user@example.com |
F5XC_HC_NAME | Optional | csd-hc | — |
F5XC_ORIGIN_IP | Optional | 44.232.69.192 | — |
F5XC_ORIGIN_POOL | Optional | csd-origin | — |
F5XC_ORIGIN_PORT | Optional | 3000 | — |
Execution Modes
Section titled “Execution Modes”The AI assistant operates in one of three modes during the demo:
| Mode | When active | Behavior |
|---|---|---|
| Normal | Default during Prepare, Execute, Teardown | Verbatim documented commands only |
| Debug | Auto-activates on failure | Creative troubleshooting, update docs |
| Q&A | During Q&A stage | Improvisational — ad-hoc commands allowed to answer audience questions |
Normal mode (default):
- Every API call, verification query, and shell command must come verbatim from the phase files (Phase 1–4) or from the Pre-flight Check section above
- Substitute only
xTOKENxplaceholders with resolved variable values - Do not construct API endpoints, jq filters, or cURL commands from general knowledge or inference
- If a needed command is not documented, stop and report to the operator: “This verification step is not covered by the phase documentation”
Debug mode (auto-activates on failure):
- Activates automatically when a documented command produces an unexpected result: non-2xx HTTP response, jq parse error, command timeout, or response body that contradicts the evidence table
- In debug mode, the AI assistant may construct diagnostic commands, inspect raw API responses, test endpoint variations, and use creative troubleshooting to find the root cause
- Prefix all debug output with
[DEBUG]so the operator can distinguish diagnostic activity from normal execution - Document what you learn: after resolving an issue, update the
relevant phase file or troubleshooting section with:
- The failure scenario (what went wrong)
- What was tried and did NOT work (so future runs don’t repeat it)
- The working resolution (the command or fix that solved it)
- The goal of debug mode is to eliminate itself — every debug session should produce a documentation update that makes the next execution fully deterministic
- Once the documentation is updated and the issue is resolved, return to normal mode and resume from the last successful documented step
- If debug mode cannot resolve the issue, report findings to the operator and stop — do not continue to the next phase
Q&A mode (during Q&A stage):
- Active only during the Q&A meeting stage, after the demo conclusion
- The AI assistant may construct ad-hoc API calls, run diagnostic commands, navigate to unscripted pages, and modify the live demo environment to illustrate answers to audience questions
- No
[DEBUG]prefix — this is intentional improvisational behavior, not error recovery - Uses the CSD Product Expertise section in
DEMO_EXECUTOR.mdas the knowledge base for product questions
Browser Context Management
Section titled “Browser Context Management”initScript accumulation is a common source of demo failures. Each
navigate_page call with an initScript parameter adds the script to
a persistent list that runs on every subsequent document load. Follow
these rules:
- Always navigate to
about:blankbefore any navigation withinitScriptto clear accumulated scripts from prior runs - Use
new_pagewithisolatedContextwhen switching between demo phases (e.g., Phase 2 → Phase 3) to ensure a completely clean browser state - Recovery from unresponsive pages — if
take_screenshotortake_snapshottimeouts occur, the browser context is resource-exhausted. Usenew_pagewithisolatedContextto create a fresh context, then retry from theabout:blanknavigation step - See the Phase 2 attack simulation asides for detailed guidance on transient origin failures and resource exhaustion recovery
Evidence Display Protocol
Section titled “Evidence Display Protocol”After every API call, the AI assistant must present structured evidence to the human operator using this format:
Creation steps (POST):
| Field | Value | Status |
|---|---|---|
| HTTP Status | 200 | PASS |
| Object Name | csd-origin | — |
| Key Property | (extracted via jq) | — |
After each creation step, run a GET to confirm the object exists and display its key properties. If the GET returns 404, report FAIL and stop.
Verification steps (GET/dig):
| Test | Result | Status |
|---|---|---|
| DNS-1: A Record | 198.51.100.10 | PASS |
| LB-1: HTTP LB State | VIRTUAL_HOST_READY | PASS |
| LB-2: HTTPS LB State | VIRTUAL_HOST_READY | INFO (optional) |
| TLS-1: Cert State | CertificateValid | INFO (optional) |
| CSD-1: JS Tag | scriptTag present | PASS |
Reference the Diagnostics & Verification test case IDs (DNS-1, TLS-1, LB-1, CSD-1, etc.) as the verification standard for each layer.
Execution Flow Summary
Section titled “Execution Flow Summary”Phase execution is sequential and gated: each phase must reach PASS on all required checks before the next phase begins. The demo follows a four-stage meeting lifecycle — see the Meeting Stages section in DEMO_EXECUTOR.md for trigger phrases and behavioral rules.
The AI assistant follows this sequence:
- Prepare — resolve variables, run pre-flight checks, confirm clean environment (can be run separately before the meeting)
- Introduction — SE introduces themselves and states outcome goals (visibility into client-side threats, PCI compliance, real-time detection)
- Execute Phase 1 (Steps 1–7) — infrastructure creation and verification; all Phase 1 checks must PASS before proceeding
- Execute Phase 2 (Steps 8–9) — attack simulation and detection verification via API; AI assistants with browser automation execute the browser steps directly, operators without browser tools perform them manually
- Execute Phase 3 — apply mitigation for all detected domains, re-run attack, verify blocking is effective
- Conclusion — restate outcome goals, summarize evidence from each phase, highlight key detections and mitigations
- Q&A — improvisational stage, demo stays live, SE answers audience questions and asks return questions
- Teardown (post-meeting) — Phase 4, explicit operator confirmation required, delete all objects in reverse dependency order, confirm clean environment
If any step returns FAIL, stop and report the failure with the relevant troubleshooting section link before continuing.
Prerequisites
Section titled “Prerequisites”- An F5 XC API token — generate one under Administration → Credentials → API Credentials
curlandjqinstalled locally- A namespace with permissions to create healthchecks, origin pools, and HTTP load balancers
Environment Setup
Section titled “Environment Setup”Create a .env file with your environment values. A template is provided in the repository:
cp .env.example .envEdit .env with your actual values:
# Required — your environmentF5XC_API_TOKEN=example-api-tokenF5XC_API_URL=https://example-tenant.console.ves.volterra.ioF5XC_DOMAINNAME=app.example.comF5XC_EMAIL=user@example.comF5XC_LB_NAME=example-lb-nameF5XC_NAMESPACE=example-namespaceF5XC_ROOT_DOMAIN=example.com
# Optional — Juice Shop demo defaults are used when not set# F5XC_HC_NAME=csd-hc# F5XC_ORIGIN_IP=44.232.69.192# F5XC_ORIGIN_POOL=csd-origin# F5XC_ORIGIN_PORT=3000Source the file to load variables into your shell session:
set -a && source .env && set +aEach xTOKENx placeholder in the curl commands maps directly to an environment variable — for example, xF5XC_API_TOKENx corresponds to $F5XC_API_TOKEN. You can substitute these values using the interactive form at the top of the page, or let an AI assistant like Claude Code read your .env and build the commands for you.
Placeholder Tokens
Section titled “Placeholder Tokens”| Token | Default | Description |
|---|---|---|
xF5XC_API_URLx | https://example-tenant.console.ves.volterra.io | XC Console API URL |
xF5XC_API_TOKENx | example-api-token | API credential token |
xF5XC_EMAILx | user@example.com | CSD notification email address |
xF5XC_NAMESPACEx | example-namespace | Namespace |
xF5XC_LB_NAMEx | example-lb | HTTP Load Balancer base name (creates ${name}-http and ${name}-https) |
xF5XC_DOMAINNAMEx | app.example.com | FQDN to protect |
xF5XC_ROOT_DOMAINx | example.com | Root domain (eTLD+1) for CSD protected domain |
xF5XC_ORIGIN_POOLx | csd-origin | Origin pool name |
xF5XC_ORIGIN_IPx | 44.232.69.192 | Origin server IP |
xF5XC_ORIGIN_PORTx | 3000 | Origin server port |
xF5XC_HC_NAMEx | csd-hc | Healthcheck name |
Automation Reference
Section titled “Automation Reference”This section summarizes the full exercise workflow for scripting or automation.
Quick Start
Section titled “Quick Start”- Clone the repository and copy the environment template:
cp .env.example .env - Edit
.envwith your tenant URL, API token, namespace, and domain values - Source the environment:
set -a && source .env && set +a - Execute each phase in order, verifying PASS at each Evidence block before proceeding to the next phase
Variable Resolution
Section titled “Variable Resolution”Values are resolved using the deterministic protocol defined in AI Assistant Execution Protocol:
.envfile — parseKEY=VALUEpairs from the repository root- Shell environment — check
env | grep F5XC_for exported values - Placeholder detection — flag any value matching a placeholder default (e.g.,
example-api-token,example-namespace) as missing - Prompt operator — ask for each missing required variable
- Apply defaults — use built-in defaults for missing optional variables
- Confirm — display the resolved variable table and wait for operator approval
Execution Order
Section titled “Execution Order”- Phase 1 — Build: Deploy infrastructure (healthcheck, origin pool, HTTP LB + HTTPS LB), configure DNS, enable CSD, register protected domain, verify all components. The HTTP LB is the primary demo target; the HTTPS LB is optional.
- Phase 2 — Attack: Run the attack simulation in a browser using
http://URLs, wait 5–10 minutes, verify detections via API (/scripts,/detected_domains,/formFields) - Phase 3 — Mitigate: Confirm clean baseline, run attack (before proof), POST each domain to
/mitigated_domains, verify mitigations applied, re-run attack usinghttp://URLs (after proof), present before/after comparison - Phase 4 — Teardown (requires explicit human confirmation): Delete HTTPS LB → HTTP LB → origin pool → DNS zone cleanup (manual records only) → healthcheck → protected domain. Do not delete the DNS zone.
Variables
Section titled “Variables”| Token | Description | Default |
|---|---|---|
xF5XC_API_URLx | XC Console API URL | https://example-tenant.console.ves.volterra.io |
xF5XC_API_TOKENx | API credential token | (user-provided) |
xF5XC_EMAILx | CSD notification email | user@example.com |
xF5XC_NAMESPACEx | Namespace | example-namespace |
xF5XC_LB_NAMEx | HTTP Load Balancer base name (creates ${name}-http and ${name}-https) | example-lb |
xF5XC_DOMAINNAMEx | FQDN to protect | app.example.com |
xF5XC_ROOT_DOMAINx | Root domain (eTLD+1) | example.com |
xF5XC_ORIGIN_POOLx | Origin pool name | csd-origin |
xF5XC_ORIGIN_IPx | Origin server IP | 44.232.69.192 |
xF5XC_ORIGIN_PORTx | Origin server port | 3000 |
xF5XC_HC_NAMEx | Healthcheck name | csd-hc |
oneOf Choice Groups
Section titled “oneOf Choice Groups”The HTTP Load Balancer spec uses oneOf choice groups where exactly one option must be set per group. Setting zero or more than one option in a group causes a 422 error.
Key CSD-related choices:
| Choice Group | Options | CSD Default |
|---|---|---|
client_side_defense_choice | client_side_defense, disable_client_side_defense | client_side_defense |
java_script_choice (nested in CSD) | disable_js_insert, js_insert_all_pages, js_insert_all_pages_except, js_insertion_rules | js_insert_all_pages |
Listener type choice (HTTP vs HTTPS):
The demo creates two LBs with different listener configurations:
| LB | Listener Choice | Config |
|---|---|---|
${F5XC_LB_NAME}-http (primary) | http | "http": { "dns_volterra_managed": true, "port": 80 } |
${F5XC_LB_NAME}-https (secondary) | https_auto_cert | "https_auto_cert": { "http_redirect": true, "port": 443, ... } |
The http and https_auto_cert keys are mutually exclusive — each LB uses exactly one.
HTTPS auto-cert nested choices (secondary LB only):
| Choice Group | Options | Default |
|---|---|---|
| port | port (number) | 443 |
server_header_choice | default_header, server_name, append_server_name | default_header |
path_normalize_choice | enable_path_normalize, disable_path_normalize | enable_path_normalize |
mtls_choice | no_mtls, use_mtls | no_mtls |
default_loadbalancer_choice | default_loadbalancer, non_default_loadbalancer | default_loadbalancer |
single_lb_app nested choices (ML config):
The single_lb_app object has its own required oneOf groups. Setting single_lb_app: \{\} without these nested choices causes a 400 error.
| Choice Group | Options | Default |
|---|---|---|
api_discovery_choice | disable_discovery, enable_discovery | disable_discovery |
ddos_detection_choice | disable_ddos_detection, enable_ddos_detection | disable_ddos_detection |
malicious_user_detection_choice | disable_malicious_user_detection, enable_malicious_user_detection | disable_malicious_user_detection |
Other LB-level choices (all set to disable/default):
disable_rate_limit · no_service_policies · round_robin · disable_waf · no_challenge · disable_bot_defense · disable_api_definition · disable_api_discovery · disable_ip_reputation · disable_malicious_user_detection · single_lb_app · disable_trust_client_ip_headers · user_id_client_ip · disable_threat_mesh · l7_ddos_action_default · system_default_timeouts · default_sensitive_data_policy · disable_malware_protection · disable_api_testing
Error Handling
Section titled “Error Handling”- 401 Unauthorized — API token is invalid or expired. Regenerate under Administration → Credentials.
- 403 Forbidden — token lacks permissions for the namespace. Check role bindings.
- 404 Not Found — namespace or object name is incorrect. List objects with
GET /api/config/namespaces/\{namespace\}/\{object_type\}. - 409 Conflict — object already exists. For protected domains, a
409with “domain already exists (in uriList)” means the root domain is already registered on the tenant — this is a success condition, not an error. For other objects, delete first or usePUTto update. - 422 Unprocessable Entity — JSON schema validation failed. Common causes: missing
oneOfchoice, multiple choices set in same group, wrong field type. Check the error message for the specific field. - Object limit exhausted (error code
8, message"Object kind {kind} has exhausted limits({N})") — tenant has hit an object quota limit. The API returns HTTP 200 with a JSON error body containing"code": 8, not HTTP 429. You can proactively check quota capacity before encountering this error using the Quota Usage API (GET /api/web/namespaces/system/quota/usage?namespace=system). Behavior depends on the object type:- Healthcheck (limit ~150): Non-blocking — skip Phase 1 Step 1 and create the origin pool without a healthcheck reference. CSD does not depend on health monitoring.
- Endpoint (limit ~500): Blocking — endpoints are sub-objects created inside origin pools. If this limit is hit, origin pool creation fails. Delete unused origin pools (which frees their endpoint sub-objects) or contact your administrator to increase the tenant limit.
- Origin pool: Blocking — delete unused origin pools or contact your administrator.
- HTTP load balancer: Blocking — delete unused load balancers or contact your administrator.
- Protected domain: Blocking — delete unused protected domains or contact your administrator.
Troubleshooting
Section titled “Troubleshooting”Healthcheck Not Linked to Origin Pool
Section titled “Healthcheck Not Linked to Origin Pool”If Phase 1 Step 2 (Verify Healthcheck is Linked) shows an empty array []:
- Delete the origin pool:
DELETE /api/config/namespaces/{namespace}/origin_pools/{pool_name} - Verify the healthcheck exists:
GET /api/config/namespaces/{namespace}/healthchecks/{hc_name} - Recreate the origin pool with the correct healthcheck reference
LB Stuck in VIRTUAL_HOST_PENDING_A_RECORD
Section titled “LB Stuck in VIRTUAL_HOST_PENDING_A_RECORD”If the load balancer state remains VIRTUAL_HOST_PENDING_A_RECORD after Phase 1 Step 4:
-
Verify DNS zone exists (F5 XC managed DNS only):
Terminal window curl -s \-H "Authorization: APIToken xF5XC_API_TOKENx" \"xF5XC_API_URLx/api/config/dns/namespaces/system/dns_zones/xF5XC_ROOT_DOMAINx" \| jq '.metadata.name'A
404means the DNS zone has not been created in F5 XC. -
Check
allow_http_lb_managed_records— if the zone exists but the LB is pending, managed records may be disabled:Terminal window curl -s \-H "Authorization: APIToken xF5XC_API_TOKENx" \"xF5XC_API_URLx/api/config/dns/namespaces/system/dns_zones/xF5XC_ROOT_DOMAINx" \| jq '.spec.primary.allow_http_lb_managed_records'If
falseornull, enable it using thePUTcommand in Phase 1 Step 4, Option A. -
External DNS — if F5 XC is not authoritative, create the A and ACME CNAME records manually at your DNS provider (see Phase 1 Step 4, Option B).
-
Verify resolution — after fixing DNS, confirm:
Terminal window dig +short xF5XC_DOMAINNAMEx AIf the A record resolves, the LB transitions to
VIRTUAL_HOST_READY. Poll every 30 seconds, up to 4 iterations (2 minutes total). If stillVIRTUAL_HOST_PENDING_A_RECORDafter 2 minutes, re-check DNS propagation and report to operator.
CSD Status Shows isConfigured: false
Section titled “CSD Status Shows isConfigured: false”CSD must be enabled at the tenant level. Contact your F5 XC administrator to enable Client-Side Defense for your tenant. This is a tenant-wide setting that cannot be configured via API.
JS Injection Not Working
Section titled “JS Injection Not Working”- Verify CSD is enabled at tenant level (Phase 1 Step 5)
- Confirm the load balancer has
client_side_defenseset in the spec - Check the JS configuration endpoint returns a
scriptTag - Visit the protected domain in a browser and view page source to confirm the script is injected
Protected Domain Registration Returns 409
Section titled “Protected Domain Registration Returns 409”A 409 with “domain already exists (in uriList)” means the root domain is already registered on the tenant. Protected domains are tenant-scoped — they do not belong to any single namespace. This is a success condition: the domain is already protected and no further action is needed. Continue to Phase 1 Step 7.
AutoCertDomainRateLimited
Section titled “AutoCertDomainRateLimited”If the HTTPS LB cert_state shows AutoCertDomainRateLimited, this means Let’s Encrypt has rate-limited certificate issuance for this domain. This is expected in demo environments where infrastructure is frequently created and destroyed.
Impact: The HTTPS LB will not serve traffic until the rate limit resets (typically 1 hour). The HTTP LB is completely unaffected — all demo traffic proceeds normally over http://.
Resolution: No action needed for demo progression. The HTTP LB (${F5XC_LB_NAME}-http) is the primary demo target and does not depend on certificate provisioning. If HTTPS is desired, wait for the rate limit to reset and the certificate will auto-provision.
Certificate Stuck — Clean Recreation (Optional)
Section titled “Certificate Stuck — Clean Recreation (Optional)”When the HTTPS LB certificate is stuck in DomainChallengePending or PreDomainChallengePending for more than 15 minutes, the fastest recovery path is to delete the HTTPS LB and recreate it. With the DNS zone already configured (managed records enabled), the clean recreation typically produces CertificateValid in 5–7 minutes — unless rate-limited.
- Delete the HTTPS Load Balancer:
DELETE .../http_loadbalancers/${F5XC_LB_NAME}-https - Wait 30 seconds for platform cleanup
- Recreate the HTTPS Load Balancer (Phase 1 Step 3)
- Monitor certificate state (Phase 1 Step 7) — expect
CertificateValidwithin 5–10 minutes
OpenAPI Reference
Section titled “OpenAPI Reference”The canonical F5 Distributed Cloud API specification is available at:
https://docs.cloud.f5.com/docs-v2/downloads/f5-distributed-cloud-open-api.zip
This ZIP contains OpenAPI 3.0 specs for all API groups including ves.io.schema.views.http_loadbalancer, ves.io.schema.healthcheck, ves.io.schema.origin_pool, and ves.io.schema.shape.csd. Use these specs to validate JSON payloads and discover additional fields.