Skip to content

Demo

This guide covers the direct API ownership mode for a complete Client-Side Defense exercise. Terraform users must follow the Terraform deployment group instead. Both modes create the same logical architecture: one origin pool and one HTTPS auto-certificate HTTP load balancer named client-side-defense for client-side-defense.f5-sales-demo.com, with HTTP redirect and CSD JavaScript insertion on all pages.

The AWS reference supplies the Application Load Balancer hostname through the origin pool’s public_name; an Azure alternate supplies its address through public_ip.

Choose one ownership mode. Never run the API create/update/delete workflow against resources present in Terraform state.

PhaseGoalSteps
Phase 1 — API BuildDeploy and validate the shared CSD architecture with API ownershipSteps 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 API-owned deployment objects after explicit confirmationTeardown

Establish XCSH_CSD_DEPLOYMENT_MODE before any request. Run these GET-only checks only in API mode. Terraform mode stops here and uses only its configured backend and state.

Before the API build, inspect the exact load balancer, origin pool, optional healthcheck, protected-domain resource, and any known mitigated-domain resource names. Skip the healthcheck GET when XCSH_HC_NAME is empty; omission is valid.

Exact detail GETs are authoritative for configuration and ownership classification. List endpoints are discovery summaries only: configuration lists expose items[].name and may omit get_spec, so use a discovered name with its detail GET before classifying an object. Do not reduce protected or mitigated domains to global counts.

Terminal window
case "${XCSH_CSD_DEPLOYMENT_MODE:-}" in
api) ;;
terraform) echo "INFO: Terraform owns lifecycle"; exit 0 ;;
*) echo "FAIL: invalid deployment mode"; exit 1 ;;
esac
curl -sS -o /dev/null -w 'load_balancer %{http_code}\n' \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/http_loadbalancers/xXCSH_LB_NAMEx"
curl -sS -o /dev/null -w 'origin_pool %{http_code}\n' \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/origin_pools/xXCSH_ORIGIN_POOLx"
if [ -n "${XCSH_HC_NAME:-}" ]; then
curl -sS -o /dev/null -w 'healthcheck %{http_code}\n' \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/healthchecks/xXCSH_HC_NAMEx"
fi
curl -sS -o /dev/null -w 'protected_domain %{http_code}\n' \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/protected_domains/xXCSH_LB_NAMEx"
# Discovery only: list summaries identify candidate names; detail GETs classify them.
MITIGATED_LIST=$(curl -sS -H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/mitigated_domains")
printf '%s' "$MITIGATED_LIST" | jq '[.items[]? | {name, get_spec}]'
printf '%s' "$MITIGATED_LIST" | jq -er '.items[]?.name' | while IFS= read -r name; do
curl -sS -H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/mitigated_domains/$name" \
| jq '{name: .metadata.name, mitigated_domain: .spec.mitigated_domain}'
done

Any exact detail GET returning 200 for an object not recorded by the current run’s ledger is pre-existing and blocks creation. Any 403, unexpected response, duplicate candidate name, missing required detail, or ambiguous ownership is unknown and blocks mutation.

A 404 detail response is absent. A list entry is never enough to classify ownership: follow its items[].name with the exact detail GET. Never auto-teardown these findings.

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?
T4Ownership & Existing StateYesIs lifecycle ownership known and consistent?
T5Certificate ReadinessYesIs the required HTTPS automatic certificate valid?

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 xXCSH_API_TOKENx" \
"xXCSH_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 xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_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 — follow the API Build namespace step"
else "Unexpected HTTP \(.http_code)"
end
)
}'
Terminal window
HTTP_CODE=$(curl -s -o /dev/null -w '%\{http_code\}' \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_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 — verify CSD access after the API Build namespace step"
else "Unexpected HTTP \(.http_code)"
end
)
}'

