Skip to content

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.

PhaseGoalSteps
Phase 1 — BuildDeploy and validate the full CSD infrastructureSteps 1–7
Phase 2 — AttackGenerate simulated attack traffic and confirm CSD detected itSteps 8–9
Phase 3 — MitigateBefore/after mitigation proof — run attack, apply mitigations, re-run attack, compareSteps 1–6
Phase 4 — TeardownRemove all deployment objects after explicit confirmationTeardown

Before starting Phase 1, verify the environment is clean. Run these API checks to determine if leftover objects exist from a prior run:

Terminal window
# Check all Phase 1 objects and compute environment status
HTTP_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 state
HTTPS_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 status
jq -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
)
}'

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:

  1. DNS resolution: dig +short xF5XC_DOMAINNAMEx A
  2. HTTP LB state: GET .../http_loadbalancers/xF5XC_LB_NAMEx-http piped to jq '{state: .spec.state}' — must show VIRTUAL_HOST_READY
  3. CSD JS configuration: GET .../js_configuration — must contain scriptTag
  4. CSD status: GET .../status piped to jq '{configured: .isConfigured, enabled: .isEnabled}' — both must be true

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.

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.

TierCategoryBlocks Demo?Purpose
T0Connectivity & AuthYesCan we reach the platform and authenticate?
T1Quotas & CapacityYes (if at limit)Is there room to create demo objects?
T2Platform PrerequisitesYesAre tenant-level services configured?
T3Origin HealthWarnIs the backend application responding?
T4Environment CleanAuto-remediateAre there leftover objects from a prior run?
T5Certificate ReadinessInformationalWill HTTPS work, or should we plan for HTTP-only?

These checks confirm the execution host can reach the F5 XC API and the credentials are valid.

Terminal window
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
)
}'
Terminal window
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
)
}'
Terminal window
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
)
}'

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.

Terminal window
PROBE_NAME="rbac-probe-nonexistent"
# Read probes
NS_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 matrix
jq -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("; ")
)
}'

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.

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.

Terminal window
# Step 1: Fetch quota data
curl -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 status
jq '
. 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.json

Gate output — the gate field is the single deterministic verdict:

  • PASS — all object kinds have remaining >= 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 has remaining < 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 valueAction
PASSProceed to PF-T1-4 (protected domain check), then T2
WARNNote limitations in the readiness report, proceed with reduced capability
FAILStop — report which kinds are exhausted and remediation steps below

Remediation by kind:

KindRemediation
healthcheckDelete unused healthchecks to free capacity. Demo proceeds without healthcheck (CSD does not require one).
origin_poolDelete unused origin pools or contact your administrator to increase the tenant limit.
endpointDelete unused origin pools in other namespaces to free endpoint capacity (endpoints are sub-objects of origin pools), or contact your administrator.
http_loadbalancerDelete 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.

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.

Terminal window
# Create probe and capture both HTTP code and response body
PROBE_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 status
echo "$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/null

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):

Terminal window
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/null

Origin pool & endpoint probe (FAIL if exhausted — both are required):

Terminal window
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/null

HTTP load balancer probe (FAIL if exhausted — LBs are required):

Terminal window
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/null

These checks verify tenant-level services that the demo depends on.

Terminal window
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
)
}'
Terminal window
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
)
}'

Only run if PF-T2-2 returned 200 (F5 XC DNS zone exists).

Check current state:

Terminal window
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:

Terminal window
# Get current zone config
ZONE_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 enabled
echo "$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:

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'
ResultDNS AuthorityStatusRemediation
trueAnyPASSLB-managed DNS records will be auto-created
false/nullF5 XCAuto-remediateEnable via GET+PUT, verify, report result
false/nullExternalINFOManaged records not applicable for external DNS — Phase 1 Step 4 will use Option B (manual record creation)
Auto-remediation failedF5 XCFAILToken may lack system namespace write access — contact tenant administrator
Terminal window
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
)
}'

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:

Terminal window
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.

Terminal window
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
)
}'

Only run if PF-T3-1 returned a valid HTTP status:

Terminal window
curl -s --max-time 10 "http://xF5XC_ORIGIN_IPx:xF5XC_ORIGIN_PORTx/" \
| grep -qi '</html>' && echo "PASS: HTML content" || echo "WARN: No HTML detected"
ResultStatusRemediation
PASS: HTML contentPASSOrigin serves HTML pages (required for CSD JS injection)
WARN: No HTML detectedWARNOrigin may be an API-only service or returning non-HTML — CSD JS injection requires HTML page responses

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:

Terminal window
# 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/null

Each DELETE returns 200 (empty {}) if the object existed, or 404 if it did not — both are expected.

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):

Terminal window
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
)
}'
fi

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:

ConditionStatus
All T0–T4 checks PASSREADY
All T0–T4 checks PASS but T3 or T5 have WARN/INFOREADY WITH WARNINGS
Any T0, T1, or T2 check is FAILNOT READY — resolve before proceeding
T4 has leftover objectsAuto-remediate (teardown), then re-check

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.

