Skip to content

Phase 1 — Build

Phase 1 deploys and validates the full CSD infrastructure. Complete all steps in order — each step must PASS before proceeding. Return to the index to complete Environment Setup and variable resolution before running these commands.

Step 0: Check & Create Namespace (Conditional)

Section titled “Step 0: Check & Create Namespace (Conditional)”

Check if the target namespace already exists on the tenant. If it does not exist, create it. Track the result in the NAMESPACE_CREATED shell variable — Phase 4 teardown uses this to decide whether to delete the namespace.

Terminal window
NS_CHECK=$(curl -s -o /dev/null -w '%\{http_code\}' \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/web/namespaces/xF5XC_NAMESPACEx")
NAMESPACE_CREATED="false"
if [ "$NS_CHECK" = "404" ]; then
curl -s -X POST \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
-H "Content-Type: application/json" \
-d '{"metadata": {"name": "xF5XC_NAMESPACEx"}, "spec": {}}' \
"xF5XC_API_URLx/api/web/namespaces" | jq .
NAMESPACE_CREATED="true"
fi

Confirm the namespace exists and record whether it was created:

Terminal window
curl -s -o /dev/null -w '%\{http_code\}' \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/web/namespaces/xF5XC_NAMESPACEx"
echo "NAMESPACE_CREATED=$NAMESPACE_CREATED"
FieldExpectedStatus
HTTP Status200PASS if returned, FAIL if 404 or other
NAMESPACE_CREATEDtrue (created) or false (pre-existing)Informational — used by Phase 4 teardown

Create an HTTP healthcheck that the origin pool can use to monitor backend health.

Terminal window
curl -s -X POST \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
-H "Content-Type: application/json" \
-d '{
"metadata": {
"name": "xF5XC_HC_NAMEx",
"namespace": "xF5XC_NAMESPACEx",
"labels": {},
"annotations": {},
"disable": false
},
"spec": {
"http_health_check": {
"use_origin_server_name": {},
"path": "/",
"use_http2": false,
"headers": {},
"request_headers_to_remove": [],
"expected_status_codes": ["200"]
},
"timeout": 3,
"interval": 15,
"jitter": 0,
"unhealthy_threshold": 1,
"healthy_threshold": 3,
"jitter_percent": 30
}
}' \
"xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks" \
| jq .

A 200 response with the created object confirms the healthcheck was created. If the response contains "code": 8 with a message like "Object kind healthcheck has exhausted limits(150)", the tenant has hit its healthcheck limit — skip to Step 2 and omit the healthcheck reference. CSD does not depend on health monitoring.

Confirm the healthcheck exists:

Terminal window
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks/xF5XC_HC_NAMEx" \
| jq '{name: .metadata.name, namespace: .metadata.namespace, path: .spec.http_health_check.path, interval: .spec.interval}'
FieldExpectedStatus
HTTP Status200 with objectPASS if returned, FAIL if 404
nameMatches F5XC_HC_NAMEPASS
path/PASS
Step 1 skipped (error code 8, limit exhausted)PASS (healthcheck is optional for CSD)

Create an origin pool pointing to your backend server. If you created a healthcheck in Step 1, include the healthcheck reference. If Step 1 was skipped, use an empty array.

With healthcheck (Step 1 succeeded):

Terminal window
curl -s -X POST \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
-H "Content-Type: application/json" \
-d '{
"metadata": {
"name": "xF5XC_ORIGIN_POOLx",
"namespace": "xF5XC_NAMESPACEx",
"labels": {},
"annotations": {},
"description": "Origin pool for CSD demo",
"disable": false
},
"spec": {
"origin_servers": [{
"public_ip": { "ip": "xF5XC_ORIGIN_IPx" },
"labels": {}
}],
"no_tls": {},
"port": xF5XC_ORIGIN_PORTx,
"same_as_endpoint_port": {},
"healthcheck": [{
"namespace": "xF5XC_NAMESPACEx",
"name": "xF5XC_HC_NAMEx",
"kind": "healthcheck"
}],
"loadbalancer_algorithm": "LB_OVERRIDE",
"endpoint_selection": "LOCAL_PREFERRED"
}
}' \
"xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools" \
| jq .

Without healthcheck (Step 1 skipped):