Establish the deployment owner before any mutation. XCSH_CSD_DEPLOYMENT_MODE must be exactly api or terraform. In API mode, use GET requests on the namespace and required list/detail endpoints to establish access and classify each target as absent, pre-existing, or unknown. In Terraform mode, stop this API workflow and use only the configured Terraform backend and state.

Do not infer write permission by deleting a nonexistent object or calling namespace cascade deletion. Those requests are mutations and are not valid readiness probes. A successful read proves visibility only; mutation authorization is established by the approved Phase 1 operation and its response.

Terminal window
case "${XCSH_CSD_DEPLOYMENT_MODE:-}" in
api|terraform) ;;
*) echo "FAIL: XCSH_CSD_DEPLOYMENT_MODE must be api or terraform"; exit 1 ;;
esac
if [ "$XCSH_CSD_DEPLOYMENT_MODE" = terraform ]; then
echo "INFO: Terraform owns lifecycle; stop the API workflow and use its configured state."
exit 0
fi
for endpoint in \
"api/web/namespaces/$XCSH_NAMESPACE" \
"api/config/namespaces/$XCSH_NAMESPACE/origin_pools/$XCSH_ORIGIN_POOL" \
"api/config/namespaces/$XCSH_NAMESPACE/http_loadbalancers/$XCSH_LB_NAME" \
"api/shape/csd/namespaces/$XCSH_NAMESPACE/status" \
"api/shape/csd/namespaces/$XCSH_NAMESPACE/protected_domains/$XCSH_LB_NAME" \
"api/shape/csd/namespaces/$XCSH_NAMESPACE/mitigated_domains"
do
curl -sS -o /dev/null -w "$endpoint %{http_code}\n" --max-time 10 \
-H "Authorization: APIToken $XCSH_API_TOKEN" \
"$XCSH_API_URL/$endpoint"
done
if [ -n "${XCSH_HC_NAME:-}" ]; then
endpoint="api/config/namespaces/$XCSH_NAMESPACE/healthchecks/$XCSH_HC_NAME"
curl -sS -o /dev/null -w "$endpoint %{http_code}\n" --max-time 10 \
-H "Authorization: APIToken $XCSH_API_TOKEN" \
"$XCSH_API_URL/$endpoint"
else
echo "healthcheck SKIP (optional and omitted)"
fi

Treat 403 as FAIL for required resources, 404 as absence only for an exact detail endpoint, and unexpected responses as UNKNOWN. For the mitigated-domain list, use only items[].name and documented get_spec summary data; follow every relevant name with its exact detail GET before evaluating spec or ownership. Do not mutate to refine an UNKNOWN result.

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 is a read-only capacity check; there is no probe-and-delete fallback.

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 xXCSH_API_TOKENx" \
"xXCSH_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: 1, 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 — an optional object such as the healthcheck has no remaining capacity. The demo can proceed without that optional object.
  • 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": 1, "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_loadbalancerFree one load balancer slot or contact your administrator. The single HTTPS load balancer is required.

CSD protected-domain capacity is not exposed by the platform Quota Usage API. Perform only read-only configuration checks: list current protected domains, confirm the target domain is not already registered under an unknown owner, and record capacity as UNKNOWN unless an administrator supplies the applicable limit and usage.

A capacity value of UNKNOWN is not evidence that quota is available. Do not create, delete, or interpret a 409 from a fixed-name probe as a readiness result.

If PF-T1-0 returns 403, 404, or an unexpected format, stop the quota gate with UNKNOWN capacity and request a read-only administrator quota/configuration check. Readiness never creates temporary healthchecks, origin pools, load balancers, protected domains, or namespaces.

Mutation probes are outside readiness and are not a supported fallback. If capacity or access remains unknown, stop and obtain read-only administrator evidence; do not create or delete temporary resources.

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

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_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 xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/dns/namespaces/system/dns_zones/xXCSH_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 Configuration

Section titled “PF-T2-3: DNS Managed Records Configuration”