Resolve each variable in this exact order. Stop at the first source that provides a non-placeholder value:

  1. Check .env file — look for .env in the repository root. If it exists, parse all KEY=VALUE pairs.
  2. Check shell environment — run env | grep F5XC_ to find any values already exported in the current session.
  3. 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).
  4. 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.
  5. 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.

  1. 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 .env and shell, stop and report the missing variables.

VariableRequiredDefaultPlaceholder (treat as missing)
F5XC_API_TOKENYesexample-api-token
F5XC_API_URLYeshttps://example-tenant.console.ves.volterra.io
F5XC_NAMESPACEYesexample-namespace
F5XC_DOMAINNAMEYesapp.example.com
F5XC_ROOT_DOMAINYesexample.com
F5XC_LB_NAMEYesexample-lb-name, example-lb
F5XC_EMAILYesuser@example.com
F5XC_HC_NAMEOptionalcsd-hc
F5XC_ORIGIN_IPOptional44.232.69.192
F5XC_ORIGIN_POOLOptionalcsd-origin
F5XC_ORIGIN_PORTOptional3000

The AI assistant operates in one of three modes during the demo:

ModeWhen activeBehavior
NormalDefault during Prepare, Execute, TeardownVerbatim documented commands only
DebugAuto-activates on failureCreative troubleshooting, update docs
Q&ADuring Q&A stageImprovisational — 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 xTOKENx placeholders 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:
    1. The failure scenario (what went wrong)
    2. What was tried and did NOT work (so future runs don’t repeat it)
    3. 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.md as the knowledge base for product questions

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:blank before any navigation with initScript to clear accumulated scripts from prior runs
  • Use new_page with isolatedContext when switching between demo phases (e.g., Phase 2 → Phase 3) to ensure a completely clean browser state
  • Recovery from unresponsive pages — if take_screenshot or take_snapshot timeouts occur, the browser context is resource-exhausted. Use new_page with isolatedContext to create a fresh context, then retry from the about:blank navigation step
  • See the Phase 2 attack simulation asides for detailed guidance on transient origin failures and resource exhaustion recovery

After every API call, the AI assistant must present structured evidence to the human operator using this format:

Creation steps (POST):

FieldValueStatus
HTTP Status200PASS
Object Namecsd-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):

TestResultStatus
DNS-1: A Record198.51.100.10PASS
LB-1: HTTP LB StateVIRTUAL_HOST_READYPASS
LB-2: HTTPS LB StateVIRTUAL_HOST_READYINFO (optional)
TLS-1: Cert StateCertificateValidINFO (optional)
CSD-1: JS TagscriptTag presentPASS

Reference the Diagnostics & Verification test case IDs (DNS-1, TLS-1, LB-1, CSD-1, etc.) as the verification standard for each layer.

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:

  1. Prepare — resolve variables, run pre-flight checks, confirm clean environment (can be run separately before the meeting)
  2. Introduction — SE introduces themselves and states outcome goals (visibility into client-side threats, PCI compliance, real-time detection)
  3. Execute Phase 1 (Steps 1–7) — infrastructure creation and verification; all Phase 1 checks must PASS before proceeding
  4. 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
  5. Execute Phase 3 — apply mitigation for all detected domains, re-run attack, verify blocking is effective
  6. Conclusion — restate outcome goals, summarize evidence from each phase, highlight key detections and mitigations
  7. Q&A — improvisational stage, demo stays live, SE answers audience questions and asks return questions
  8. 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.

  • An F5 XC API token — generate one under AdministrationCredentialsAPI Credentials
  • curl and jq installed locally
  • A namespace with permissions to create healthchecks, origin pools, and HTTP load balancers

Create a .env file with your environment values. A template is provided in the repository:

Terminal window
cp .env.example .env

Edit .env with your actual values:

.env
# Required — your environment
F5XC_API_TOKEN=example-api-token
F5XC_API_URL=https://example-tenant.console.ves.volterra.io
F5XC_DOMAINNAME=app.example.com
F5XC_EMAIL=user@example.com
F5XC_LB_NAME=example-lb-name
F5XC_NAMESPACE=example-namespace
F5XC_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=3000

Source the file to load variables into your shell session:

Terminal window
set -a && source .env && set +a

Each 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.

TokenDefaultDescription
xF5XC_API_URLxhttps://example-tenant.console.ves.volterra.ioXC Console API URL
xF5XC_API_TOKENxexample-api-tokenAPI credential token
xF5XC_EMAILxuser@example.comCSD notification email address
xF5XC_NAMESPACExexample-namespaceNamespace
xF5XC_LB_NAMExexample-lbHTTP Load Balancer base name (creates ${name}-http and ${name}-https)
xF5XC_DOMAINNAMExapp.example.comFQDN to protect
xF5XC_ROOT_DOMAINxexample.comRoot domain (eTLD+1) for CSD protected domain
xF5XC_ORIGIN_POOLxcsd-originOrigin pool name
xF5XC_ORIGIN_IPx44.232.69.192Origin server IP
xF5XC_ORIGIN_PORTx3000Origin server port
xF5XC_HC_NAMExcsd-hcHealthcheck name