Terminal window
curl -s -X POST \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
-H "Content-Type: application/json" \
-d '{
"metadata": {
"name": "xF5XC_ORIGIN_POOLx",
"namespace": "xF5XC_NAMESPACEx",
"labels": {},
"annotations": {},
"description": "Origin pool for CSD demo",
"disable": false
},
"spec": {
"origin_servers": [{
"public_ip": { "ip": "xF5XC_ORIGIN_IPx" },
"labels": {}
}],
"no_tls": {},
"port": xF5XC_ORIGIN_PORTx,
"same_as_endpoint_port": {},
"healthcheck": [],
"loadbalancer_algorithm": "LB_OVERRIDE",
"endpoint_selection": "LOCAL_PREFERRED"
}
}' \
"xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools" \
| jq .

A 200 response confirms the origin pool was created.

If you included a healthcheck reference, confirm it is properly linked:

Terminal window
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools/xF5XC_ORIGIN_POOLx" \
| jq '.spec.healthcheck'

A populated array confirms the link. An empty array [] is expected if Step 1 was skipped, or means the healthcheck was not found if you intended to link one.

Confirm the origin pool exists and has the correct configuration:

Terminal window
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools/xF5XC_ORIGIN_POOLx" \
| jq '{name: .metadata.name, origin_ip: .spec.origin_servers[0].public_ip.ip, port: .spec.port, healthcheck_count: (.spec.healthcheck | length)}'
FieldExpectedStatus
HTTP Status200PASS if returned, FAIL if 404
nameMatches F5XC_ORIGIN_POOLPASS
origin_ipMatches F5XC_ORIGIN_IPPASS
portMatches F5XC_ORIGIN_PORTPASS
healthcheck_count1 (with HC) or 0 (without)PASS either way

Step 3: Create HTTP Load Balancers with CSD

Section titled “Step 3: Create HTTP Load Balancers with CSD”

Create two load balancers with Client-Side Defense enabled — an HTTP LB (primary, port 80) and an HTTPS LB (secondary, port 443 with automatic certificate management). The HTTP LB is the default for all demo traffic. The HTTPS LB is a nice to have that depends on Let’s Encrypt certificate provisioning, which can hit rate limits in demo environments.

This is the primary load balancer for demos. It uses an http listener on port 80 with F5 XC managed DNS. It does not depend on TLS certificate provisioning, so it becomes ready immediately after DNS resolves.