Only run if PF-T2-2 returned 200 (F5 XC DNS zone exists). Read and report the current setting; readiness must not update the shared DNS zone.

Terminal window
curl -sS \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/dns/namespaces/system/dns_zones/xXCSH_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 disabled; choose the documented external/manual DNS path or obtain separately approved DNS-owner change control" end)
}'

Do not automatically PUT the zone. A shared DNS change requires separate approval from its established owner, a reviewed complete spec containing only permitted metadata and spec fields, and post-change verification.

ResultDNS AuthorityStatusRemediation
trueAnyPASSLB-managed DNS records are enabled
false/nullF5 XCWARNUse external/manual DNS, or obtain separate DNS-owner approval for a reviewed change
false/nullExternalINFOUse DNS recovery guidance
Terminal window
NS_RECORDS=$(dig +short NS xXCSH_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 — use the DNS recovery guidance in the API Build phase"
end
)
}'

These checks verify the backend application is reachable.

Origin contract guard: validate the owner, representation, exclusivity, port, application marker, and placeholders before connectivity checks.

Terminal window
case "${XCSH_CSD_DEPLOYMENT_MODE:-}" in api|terraform) ;; *) echo "FAIL: invalid deployment mode"; exit 1;; esac
APPLICATION_MARKER=${XCSH_APPLICATION_MARKER:-}
[ -n "$APPLICATION_MARKER" ] || { echo "FAIL: application marker is required"; exit 1; }
case "$APPLICATION_MARKER" in replace-with-application-marker|replace-with-azure-application-marker) echo "FAIL: replace the application marker placeholder"; exit 1;; esac
case "${XCSH_ORIGIN_KIND:-}" in
public_name)
ORIGIN_VALUE=${XCSH_ORIGIN_HOSTNAME:-}
[ -z "${XCSH_ORIGIN_IP:-}" ] || { echo "FAIL: IP must be empty"; exit 1; }
;;
public_ip)
ORIGIN_VALUE=${XCSH_ORIGIN_IP:-}
[ -z "${XCSH_ORIGIN_HOSTNAME:-}" ] || { echo "FAIL: hostname must be empty"; exit 1; }
;;
*) echo "FAIL: invalid origin kind"; exit 1 ;;
esac
[ "$XCSH_CSD_DEPLOYMENT_MODE" != api ] || [ -n "$ORIGIN_VALUE" ] || { echo "FAIL: missing API origin"; exit 1; }
case "$ORIGIN_VALUE" in origin.example.com|192.0.2.*|198.51.100.*|203.0.113.*) echo "FAIL: example origin"; exit 1;; esac
case "${XCSH_ORIGIN_PORT:-}" in ''|*[!0-9]*) echo "FAIL: origin port must be an integer"; exit 1;; esac
[ "$XCSH_ORIGIN_PORT" -ge 1 ] && [ "$XCSH_ORIGIN_PORT" -le 65535 ] || { echo "FAIL: origin port out of range"; exit 1; }
Terminal window
HTTP_CODE=$(curl -s -o /dev/null -w '%\{http_code\}' --connect-timeout 10 --max-time 15 \
"http://$ORIGIN_VALUE:$XCSH_ORIGIN_PORT/")
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://$ORIGIN_VALUE:$XCSH_ORIGIN_PORT/" \
| 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

Establish ownership before any mutation. API mode expects the named resources to be absent, or to appear in the current run’s atomic ownership ledger. Terraform mode must use the configured backend and state and must not read or mutate those resources through the API workflow.

The API ledger must be executable data and updated atomically after every GET and POST result. Each entry contains an allowed resource kind, exact name, namespace, and one status: created, pre-existing, or unknown. There must be exactly one entry per kind/name/namespace. A 409 is always pre-existing, never created. Phase 3 also records every mitigated domain it creates.