This section summarizes the full exercise workflow for scripting or automation.

  1. Clone the repository and copy the environment template: cp .env.example .env
  2. Edit .env with your tenant URL, API token, namespace, and domain values
  3. Source the environment: set -a && source .env && set +a
  4. Execute each phase in order, verifying PASS at each Evidence block before proceeding to the next phase

Values are resolved using the deterministic protocol defined in AI Assistant Execution Protocol:

  1. .env file — parse KEY=VALUE pairs from the repository root
  2. Shell environment — check env | grep F5XC_ for exported values
  3. Placeholder detection — flag any value matching a placeholder default (e.g., example-api-token, example-namespace) as missing
  4. Prompt operator — ask for each missing required variable
  5. Apply defaults — use built-in defaults for missing optional variables
  6. Confirm — display the resolved variable table and wait for operator approval
  1. 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.
  2. 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)
  3. Phase 3 — Mitigate: Confirm clean baseline, run attack (before proof), POST each domain to /mitigated_domains, verify mitigations applied, re-run attack using http:// URLs (after proof), present before/after comparison
  4. 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.
TokenDescriptionDefault
xF5XC_API_URLxXC Console API URLhttps://example-tenant.console.ves.volterra.io
xF5XC_API_TOKENxAPI credential token(user-provided)
xF5XC_EMAILxCSD notification emailuser@example.com
xF5XC_NAMESPACExNamespaceexample-namespace
xF5XC_LB_NAMExHTTP Load Balancer base name (creates ${name}-http and ${name}-https)example-lb
xF5XC_DOMAINNAMExFQDN to protectapp.example.com
xF5XC_ROOT_DOMAINxRoot domain (eTLD+1)example.com
xF5XC_ORIGIN_POOLxOrigin pool namecsd-origin
xF5XC_ORIGIN_IPxOrigin server IP44.232.69.192
xF5XC_ORIGIN_PORTxOrigin server port3000
xF5XC_HC_NAMExHealthcheck namecsd-hc

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 GroupOptionsCSD Default
client_side_defense_choiceclient_side_defense, disable_client_side_defenseclient_side_defense
java_script_choice (nested in CSD)disable_js_insert, js_insert_all_pages, js_insert_all_pages_except, js_insertion_rulesjs_insert_all_pages

Listener type choice (HTTP vs HTTPS):

The demo creates two LBs with different listener configurations:

LBListener ChoiceConfig
${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 GroupOptionsDefault
portport (number)443
server_header_choicedefault_header, server_name, append_server_namedefault_header
path_normalize_choiceenable_path_normalize, disable_path_normalizeenable_path_normalize
mtls_choiceno_mtls, use_mtlsno_mtls
default_loadbalancer_choicedefault_loadbalancer, non_default_loadbalancerdefault_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 GroupOptionsDefault
api_discovery_choicedisable_discovery, enable_discoverydisable_discovery
ddos_detection_choicedisable_ddos_detection, enable_ddos_detectiondisable_ddos_detection
malicious_user_detection_choicedisable_malicious_user_detection, enable_malicious_user_detectiondisable_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

  • 401 Unauthorized — API token is invalid or expired. Regenerate under AdministrationCredentials.
  • 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 409 with “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 use PUT to update.
  • 422 Unprocessable Entity — JSON schema validation failed. Common causes: missing oneOf choice, 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.

If Phase 1 Step 2 (Verify Healthcheck is Linked) shows an empty array []:

  1. Delete the origin pool: DELETE /api/config/namespaces/{namespace}/origin_pools/{pool_name}
  2. Verify the healthcheck exists: GET /api/config/namespaces/{namespace}/healthchecks/{hc_name}
  3. Recreate the origin pool with the correct healthcheck reference

If the load balancer state remains VIRTUAL_HOST_PENDING_A_RECORD after Phase 1 Step 4:

  1. 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 404 means the DNS zone has not been created in F5 XC.

  2. 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 false or null, enable it using the PUT command in Phase 1 Step 4, Option A.

  3. 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).

  4. Verify resolution — after fixing DNS, confirm:

    Terminal window
    dig +short xF5XC_DOMAINNAMEx A

    If the A record resolves, the LB transitions to VIRTUAL_HOST_READY. Poll every 30 seconds, up to 4 iterations (2 minutes total). If still VIRTUAL_HOST_PENDING_A_RECORD after 2 minutes, re-check DNS propagation and report to operator.

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.

  1. Verify CSD is enabled at tenant level (Phase 1 Step 5)
  2. Confirm the load balancer has client_side_defense set in the spec
  3. Check the JS configuration endpoint returns a scriptTag
  4. Visit the protected domain in a browser and view page source to confirm the script is injected

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.

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.

  1. Delete the HTTPS Load Balancer: DELETE .../http_loadbalancers/${F5XC_LB_NAME}-https
  2. Wait 30 seconds for platform cleanup
  3. Recreate the HTTPS Load Balancer (Phase 1 Step 3)
  4. Monitor certificate state (Phase 1 Step 7) — expect CertificateValid within 5–10 minutes

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.