Terminal window
curl -s -X POST \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
-H "Content-Type: application/json" \
-d '{
"metadata": {
"name": "xF5XC_LB_NAMEx-http",
"namespace": "xF5XC_NAMESPACEx",
"labels": {},
"annotations": {},
"description": "HTTP LB with Client-Side Defense enabled (primary demo LB)",
"disable": false
},
"spec": {
"domains": ["xF5XC_DOMAINNAMEx"],
"http": {
"dns_volterra_managed": true,
"port": 80
},
"advertise_on_public_default_vip": {},
"default_route_pools": [{
"pool": {
"namespace": "xF5XC_NAMESPACEx",
"name": "xF5XC_ORIGIN_POOLx",
"kind": "origin_pool"
},
"weight": 1,
"priority": 1
}],
"client_side_defense": {
"policy": {
"js_insert_all_pages": {}
}
},
"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" \
| jq .

A 200 response confirms the HTTP load balancer was created with CSD enabled.

This is the secondary load balancer. It uses https_auto_cert on port 443 with automatic Let’s Encrypt certificate provisioning. Before creating it, check if a skeleton HTTPS LB exists from a previous teardown — if so, restore it via PUT instead of POST to preserve the existing Let’s Encrypt certificate and avoid rate limits (5 duplicate certificates per exact identifier set per 7 days).

Check if the HTTPS LB already exists:

Terminal window
HTTPS_CHECK=$(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")

If HTTPS_CHECK is 200, a skeleton HTTPS LB exists — use Path A (PUT). If 404, use Path B (POST).

Path A: Restore Skeleton via PUT (HTTPS LB exists)

Section titled “Path A: Restore Skeleton via PUT (HTTPS LB exists)”

When a skeleton HTTPS LB exists from a prior teardown, restore the full configuration via PUT. This re-attaches the origin pool and re-enables CSD without triggering a new Let’s Encrypt certificate request — the existing certificate remains valid.

Terminal window
curl -s -X PUT \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
-H "Content-Type: application/json" \
-d '{
"metadata": {
"name": "xF5XC_LB_NAMEx-https",
"namespace": "xF5XC_NAMESPACEx",
"labels": {},
"annotations": {},
"description": "HTTPS LB with Client-Side Defense enabled (secondary, cert-dependent)",
"disable": false
},
"spec": {
"domains": ["xF5XC_DOMAINNAMEx"],
"https_auto_cert": {
"http_redirect": false,
"add_hsts": false,
"port": 443,
"default_header": {},
"enable_path_normalize": {},
"no_mtls": {},
"default_loadbalancer": {}
},
"advertise_on_public_default_vip": {},
"default_route_pools": [{
"pool": {
"namespace": "xF5XC_NAMESPACEx",
"name": "xF5XC_ORIGIN_POOLx",
"kind": "origin_pool"
},
"weight": 1,
"priority": 1
}],
"client_side_defense": {
"policy": {
"js_insert_all_pages": {}
}
},
"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/xF5XC_LB_NAMEx-https" \
| jq .

A 200 response confirms the skeleton was restored with the full configuration. The certificate state should remain CertificateValid — no new Let’s Encrypt provisioning is triggered.

Path B: Create New via POST (HTTPS LB does not exist)

Section titled “Path B: Create New via POST (HTTPS LB does not exist)”

When no HTTPS LB exists (first run or after a full teardown), create it via POST. This triggers Let’s Encrypt certificate provisioning, which may take 5–10 minutes.

Terminal window
curl -s -X POST \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
-H "Content-Type: application/json" \
-d '{
"metadata": {
"name": "xF5XC_LB_NAMEx-https",
"namespace": "xF5XC_NAMESPACEx",
"labels": {},
"annotations": {},
"description": "HTTPS LB with Client-Side Defense enabled (secondary, cert-dependent)",
"disable": false
},
"spec": {
"domains": ["xF5XC_DOMAINNAMEx"],
"https_auto_cert": {
"http_redirect": false,
"add_hsts": false,
"port": 443,
"default_header": {},
"enable_path_normalize": {},
"no_mtls": {},
"default_loadbalancer": {}
},
"advertise_on_public_default_vip": {},
"default_route_pools": [{
"pool": {
"namespace": "xF5XC_NAMESPACEx",
"name": "xF5XC_ORIGIN_POOLx",
"kind": "origin_pool"
},
"weight": 1,
"priority": 1
}],
"client_side_defense": {
"policy": {
"js_insert_all_pages": {}
}
},
"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" \
| jq .

A 200 response confirms the HTTPS load balancer was created. Certificate provisioning begins automatically.

Confirm both load balancers exist with CSD enabled. Always use a GET for evidence — the POST response returns transient state values that may not reflect the settled configuration.

HTTP LB (primary):

Terminal window
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-http" \
| jq '{name: .metadata.name, domains: .spec.domains, csd_enabled: (.spec.client_side_defense != null), state: .spec.state}'
FieldExpectedStatus
HTTP Status200PASS if returned, FAIL if 404
name${F5XC_LB_NAME}-httpPASS
domainsContains F5XC_DOMAINNAMEPASS
csd_enabledtruePASS — CSD is configured on this LB
stateVIRTUAL_HOST_READY or VIRTUAL_HOST_PENDING_A_RECORDExpected — HTTP LB transitions to READY once DNS resolves. Any other state (e.g. VIRTUAL_HOST_FAILED) is a FAIL — report to operator and do not proceed

HTTPS LB (secondary):

Terminal window
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-https" \
| jq '{name: .metadata.name, domains: .spec.domains, csd_enabled: (.spec.client_side_defense != null), state: .spec.state, cert_state: .spec.cert_state}'
FieldExpectedStatus
HTTP Status200PASS if returned, FAIL if 404
name${F5XC_LB_NAME}-httpsPASS
domainsContains F5XC_DOMAINNAMEPASS
csd_enabledtruePASS — CSD is configured on this LB
creation_methodPUT (restored skeleton) or POST (new)INFO — PUT preserves existing certificate
stateVIRTUAL_HOST_READY (PUT) or VIRTUAL_HOST_PENDING_A_RECORD (POST)Expected — PUT path may already be READY since DNS persisted from skeleton
cert_stateCertificateValid (PUT) or PreDomainChallengePending (POST)PUT preserves existing cert; POST triggers new provisioning

After creating the load balancer, it enters VIRTUAL_HOST_PENDING_A_RECORD status and the automatic certificate stays in PreDomainChallengePending. Two DNS records must exist before the LB becomes fully operational:

  1. A recordxF5XC_DOMAINNAMEx pointing to the VIP IP address (from dns_info in the LB response)
  2. ACME challenge record_acme-challenge.xF5XC_DOMAINNAMEx for TLS certificate validation (managed automatically when using F5 XC DNS with managed records; manual CNAME required for external DNS)

The approach depends on whether F5 XC is the authoritative DNS provider for your domain.

Check the nameservers for your root domain:

Terminal window
dig +short NS xF5XC_ROOT_DOMAINx

If the response includes ns1.f5clouddns.com and ns2.f5clouddns.com, F5 XC is the authoritative DNS provider — follow Option A. Otherwise, follow Option B for external DNS.

When F5 XC is the authoritative DNS provider, the platform can automatically create both the A record and the ACME challenge CNAME — but only when the DNS zone has allow_http_lb_managed_records enabled.

1. Check if managed records are enabled:

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 the response is true, the platform will auto-create DNS records for load balancers — skip to 3. Verify DNS resolution. If false or null, continue to the next step.

2. Enable managed records:

Retrieve the current zone configuration, enable managed records, and update:

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 .

A 200 response (empty \{\}) confirms the zone was updated. F5 XC will create the A record and ACME challenge record in the zone’s auto-managed record group. If the managed records do not appear within 60 seconds, re-apply the load balancer to trigger record creation:

Terminal window
LB_CONFIG=$(curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-http")
echo "$LB_CONFIG" | curl -s -X PUT \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
-H "Content-Type: application/json" \
-d @- \
"xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-http" \
| jq .

This GET+PUT re-apply does not change the LB configuration — it simply triggers the platform to re-evaluate the DNS zone and create managed records. If the A record still does not resolve within 60 seconds after the re-apply, stop and report to the operator — do not retry the re-apply more than once.

3. Verify DNS resolution:

Terminal window
dig +short xF5XC_DOMAINNAMEx A

The response should return the VIP IP address. DNS propagation is typically immediate for F5 XC managed zones, but allow up to 60 seconds.

When your domain uses an external DNS provider, you must create the records manually. Extract the required values from the load balancer:

Terminal window
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-http" \
| jq '{
vip_ip: .spec.dns_info[0].ip_address,
acme_target: .spec.auto_cert_info.dns_records
}'

Create these records at your DNS provider:

TypeNameValue
AxF5XC_DOMAINNAMExVIP IP from dns_info[0].ip_address
CNAME_acme-challenge.xF5XC_DOMAINNAMEx*.autocerts.ves.volterra.io

After creating the records, verify resolution:

Terminal window
dig +short xF5XC_DOMAINNAMEx A
dig +short _acme-challenge.xF5XC_DOMAINNAMEx CNAME
TestCommandExpectedStatus
DNS-1: A Recorddig +short $F5XC_DOMAINNAME AVIP IP address returnedPASS if IP returned, FAIL if empty
DNS-2: ACME CNAMEdig +short _acme-challenge.$F5XC_DOMAINNAME CNAME*.autocerts.ves.volterra.ioPASS if CNAME returned
DNS authoritydig +short NS $F5XC_ROOT_DOMAINF5 XC or external nameserversInformational — determines Option A vs B

If DNS-1 returns empty after 60 seconds, see Troubleshooting — LB Stuck in VIRTUAL_HOST_PENDING_A_RECORD.

Check that Client-Side Defense is enabled for the tenant. CSD is configured at the tenant level — if it has not been enabled yet, contact your F5 XC administrator.

Terminal window
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/status" \
| jq .

A response containing "isConfigured": true and "isEnabled": true confirms CSD is active.

Terminal window
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/status" \
| jq '{configured: .isConfigured, enabled: .isEnabled}'
FieldExpectedStatus
configuredtruePASS
enabledtruePASS
Either is falseFAIL — contact F5 XC administrator to enable CSD at tenant level

Register the root domain that CSD will monitor. The protected_domain field must be the eTLD+1 root domain (e.g., f5demos.com), not the full FQDN.

Before creating, check whether the domain is already registered on the tenant. A 409 response to the POST means the domain already exists — this is a success condition, not an error.

Terminal window
curl -s -X POST \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
-H "Content-Type: application/json" \
-d '{
"metadata": {
"name": "xF5XC_DOMAINNAMEx",
"namespace": "xF5XC_NAMESPACEx"
},
"spec": {
"protected_domain": "xF5XC_ROOT_DOMAINx"
}
}' \
"xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains" \
| jq .
ResponseMeaningAction
200Domain registered successfullyContinue to Step 7
409 (domain already exists)Domain was previously registered on this tenantAlready done — continue to Step 7
"code": 8 (exhausted limits)Protected domain quota fullBlocking — protected domains are required for CSD. Delete unused protected domains or contact your administrator.

The POST response itself is the primary evidence — it returns the registered domain object with spec.protected_domain matching your root domain. Alternatively, list all protected domains to confirm:

Terminal window
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains" \
| jq '.items | length'
FieldExpectedStatus
POST returned protected_domainMatches F5XC_ROOT_DOMAINPASS
POST returned 200 or 409Domain registered or already existsPASS
List items count> 0PASS

Confirm the A record is resolving and the ACME challenge CNAME is in place:

Terminal window
dig +short xF5XC_DOMAINNAMEx A
dig +short _acme-challenge.xF5XC_DOMAINNAMEx CNAME

The A record should return the VIP IP address. The CNAME should point to *.autocerts.ves.volterra.io (or a related autocerts target).

Check that both load balancers have transitioned out of their pending states. The HTTP LB is the primary check — it should reach VIRTUAL_HOST_READY once DNS resolves, with no certificate dependency. The HTTPS LB certificate state is informational only.

HTTP LB (primary — must be READY to proceed):

Terminal window
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-http" \
| jq '{state: .spec.state}'
FieldExpectedIntermediate States
stateVIRTUAL_HOST_READYVIRTUAL_HOST_PENDING_A_RECORD — DNS not configured (see Step 4)

HTTPS LB (secondary — informational, does not block progression):

Terminal window
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-https" \
| jq '{state: .spec.state, cert_state: .spec.cert_state}'
FieldExpectedIntermediate States
stateVIRTUAL_HOST_READYVIRTUAL_HOST_PENDING_A_RECORD — DNS not configured; VIRTUAL_HOST_DNS_A_RECORD_ADDED — A record found, waiting for cert
cert_stateCertificateValidPreDomainChallengePending — waiting for ACME CNAME; DomainChallengeStarted — ACME challenge in progress; AutoCertDomainRateLimited — Let’s Encrypt rate limit hit (expected in demo environments, does not affect HTTP LB)
Terminal window
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/js_configuration" \
| jq .

The response contains a scriptTag field with the full HTML <script> tag that the load balancer injects into page responses.

Terminal window
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/detected_domains" \
| jq '{summary: .domain_summary, domains: .domains_list}'

The scripts endpoint requires a time range using epoch timestamps (seconds since Unix epoch). The example below queries the last 7 days.

Terminal window
NOW=$(date +%s)
START=$(( NOW - 604800 ))
curl -s -X POST \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
-H "Content-Type: application/json" \
-d "{\"startTime\": \"$START\", \"endTime\": \"$NOW\"}" \
"xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/scripts" \
| jq '[.scripts[]? | {script_name: .script_name, risk_level: .risk_level}]'

The form fields endpoint requires a time range using epoch timestamps, passed as query parameters.

Terminal window
NOW=$(date +%s)
START=$(( NOW - 604800 ))
curl -s \
-H "Authorization: APIToken xF5XC_API_TOKENx" \
"xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/formFields?startTime=$START&endTime=$NOW" \
| jq '.form_fields'

After completing all verification checks, the AI assistant should present a consolidated status table:

Test IDCheckExpectedRequiredStatus
DNS-1A Record resolvesVIP IP returnedYesPASS / FAIL
DNS-2ACME CNAME exists*.autocerts.ves.volterra.ioNoPASS / PENDING
LB-1HTTP LB stateVIRTUAL_HOST_READYYesPASS / PENDING
LB-2HTTPS LB stateVIRTUAL_HOST_READYNoPASS / PENDING / INFO
TLS-1Certificate stateCertificateValidNoPASS / PENDING / INFO
CSD-1JS configurationscriptTag presentYesPASS / FAIL
CSD-2CSD statusisEnabled: trueYesPASS / FAIL
CSD-3Protected domainDomain registeredYesPASS / FAIL

If LB-1 shows PENDING, poll every 30 seconds for up to 4 iterations (2 minutes total). If LB-1 is still not VIRTUAL_HOST_READY after 4 iterations, check DNS resolution with dig and report to operator — do not proceed until LB-1 reaches READY. For LB-2 and TLS-1, poll every 60 seconds for up to 10 iterations (10 minutes). If still in an intermediate state after 10 iterations, record the current state as INFO and proceed — these are informational and do not block demo progression.


Phase 1 complete. Proceed to Phase 2 — Attack to run the attack simulation.