Phase 4 derives deletion targets only from ledger entries marked created, fails if the ledger environment does not exactly match the active API URL and namespace, and requires explicit approval. Namespace cascade deletion requires a second, separate approval and is permitted only for a namespace recorded as created by this run.

Do not auto-teardown unknown, mixed-owner, pre-existing, fixed-name probe, or Terraform-owned resources. Readiness performs GET-only inspection and reports unresolved ownership as FAIL.

These checks determine whether the required HTTPS endpoint and automatic certificate are ready. A certificate failure blocks this one-load-balancer architecture; there is no HTTP-only fallback.

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 certificate issuance limits.

PF-T5-2: Existing Load Balancer Certificate State

Section titled “PF-T5-2: Existing Load Balancer Certificate State”

Run only if the load balancer exists from a prior API-owned deployment:

Terminal window
CERT_BODY=$(curl -s -w '\n%\{http_code\}' \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/http_loadbalancers/xXCSH_LB_NAMEx")
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 load balancer — assess certificate state after Phase 1"}'
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 "Certificate issuance rate limited — deployment not ready"
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 | 1 | PASS |
| PF-T1-0: Quota Usage Gate | **gate** | — | — | — | — | **PASS** |
| PF-T1-4: Protected Domain | — | — | — | — | — | UNKNOWN (administrator confirmation required) |
### 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 | independently supplied origin | PASS/WARN |
| PF-T3-2: HTML Content | observed response | PASS/WARN |
### T4: Ownership and Existing State
| Check | Result | Status |
|---|---|---|
| HTTP Load Balancer | 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 load balancer) | 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 unknown, mixed, or conflicting ownershipNOT READY — resolve ownership without mutation

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 and validate the environment before any API request:

  1. Read .env, then the shell environment.
  2. Require XCSH_CSD_DEPLOYMENT_MODE to equal api or terraform; this establishes the lifecycle owner.
  3. In API mode, require independently supplied origin values. Do not read Terraform or AWS state.
  4. Require XCSH_ORIGIN_KIND to equal public_name or public_ip.
  5. For public_name, require a non-placeholder XCSH_ORIGIN_HOSTNAME and require XCSH_ORIGIN_IP empty.
  6. For public_ip, require a non-placeholder XCSH_ORIGIN_IP and require XCSH_ORIGIN_HOSTNAME empty.
  7. Always require a non-empty, non-placeholder XCSH_APPLICATION_MARKER, independent of origin representation. The repository reference defaults to OWASP Juice Shop; non-reference API and Azure scenarios must set a stable, deployment-specific marker.
  8. Require XCSH_ORIGIN_PORT to be an integer from 1 through 65535. Reject example credentials, origin.example.com, and RFC 5737 TEST-NET addresses.
  9. Preserve an empty XCSH_HC_NAME; empty means omit the optional healthcheck.
  10. Display the resolved contract and obtain approval before mutation. Prepare may continue with GET-only checks without mutation approval.

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 (reject)
XCSH_CSD_DEPLOYMENT_MODEYesapianything except api or terraform
XCSH_API_TOKENAPI mode—example-api-token
XCSH_API_URLAPI mode—https://example-tenant.console.ves.volterra.io
XCSH_NAMESPACEYesclient-side-defense—
XCSH_DOMAINNAMEYesclient-side-defense.f5-sales-demo.com—
XCSH_ROOT_DOMAINYesf5-sales-demo.com—
XCSH_LB_NAMEYesclient-side-defense—
XCSH_EMAILYes—user@example.com
XCSH_ORIGIN_KINDAPI modepublic_nameanything except public_name or public_ip
XCSH_ORIGIN_HOSTNAMEConditional—origin.example.com
XCSH_ORIGIN_IPConditional—RFC 5737 TEST-NET addresses
XCSH_APPLICATION_MARKERYesOWASP Juice Shopempty, replace-with-application-marker, or replace-with-azure-application-marker
XCSH_ORIGIN_POOLYescsd-juice-shop—
XCSH_ORIGIN_PORTAPI mode80non-integer or outside 1–65535
XCSH_HC_NAMEOptionalempty (omit)—

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-juice-shop—
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 Record(observed deployed address)PASS
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 Administration → Credentials → API Credentials
  • curl and jq installed locally
  • Access to namespace client-side-defense with permissions for one origin pool, one HTTP load balancer, and the protected domain; healthcheck permission is optional

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 — environment and owner
XCSH_API_TOKEN=example-api-token
XCSH_API_URL=https://example-tenant.console.ves.volterra.io
XCSH_EMAIL=user@example.com
XCSH_CSD_DEPLOYMENT_MODE=api
XCSH_NAMESPACE=client-side-defense
XCSH_LB_NAME=client-side-defense
XCSH_DOMAINNAME=client-side-defense.f5-sales-demo.com
XCSH_ROOT_DOMAIN=f5-sales-demo.com
# API-owned origin: independent input, never Terraform/AWS state
XCSH_ORIGIN_KIND=public_name
XCSH_ORIGIN_HOSTNAME=origin.example.com
XCSH_ORIGIN_IP=
# Set a stable scenario-specific marker for non-reference API or Azure deployments.
XCSH_APPLICATION_MARKER=OWASP Juice Shop
XCSH_ORIGIN_POOL=csd-juice-shop
XCSH_ORIGIN_PORT=80
# Empty means omit the optional healthcheck
XCSH_HC_NAME=

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, xXCSH_API_TOKENx corresponds to $XCSH_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
xXCSH_CSD_DEPLOYMENT_MODExapiExclusive lifecycle owner: api or terraform
xXCSH_API_URLxhttps://example-tenant.console.ves.volterra.ioXC Console API URL; example value is rejected
xXCSH_API_TOKENxexample-api-tokenAPI credential token; example value is rejected
xXCSH_EMAILxuser@example.comCSD notification email; example value is rejected
xXCSH_NAMESPACExclient-side-defenseNamespace
xXCSH_LB_NAMExclient-side-defenseSingle HTTP Load Balancer name
xXCSH_DOMAINNAMExclient-side-defense.f5-sales-demo.comFQDN to protect
xXCSH_ROOT_DOMAINxf5-sales-demo.comRoot domain (eTLD+1)
xXCSH_ORIGIN_KINDxpublic_nameExactly public_name or public_ip
xXCSH_ORIGIN_HOSTNAMExorigin.example.comIndependent API origin hostname; example value is rejected
xXCSH_ORIGIN_IPxemptyIndependent API origin IP; TEST-NET values are rejected
xXCSH_APPLICATION_MARKERxOWASP Juice ShopRequired application/scenario marker, independent of origin representation; non-reference API and Azure scenarios must set a stable value
xXCSH_ORIGIN_POOLxcsd-juice-shopOrigin pool name
xXCSH_ORIGIN_PORTx80Integer origin port
xXCSH_HC_NAMExemptyOptional healthcheck; empty means omitted

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 XCSH_ for exported values
  3. Placeholder detection — flag unresolved credentials and provider-specific origin placeholders 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 — API Build: Deploy the optional healthcheck, provider-specific origin pool, and single HTTPS auto-certificate HTTP load balancer; configure DNS and CSD; then prove HTTP redirect, HTTPS service, application rendering, script injection, and collection traffic.
  2. Phase 2 — Attack: Run the authorized browser simulation over https://, then verify detections through the API.
  3. Phase 3 — Mitigate: Establish the baseline, apply mitigations, repeat the HTTPS simulation, and compare the result.
  4. Phase 4 — Teardown (requires explicit human confirmation): Delete the API-owned load balancer, origin pool, optional healthcheck, and protected domain in dependency order. Do not delete the DNS zone.
TokenDescriptionDefault
xXCSH_CSD_DEPLOYMENT_MODExExclusive lifecycle ownerapi
xXCSH_API_URLxXC Console API URL(user-provided)
xXCSH_API_TOKENxAPI credential token(user-provided)
xXCSH_EMAILxCSD notification email(user-provided)
xXCSH_NAMESPACExNamespaceclient-side-defense
xXCSH_LB_NAMExSingle HTTP Load Balancer nameclient-side-defense
xXCSH_DOMAINNAMExFQDN to protectclient-side-defense.f5-sales-demo.com
xXCSH_ROOT_DOMAINxRoot domain (eTLD+1)f5-sales-demo.com
xXCSH_ORIGIN_KINDxOrigin representationpublic_name
xXCSH_ORIGIN_HOSTNAMExAPI-owned hostname origin(required for public_name)
xXCSH_ORIGIN_IPxAPI-owned IP origin(required for public_ip)
xXCSH_APPLICATION_MARKERxApplication/scenario content marker, independent of origin representationOWASP Juice Shop; set a stable value for non-reference API and Azure scenarios
xXCSH_ORIGIN_POOLxOrigin pool namecsd-juice-shop
xXCSH_ORIGIN_PORTxInteger origin server port80
xXCSH_HC_NAMExOptional healthcheck nameempty (omit)

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:

The single ${XCSH_LB_NAME} resource uses https_auto_cert with http_redirect: true. It advertises on the public default VIP; HTTP requests redirect to the same HTTPS virtual host. Do not create a separate plaintext load balancer.

HTTPS auto-cert nested choices:

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 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 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, classify ownership with the exact detail GET; update only an API-owned object and never delete a pre-existing object as conflict recovery.
  • 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. Check capacity first with PF-T1-0. Behavior depends on the object type:
    • Healthcheck (limit ~150): Non-blocking — omit the optional healthcheck 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 the origin-pool detail GET omits the healthcheck reference and XCSH_HC_NAME is non-empty:

  1. Stop; do not delete or recreate anything during readiness.
  2. Verify the exact healthcheck with GET /api/config/namespaces/{namespace}/healthchecks/{hc_name}.
  3. During the approved API build, update only an API-owned origin pool using the documented payload and verify it with an exact detail GET. If XCSH_HC_NAME is empty, the omitted reference is valid and needs no remediation.

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

  1. Verify DNS zone exists (F5 XC managed DNS only):

    Terminal window
    curl -s \
    -H "Authorization: APIToken xXCSH_API_TOKENx" \
    "xXCSH_API_URLx/api/config/dns/namespaces/system/dns_zones/xXCSH_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 xXCSH_API_TOKENx" \
    "xXCSH_API_URLx/api/config/dns/namespaces/system/dns_zones/xXCSH_ROOT_DOMAINx" \
    | jq '.spec.primary.allow_http_lb_managed_records'

    If false or null, do not change it during readiness. Follow the DNS recovery guidance, and obtain separate approval from the established DNS-zone owner before any reviewed update.

  3. External DNS — if F5 XC is not authoritative, create the A and ACME CNAME records manually at your DNS provider (see the DNS recovery guidance).

  4. Verify resolution — after fixing DNS, confirm:

    Terminal window
    dig +short xXCSH_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 before the API build
  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 complete-path verification.

If cert_state reports AutoCertDomainRateLimited, DomainChallengePending, or PreDomainChallengePending, the required HTTPS endpoint is not ready. Stop the demo and resolve certificate issuance or DNS propagation before continuing; do not fall back to a separate HTTP load balancer.

For API-owned resources, retain the same namespace, domain, and object names. Correct DNS or wait for propagation, then retry the documented API update and verification steps. For Terraform-owned resources, retain the same backend and state, correct the input or external condition, and create, review, and apply a fresh saved plan. Do not edit or delete the resource in the console.

Validate Client-Side Defense payloads against the enriched Shape API reference and HTTP load balancer, origin pool, and healthcheck payloads against the enriched Virtual API reference.