Skip to content

Diagnostics & Verification

This page provides a layered UAT verification matrix for validating your Client-Side Defense deployment end-to-end. Each test case follows the infrastructure dependency chain — from DNS resolution through CSD telemetry — so you can systematically prove every component is working correctly.

These commands are the API equivalent of the CSD Console Walkthrough — use them when you need to verify from the terminal, automate monitoring, or demonstrate CSD capabilities without the UI.

Set up your environment variables as described in API Automation — Environment Setup:

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

All commands below use the xTOKENx placeholder format. Substitute with your environment variables ($XCSH_API_TOKEN, $XCSH_NAMESPACE, etc.) or use the interactive form at the top of the page.

Many CSD endpoints require epoch timestamps (seconds since Unix epoch). These one-liners compute start/end times for common ranges.

Cross-platform (macOS + Linux):

Terminal window
# Current time as epoch seconds
NOW=$(date +%s)
# 1 hour ago
START_1H=$(( NOW - 3600 ))
# 24 hours ago
START_24H=$(( NOW - 86400 ))
# 7 days ago
START_7D=$(( NOW - 604800 ))
# 30 days ago
START_30D=$(( NOW - 2592000 ))
PresetSecondsShell expression
1 hour3,600$(( $(date +%s) - 3600 ))
24 hours86,400$(( $(date +%s) - 86400 ))
7 days604,800$(( $(date +%s) - 604800 ))
30 days2,592,000$(( $(date +%s) - 2592000 ))

Each test below follows this structure:

FieldDescription
Test IDLayer number + sequential ID (e.g., DNS-1, TLS-2)
What it provesThe specific infrastructure fact being verified
CommandReady-to-run curl or dig command
PASS / FAILExpected output for healthy vs. unhealthy state
FixLink to the relevant setup or troubleshooting section

DNS is the foundation — if the domain does not resolve to the load balancer VIP, nothing else works.

DNS-1: A Records Match the Current LB VIPs

Section titled “DNS-1: A Records Match the Current LB VIPs”

What it proves: Every A record for the exact protected domain resolves to a VIP currently reported by the exact load balancer in the expected namespace.

Terminal window
LB=$(curl -fsS \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/http_loadbalancers/xXCSH_LB_NAMEx")
DNS_A=$(dig +short xXCSH_DOMAINNAMEx A | sort -u)
LB_VIPS=$(printf '%s' "$LB" | jq -r '.spec.dns_info[]?.ip_address // empty' | sort -u)
printf '%s' "$LB" | jq -e \
--arg name "xXCSH_LB_NAMEx" \
--arg namespace "xXCSH_NAMESPACEx" \
--arg domain "xXCSH_DOMAINNAMEx" \
'.metadata.name == $name and .metadata.namespace == $namespace and (.spec.domains | index($domain) != null)' >/dev/null
test -n "$DNS_A"
test "$DNS_A" = "$LB_VIPS"
ResultMeaning
PASS — identity/domain assertion succeeds and the non-empty sorted A-record set exactly equals the current LB VIP setDNS names the intended LB and no stale or foreign VIP is present
FAIL — identity/domain mismatch, empty set, or unequal setsCorrect the domain, namespace, LB selection, or DNS records before continuing

What it proves: The exact challenge owner has the record type and value required by the selected DNS ownership mode.

Terminal window
ACME_OWNER="_acme-challenge.xXCSH_DOMAINNAMEx"
ACME_CNAME=$(dig +short "$ACME_OWNER" CNAME)
ACME_TXT=$(dig +short "$ACME_OWNER" TXT)
printf 'owner=%s\nCNAME=%s\nTXT=%s\n' "$ACME_OWNER" "$ACME_CNAME" "$ACME_TXT"
ResultMeaning
PASS (external DNS) — the exact owner has the configured F5 automatic-certificate CNAME targetExternal ACME delegation is present
PASS (F5 Distributed Cloud managed DNS) — the exact owner has the platform-managed TXT value and no conflicting external CNAMEManaged ACME validation is present
FAIL — expected value missing, wrong owner, conflicting records, or both emptyCertificate validation is not proven

Do not accept any non-empty CNAME or TXT as PASS. Compare the observed value with the value reported for this domain by the current LB certificate metadata or the DNS configuration. For external DNS, create the exact CNAME instructed by that metadata. For managed DNS, enable allow_http_lb_managed_records and verify the platform-managed TXT record.

What it proves: Whether F5 XC is the authoritative DNS provider for the root domain.

Terminal window
dig +short NS xXCSH_ROOT_DOMAINx
ResultMeaning
Includes ns1.f5clouddns.com and ns2.f5clouddns.comF5 XC managed DNS — records can be auto-created
Other nameserversExternal DNS — records must be created manually

Fix: Phase 1 — DNS, Certificate, and Origin Recovery

What it proves: The F5 XC DNS zone allows automatic record creation for load balancers (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 '.spec.primary.allow_http_lb_managed_records'
ResultMeaning
PASS — truePlatform will auto-create A and ACME records for LBs
FAIL — false or nullManaged records disabled; enable via zone update

Fix: Phase 1 — DNS, Certificate, and Origin Recovery


The single load balancer named by XCSH_LB_NAME terminates HTTPS and redirects HTTP to HTTPS. HTTP is redirect behavior on this object, not a second load balancer or a fallback path.

What it proves: The automatic TLS certificate has been issued on the load balancer.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/http_loadbalancers/xXCSH_LB_NAMEx" \
| jq '.spec.cert_state'
ResultMeaning
PASS — "CertificateValid" or "AutoCertRenewing"Certificate is valid and active
WAIT — "DomainChallengePending" or "DomainChallengeStarted"ACME challenge is still progressing
FAIL — "PreDomainChallengePending" or "AutoCertDomainRateLimited"HTTPS is not ready; resolve the ACME record or rate limit before end-to-end validation

What it proves: The certificate metadata and ACME records correspond to the configured domain.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/http_loadbalancers/xXCSH_LB_NAMEx" \
| jq '{
cert_state: .spec.cert_state,
auto_cert_info: .spec.auto_cert_info,
vip_ip: .spec.dns_info[0].ip_address
}'

What it proves: A client can complete TLS to the protected domain with a valid certificate.

Terminal window
curl -sv "https://xXCSH_DOMAINNAMEx/" 2>&1 \
| grep -E 'SSL connection|subject:|expire date:|issuer:'
ResultMeaning
PASS — TLS connection, expected subject, and valid datesClient-to-F5 XC TLS is ready
FAIL — connection or certificate errorCheck DNS, LB virtual-host state, and certificate state

All checks in this layer read the same unsuffixed load balancer object.

What it proves: The virtual host is deployed and ready.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/http_loadbalancers/xXCSH_LB_NAMEx" \
| jq '.spec.state'
ResultMeaning
PASS — "VIRTUAL_HOST_READY"LB virtual host is operational
FAIL — pending stateInspect DNS, certificate state, and virtual-host error details

LB-2: Domain and HTTPS Redirect Configuration

Section titled “LB-2: Domain and HTTPS Redirect Configuration”

What it proves: The LB serves the expected domain with automatic HTTPS and HTTP redirect enabled.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/http_loadbalancers/xXCSH_LB_NAMEx" \
| jq '{domains: .spec.domains, https_auto_cert: .spec.https_auto_cert, http_redirect: .spec.https_auto_cert.http_redirect}'
ResultMeaning
PASS — expected domain and http_redirect: trueOne LB owns HTTPS and redirects HTTP
FAIL — missing domain, HTTPS, or redirectRemediate through the selected ownership mode

What it proves: Client-Side Defense injects JavaScript on all pages.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/http_loadbalancers/xXCSH_LB_NAMEx" \
| jq '{
csd_enabled: (if .spec.client_side_defense then true else false end),
js_policy: .spec.client_side_defense.policy
}'
ResultMeaning
PASS — csd_enabled: true with js_insert_all_pagesCSD injection is configured for all pages
FAIL — absent CSD configurationRemediate through the selected ownership mode

What it proves: The LB references the expected origin pool.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/http_loadbalancers/xXCSH_LB_NAMEx" \
| jq '.spec.default_route_pools[] | {pool: .pool.name, namespace: .pool.namespace, weight, priority}'

What it proves: The virtual host, VIP, certificate, and route configuration are reported by the same LB object.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/http_loadbalancers/xXCSH_LB_NAMEx" \
| jq '{
name: .metadata.name,
state: .spec.state,
cert_state: .spec.cert_state,
domains: .spec.domains,
dns_info: .spec.dns_info,
http_redirect: .spec.https_auto_cert.http_redirect,
csd_enabled: (if .spec.client_side_defense then true else false end),
route_pools: [.spec.default_route_pools[] | .pool.name],
virtual_host_errors: [.status[]? | select(.virtual_host_status != null) | .virtual_host_status | {state, error_description, suggested_action}]
}'

The origin pool supports either an AWS ALB hostname (public_name) or an Azure/public lab IP (public_ip). Set XCSH_ORIGIN_KIND to public_name or public_ip; set the matching XCSH_ORIGIN_HOSTNAME or XCSH_ORIGIN_IP variable.

What it proves: The configured oneOf origin kind and value match the intended backend.

Terminal window
ORIGIN_POOL=$(curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/origin_pools/xXCSH_ORIGIN_POOLx")
case "xXCSH_ORIGIN_KINDx" in
public_name)
echo "$ORIGIN_POOL" | jq --arg expected "xXCSH_ORIGIN_HOSTNAMEx" '{
name: .metadata.name,
origin_kind: "public_name",
origin: [.spec.origin_servers[] | .public_name.dns_name],
expected: $expected,
port: .spec.port
}'
;;
public_ip)
echo "$ORIGIN_POOL" | jq --arg expected "xXCSH_ORIGIN_IPx" '{
name: .metadata.name,
origin_kind: "public_ip",
origin: [.spec.origin_servers[] | .public_ip.ip],
expected: $expected,
port: .spec.port
}'
;;
*) echo "FAIL: XCSH_ORIGIN_KIND must be public_name or public_ip" >&2; exit 1 ;;
esac
ResultMeaning
PASS — origin contains the matching expected hostname or IP and the expected portOrigin pool oneOf choice is correct
FAIL — wrong kind, value, or portRemediate the origin pool through its selected owner
Terminal window
echo "$ORIGIN_POOL" | jq '{
tls_config: (if .spec.no_tls then "no_tls (plaintext)" elif .spec.use_tls then "use_tls (encrypted)" else "unknown" end)
}'
Terminal window
echo "$ORIGIN_POOL" | jq '.spec.healthcheck'

An empty array is valid when no optional healthcheck was selected.

What it proves: The origin responds from the operator’s current network. It does not prove F5 XC-to-origin health.

Terminal window
case "xXCSH_ORIGIN_KINDx" in
public_name) ORIGIN_URL="http://xXCSH_ORIGIN_HOSTNAMEx:xXCSH_ORIGIN_PORTx/" ;;
public_ip) ORIGIN_URL="http://xXCSH_ORIGIN_IPx:xXCSH_ORIGIN_PORTx/" ;;
*) echo "FAIL: invalid XCSH_ORIGIN_KIND" >&2; exit 1 ;;
esac
curl -sS -o /dev/null -w '%{http_code}\n' "$ORIGIN_URL"

Health checks monitor backend availability. They are optional for CSD but useful for production deployments.

What it proves: The healthcheck exists with the correct type, path, timing, and thresholds.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/healthchecks/xXCSH_HC_NAMEx" \
| jq '{
name: .metadata.name,
type: (if .spec.http_health_check then "HTTP" elif .spec.tcp_health_check then "TCP" else "unknown" end),
path: .spec.http_health_check.path,
expected_status: .spec.http_health_check.expected_status_codes,
timeout: .spec.timeout,
interval: .spec.interval,
unhealthy_threshold: .spec.unhealthy_threshold,
healthy_threshold: .spec.healthy_threshold
}'
ResultMeaning
PASS — shows HTTP type with path / and expected 200Healthcheck configured correctly
FAIL — 404 responseHealthcheck does not exist (it may have been omitted; see Phase 1 Step 3)

What it proves: Enumerates all healthchecks in the namespace to verify naming and count.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/healthchecks" \
| jq -r '
["NAME", "NAMESPACE", "DESCRIPTION"],
(.items[] | [
.name,
.namespace,
(.description | if length == 0 then "—" else . end)
])
| @tsv' | column -t

CSD must be enabled at the tenant level and have the JavaScript injection tag configured.

What it proves: CSD is configured and enabled for the tenant.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/status" \
| jq '{isConfigured, isEnabled}'
ResultMeaning
PASS — both trueCSD is active for this tenant
FAIL — isConfigured: falseCSD not enabled at tenant level — contact F5 XC administrator
FAIL — isEnabled: falseCSD configured but not active

What it proves: The CSD JavaScript injection tag is generated and ready for injection.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/js_configuration" \
| jq '{has_script_tag: (.scriptTag | length > 0)}'
ResultMeaning
PASS — has_script_tag: trueJS injection tag is configured
FAIL — has_script_tag: falseNo script tag — verify CSD is enabled and a protected domain is registered

What it proves: Shows the full script tag for verification or manual injection.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/js_configuration" \
| jq '.scriptTag'

What it proves: The exact protected-domain name and namespace match the expected application.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/protected_domains" \
| jq -e --arg name "xXCSH_DOMAINNAMEx" --arg namespace "xXCSH_NAMESPACEx" \
'.items[] | select(.name == $name and .namespace == $namespace) | {name, namespace, description}'
ResultMeaning
PASS — an item exactly matches both XCSH_DOMAINNAME and XCSH_NAMESPACEThe intended domain is registered in the intended namespace
FAIL — no exact matchA different or merely non-empty item does not prove protection for this application

CSD-5: Live Application and JS Injection Verification

Section titled “CSD-5: Live Application and JS Injection Verification”

What it proves: The protected HTTPS response is the expected scenario application and contains the injected CSD path. XCSH_CSD_DEPLOYMENT_MODE selects ownership (api or terraform) only; it never selects AWS or Azure evidence.

Terminal window
case "xXCSH_CSD_DEPLOYMENT_MODEx" in
api|terraform) ;;
*) echo "FAIL: XCSH_CSD_DEPLOYMENT_MODE must be api or terraform" >&2; exit 1 ;;
esac
APPLICATION_MARKER='xXCSH_APPLICATION_MARKERx'
test -n "$APPLICATION_MARKER" || { echo "FAIL: XCSH_APPLICATION_MARKER is required for every scenario" >&2; exit 1; }
case "$APPLICATION_MARKER" in
'replace-with-application-marker'|'replace-with-azure-application-marker'|'xXCSH_APPLICATION_MARKERx')
echo "FAIL: replace the XCSH_APPLICATION_MARKER documentation placeholder" >&2
exit 1
;;
esac
APPLICATION_HTML=$(curl -fsS "https://xXCSH_DOMAINNAMEx/")
printf '%s' "$APPLICATION_HTML" | grep -Fqi "$APPLICATION_MARKER" \
&& echo "PASS: expected application rendered" \
|| { echo "FAIL: expected application marker missing" >&2; exit 1; }
printf '%s' "$APPLICATION_HTML" | grep -q '__imp_apg__' \
&& echo "PASS: __imp_apg__ injected" \
|| { echo "FAIL: __imp_apg__ missing" >&2; exit 1; }

Both assertions must succeed. XCSH_APPLICATION_MARKER always identifies the expected application/scenario independently of XCSH_ORIGIN_KIND. The repository reference defaults to OWASP Juice Shop; every non-reference API or Azure scenario must supply its own stable, deployment-specific marker. Empty and documentation-placeholder values fail closed. XCSH_CSD_DEPLOYMENT_MODE continues to identify ownership only.

An HTTPS status alone does not identify the intended origin, and direct-origin reachability does not prove the F5 path.


Verify that live traffic is reaching the load balancer and being processed correctly.

What it proves: Traffic is reaching the load balancer. Zero results means no traffic is arriving.

Terminal window
curl -s -X POST \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
-H "Content-Type: application/json" \
-d '{
"start_time": "'"$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-24H +%Y-%m-%dT%H:%M:%SZ)"'",
"end_time": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"
}' \
"xXCSH_API_URLx/api/data/namespaces/xXCSH_NAMESPACEx/access_logs/aggregation" \
| jq '{total_requests: .total_hits}'
ResultMeaning
PASS — total_requests is a non-zero string (e.g., "380")Traffic is flowing through the LB
FAIL — "0" or no dataNo traffic reaching the LB — check DNS-1 and LB-1

What it proves: The breakdown of response status codes reveals error patterns.

Terminal window
curl -s -X POST \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
-H "Content-Type: application/json" \
-d '{
"start_time": "'"$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-24H +%Y-%m-%dT%H:%M:%SZ)"'",
"end_time": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",
"sort": "DESCENDING",
"limit": 100
}' \
"xXCSH_API_URLx/api/data/namespaces/xXCSH_NAMESPACEx/access_logs" \
| jq -r '
[.logs[] | fromjson | .rsp_code_class]
| group_by(.) | map({class: .[0], count: length})
| sort_by(-.count)
| ["STATUS_CLASS", "COUNT"], (.[] | [.class, .count])
| @tsv' | column -t

Expected output for a healthy site:

STATUS_CLASS COUNT
2xx 82
downstream_remote_disconnect 18
ResultMeaning
PASS — majority 2xxSite is serving successful responses
WARN — high 4xx countClient errors (bad URLs, missing resources)
FAIL — high 5xx countServer errors — check origin server health

The downstream_remote_disconnect class indicates the client closed the connection before the response completed (common for long-polling or WebSocket upgrade requests).

What it proves: A recent request for the protected domain was recorded after the validation request.

Terminal window
VALIDATION_START=$(date -u -d '5 minutes ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-5M +%Y-%m-%dT%H:%M:%SZ)
curl -fsS "https://xXCSH_DOMAINNAMEx/" >/dev/null
curl -s -X POST \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
-H "Content-Type: application/json" \
-d '{
"start_time": "'"$VALIDATION_START"'",
"end_time": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)'",
"sort": "DESCENDING",
"limit": 100
}' \
"xXCSH_API_URLx/api/data/namespaces/xXCSH_NAMESPACEx/access_logs" \
| jq -e --arg domain "xXCSH_DOMAINNAMEx" \
'[.logs[] | fromjson | select((.authority // .host // .req_host) == $domain)] | first | {timestamp: .["@timestamp"], method, path: .req_path, status: .rsp_code}'

PASS requires a matching event inside this time window for the exact protected host. Old namespace traffic or an unrelated host is not application evidence. Field availability can vary by log schema; inspect one parsed event and select the documented host field if none of authority, host, or req_host is present.

What it proves: Access logs confirm CSD JavaScript is being injected into responses.

Terminal window
curl -s -X POST \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
-H "Content-Type: application/json" \
-d '{
"start_time": "'"$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-1H +%Y-%m-%dT%H:%M:%SZ)"'",
"end_time": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",
"sort": "DESCENDING",
"limit": 20
}' \
"xXCSH_API_URLx/api/data/namespaces/xXCSH_NAMESPACEx/access_logs" \
| jq '[.logs[] | fromjson | select(.csd_js_injection == "true")] | length as $injected |
{injected_count: $injected, total_sampled: 20}'
ResultMeaning
PASS — injected_count > 0CSD JS is being injected into page responses
FAIL — injected_count: 0JS not injected — check CSD-1 and LB-3

TV-5: End-to-End Protocol and Telemetry Proof

Section titled “TV-5: End-to-End Protocol and Telemetry Proof”

What it proves: HTTP redirects to the single HTTPS virtual host, HTTPS serves the application through F5 XC, and the browser emits CSD telemetry.

Terminal window
curl -sS -o /dev/null -D - "http://xXCSH_DOMAINNAMEx/" \
| grep -E '^HTTP/|^[Ll]ocation:'
curl -sS -o /dev/null -w 'HTTPS %{http_code}\n' \
"https://xXCSH_DOMAINNAMEx/"
ResultMeaning
PASS — HTTP 301 with an HTTPS Location, then HTTPS 200Redirect and protected application path are operational
FAIL — any other resultCheck the one LB’s redirect, certificate, route pool, and origin health

Then open https://xXCSH_DOMAINNAMEx/ in a browser, use DevTools Network, filter for dip, and confirm a request from the page to the F5-operated CSD collection endpoint. The dip browser request proves client-side telemetry execution; curl alone cannot prove it.


These commands query the same data displayed in the CSD Console dashboard, script list, form fields, and network views.

What it proves: CSD is detecting and cataloging scripts running on the protected domain.

Terminal window
NOW=$(date +%s)
START=$(( NOW - 604800 ))
curl -s -X POST \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
-H "Content-Type: application/json" \
-d "{
\"startTime\": \"${START}\",
\"endTime\": \"${NOW}\"
}" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/scripts" \
| jq -r '
["SCRIPT", "RISK", "STATUS", "FIELDS", "USERS"],
(.scripts[] | [
(.script_name | if length > 50 then .[:47] + "..." else . end),
.risk_level,
.status,
(.form_fields_read // 0),
(.affected_users_count // 0)
])
| @tsv' | column -t
ResultMeaning
PASS — scripts listed with risk levelsCSD is actively monitoring scripts
FAIL — empty arraySee Troubleshooting: Empty Scripts Array

What it proves: CSD has detected script source domains and classified them by status.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/detected_domains" \
| jq '{
summary: {
action_needed: .domain_summary.actionNeededCount.count,
mitigated: .domain_summary.mitigatedDomains.count,
allowed: .domain_summary.allowedDomains.count,
total: .domain_summary.totalDomains.count
},
domains: [.domains_list[] | {
domain: .domain,
category: .category,
status: .status,
first_seen: (.firstSeenDate | tonumber | todate),
latest_seen: (.latestSeenDate | tonumber | todate)
}]
}'
ResultMeaning
PASS — total > 0 with domains listedCSD is tracking script domains
WARN — action_needed > 0Some domains need review
FAIL — empty responseNo telemetry data; check CSD-5

What it proves: Shows the distribution of allowed vs. mitigated domains and whether policy is consistent.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/detected_domains" \
| jq '{
action_needed: .domain_summary.actionNeededCount.count,
mitigated: .domain_summary.mitigatedDomains.count,
allowed: .domain_summary.allowedDomains.count,
total: .domain_summary.totalDomains.count,
domains_needing_action: [.domains_list[] | select(.status == "AN") | .domain]
}'
ResultMeaning
PASS — action_needed: 0All domains have been reviewed and classified
WARN — action_needed > 0Domains listed in domains_needing_action require review

What it proves: CSD has detected form fields that scripts are reading, with sensitivity classification.

Terminal window
NOW=$(date +%s)
START=$(( NOW - 604800 ))
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/formFields?startTime=${START}&endTime=${NOW}" \
| jq -r '
["FIELD", "SENSITIVITY", "SCRIPTS"],
(.form_fields[] | [
.name,
.analysis,
(.scripts_count // 0)
])
| @tsv' | column -t
ResultMeaning
PASS — form fields listed with sensitivityCSD is tracking form field access
INFO — empty arrayNo form fields detected (expected if no forms on the site)

What it proves: Detailed information about a specific script including risk, behaviors, and network interactions.

First, get a script ID from TEL-1, then query its details:

Terminal window
SCRIPT_ID="your-script-id"
# Overview (risk level, type, source domain)
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/scripts/${SCRIPT_ID}/dashboard" \
| jq '{script_name, risk_level, type, source_domain, status}'
# Behaviors over time
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/scripts/${SCRIPT_ID}/behaviors" \
| jq '.behaviors'
# Network interactions (domains the script communicates with)
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/scripts/${SCRIPT_ID}/networkInteractions" \
| jq '.network_interactions'

What it proves: Lists users impacted by a specific script, showing the scope of exposure.

Terminal window
SCRIPT_ID="your-script-id"
NOW=$(date +%s)
START=$(( NOW - 604800 ))
curl -s -X POST \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
-H "Content-Type: application/json" \
-d "{
\"startTime\": \"${START}\",
\"endTime\": \"${NOW}\"
}" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/scripts/${SCRIPT_ID}/affectedUsers" \
| jq -r '
["IP_ADDRESS", "DEVICE_ID", "GEO", "CHANNEL", "USER_AGENT"],
(.affected_users[] | [
.ip_address,
(.device_id | if length > 12 then .[:12] + "..." else . end),
.geolocation,
.channel,
(.user_agent | if length > 30 then .[:27] + "..." else . end)
])
| @tsv' | column -t

What it proves: Aggregated risk distribution across all detected scripts.

Terminal window
NOW=$(date +%s)
START=$(( NOW - 604800 ))
curl -s -X POST \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
-H "Content-Type: application/json" \
-d "{
\"startTime\": \"${START}\",
\"endTime\": \"${NOW}\"
}" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/scripts" \
| jq '[.scripts[] | .risk_level] | group_by(.) | map({risk_level: .[0], count: length}) | sort_by(-.count)'
ResultMeaning
PASS — all No RiskNo risky scripts detected
WARN — Low Risk or High Risk presentReview flagged scripts in TEL-5

What it proves: CSD telemetry data is recent, confirming active monitoring.

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/detected_domains" \
| jq '{
total_domains: .domain_summary.totalDomains.count,
latest_update: (.domain_summary.totalDomains.lastUpdated // "unknown"),
most_recent_domain: (.domains_list | sort_by(.latestSeenDate) | last | {
domain: .domain,
latest_seen: (.latestSeenDate | tonumber | todate)
})
}'
ResultMeaning
PASS — latest_seen is within the last 24 hoursTelemetry is actively collecting data
WARN — latest_seen is older than 24 hoursTraffic may have stopped or CSD processing is delayed

Run only the subsection matching the selected cloud scenario and owner. XCSH_CSD_DEPLOYMENT_MODE=api|terraform selects F5 resource ownership; it is not a cloud-provider selector. Cloud-side health is supporting origin evidence; the common DNS, F5 Distributed Cloud, TLS, protected HTTPS, access-log, and CSD checks above remain mandatory.

Terraform-Owned AWS Origin and Log Evidence

Section titled “Terraform-Owned AWS Origin and Log Evidence”

Use this subsection only for the Terraform-owned AWS reference stack. It observes current Terraform state plus AWS origin and delivery evidence; it does not prove the protected F5 path by itself.

Terminal window
TF_DIR="$(git rev-parse --show-toplevel)/terraform/aws"
TF_JSON=$(terraform -chdir="$TF_DIR" show -json)
AWS_REGION=$(terraform -chdir="$TF_DIR" output -raw aws_region)
VPC_ID=$(terraform -chdir="$TF_DIR" output -raw vpc_id)
TARGET_GROUP_ARN=$(terraform -chdir="$TF_DIR" output -raw target_group_arn)
KMS_KEY_ARN=$(terraform -chdir="$TF_DIR" output -raw cloudwatch_logs_kms_key_arn)
ALB_LOG_BUCKET=$(terraform -chdir="$TF_DIR" output -raw alb_access_logs_bucket)
ALB_LOG_PREFIX=$(terraform -chdir="$TF_DIR" output -raw alb_access_logs_prefix)
APPLICATION_URL=$(terraform -chdir="$TF_DIR" output -raw application_url)
XCSH_LB_NAME=$(terraform -chdir="$TF_DIR" output -raw xc_http_loadbalancer_name)
XCSH_ORIGIN_POOL=$(terraform -chdir="$TF_DIR" output -raw xc_origin_pool_name)
ECS_CLUSTER=$(printf '%s' "$TF_JSON" | jq -r '.. | objects | select(.address? == "module.origin.aws_ecs_cluster.this") | .values.name')
ECS_SERVICE=$(printf '%s' "$TF_JSON" | jq -r '.. | objects | select(.address? == "module.origin.aws_ecs_service.this") | .values.name')
APP_LOG_GROUP=$(printf '%s' "$TF_JSON" | jq -r '.. | objects | select(.address? == "module.origin.aws_cloudwatch_log_group.this") | .values.name')
FLOW_LOG_GROUP=$(printf '%s' "$TF_JSON" | jq -r '.. | objects | select(.address? == "aws_cloudwatch_log_group.vpc_flow") | .values.name')

Do not copy identifiers from a prior run. The root values come from Terraform outputs; module-only runtime identifiers are read from the current state at execution time.

Terminal window
aws ecs wait services-stable \
--region "$AWS_REGION" \
--cluster "$ECS_CLUSTER" \
--services "$ECS_SERVICE"
aws ecs describe-services \
--region "$AWS_REGION" \
--cluster "$ECS_CLUSTER" \
--services "$ECS_SERVICE" \
--query 'services[0].{status:status,desired:desiredCount,running:runningCount,pending:pendingCount}'

PASS requires status ACTIVE, running equal to desired, and pending zero.

Terminal window
aws elbv2 describe-target-health \
--region "$AWS_REGION" \
--target-group-arn "$TARGET_GROUP_ARN" \
--query 'TargetHealthDescriptions[].TargetHealth.{state:State,reason:Reason,description:Description}'

At least one target must be healthy. This is AWS-side target health; the protected HTTPS test remains necessary to prove the F5 XC path.

AWS-3: KMS-Encrypted CloudWatch and Flow Logs

Section titled “AWS-3: KMS-Encrypted CloudWatch and Flow Logs”
Terminal window
aws logs describe-log-groups \
--region "$AWS_REGION" \
--log-group-name-prefix "$APP_LOG_GROUP" \
--query 'logGroups[?logGroupName==`'"$APP_LOG_GROUP"'`].{name:logGroupName,kmsKeyId:kmsKeyId,retention:retentionInDays}'
aws ec2 describe-flow-logs \
--region "$AWS_REGION" \
--filter "Name=resource-id,Values=$VPC_ID" \
--query 'FlowLogs[].{status:FlowLogStatus,delivery:DeliverLogsStatus,destination:LogDestination}'
aws logs describe-log-streams \
--region "$AWS_REGION" \
--log-group-name "$FLOW_LOG_GROUP" \
--order-by LastEventTime --descending --max-items 1 \
--query 'logStreams[0].{stream:logStreamName,lastEvent:lastEventTimestamp}'

PASS requires the application log group to reference KMS_KEY_ARN, 365-day retention, an active flow log with successful delivery, and a flow-log stream with a recent event after traffic is generated.

Terminal window
aws s3api list-objects-v2 \
--region "$AWS_REGION" \
--bucket "$ALB_LOG_BUCKET" \
--prefix "$ALB_LOG_PREFIX/AWSLogs/" \
--max-items 5 \
--query 'Contents[].{key:Key,lastModified:LastModified,size:Size}'

PASS requires a recent, non-empty object after sending traffic. Delivery can lag; absence immediately after a request is not proof of failure.

Terminal window
curl -sS -o /dev/null -D - "${APPLICATION_URL/https:/http:}" \
| grep -E '^HTTP/|^[Ll]ocation:'
APPLICATION_HTML=$(curl -fsS "$APPLICATION_URL")
printf '%s' "$APPLICATION_HTML" | grep -qi 'OWASP Juice Shop'
printf '%s' "$APPLICATION_HTML" | grep -q '__imp_apg__'

PASS requires HTTP 301, HTTPS 200, rendered Juice Shop content, and __imp_apg__. Also confirm VIRTUAL_HOST_READY, CertificateValid or AutoCertRenewing, and a browser dip request using the earlier checks.

Terminal window
terraform -chdir="$TF_DIR" plan -detailed-exitcode -out=csd-final.tfplan

Exit code 0 is PASS (no drift). Exit code 2 means the saved plan contains changes: review it with terraform -chdir="$TF_DIR" show csd-final.tfplan, correct the owning Terraform configuration or runtime issue, and create a fresh saved plan. Retain the same backend and state. Do not remediate Terraform-owned F5 XC resources with API mutation.

For the Azure alternate, verify ownership and runtime with the Azure source of truth selected for that deployment. Record the exact subscription, resource group, resource IDs, deployment owner, provisioning state, and application endpoint used by XCSH_ORIGIN_IP.

Require an owner-backed no-drift result where declarative IaC owns the resources, and verify a recent Azure application-log event after a protected HTTPS request. Do not reuse the AWS ECS, ALB, CloudWatch, flow-log, S3, or Terraform-output assertions for Azure.

Azure origin health and owner evidence are not substitutes for DNS-to-current-VIP equality, the exact protected-domain record, certificate state, HTTP redirect, HTTPS application marker, F5 access-log event, CSD injection, and browser dip telemetry.


Run a single read-only command that checks critical health indicators across every layer and produces a summary table:

Terminal window
echo "=== CSD Verification Dashboard ==="
echo ""
# Layer 1: DNS evidence (display only; DNS-1 and DNS-2 define PASS)
DNS_A=$(dig +short xXCSH_DOMAINNAMEx A | sort -u | paste -sd, -)
DNS_ACME_CNAME=$(dig +short _acme-challenge.xXCSH_DOMAINNAMEx CNAME | paste -sd, -)
DNS_ACME_TXT=$(dig +short _acme-challenge.xXCSH_DOMAINNAMEx TXT | paste -sd, -)
# Layers 2-4: one LB, TLS, and origin
LB=$(curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/http_loadbalancers/xXCSH_LB_NAMEx")
ORIGIN_POOL=$(curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/origin_pools/xXCSH_ORIGIN_POOLx")
LB_STATE=$(echo "$LB" | jq -r '.spec.state // "UNKNOWN"')
CERT_STATE=$(echo "$LB" | jq -r '.spec.cert_state // "UNKNOWN"')
HTTP_REDIRECT=$(echo "$LB" | jq -r '.spec.https_auto_cert.http_redirect // false')
CSD_ON_LB=$(echo "$LB" | jq -r 'if .spec.client_side_defense then "ENABLED" else "DISABLED" end')
DOMAINS=$(echo "$LB" | jq -r '.spec.domains | join(", ")')
ROUTE_POOL=$(echo "$LB" | jq -r '[.spec.default_route_pools[] | .pool.name] | join(", ")')
case "xXCSH_ORIGIN_KINDx" in
public_name) ORIGIN=$(echo "$ORIGIN_POOL" | jq -r '.spec.origin_servers[0].public_name.dns_name // "MISSING"') ;;
public_ip) ORIGIN=$(echo "$ORIGIN_POOL" | jq -r '.spec.origin_servers[0].public_ip.ip // "MISSING"') ;;
*) ORIGIN="INVALID_ORIGIN_KIND" ;;
esac
# Layer 6: CSD status
CSD=$(curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/status")
CSD_CONFIGURED=$(echo "$CSD" | jq -r '.isConfigured // false')
CSD_ENABLED=$(echo "$CSD" | jq -r '.isEnabled // false')
# Layer 6: JS config
JS_TAG=$(curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/js_configuration" \
| jq -r 'if (.scriptTag | length) > 0 then "PRESENT" else "MISSING" end')
# Layer 7: Traffic (last 24h)
TRAFFIC=$(curl -s -X POST \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
-H "Content-Type: application/json" \
-d "{
\"start_time\": \"$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-24H +%Y-%m-%dT%H:%M:%SZ)\",
\"end_time\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
}" \
"xXCSH_API_URLx/api/data/namespaces/xXCSH_NAMESPACEx/access_logs/aggregation" \
| jq -r '.total_hits // "0"')
printf "%-28s %s\n" "CHECK" "STATUS"
printf "%-28s %s\n" "----------------------------" "----------------------------"
printf "%-28s %s\n" "[DNS] A Records" "${DNS_A:-NOT_FOUND}"
printf "%-28s %s\n" "[DNS] ACME CNAME" "${DNS_ACME_CNAME:-NOT_FOUND}"
printf "%-28s %s\n" "[DNS] ACME TXT" "${DNS_ACME_TXT:-NOT_FOUND}"
printf "%-28s %s\n" "[TLS] Certificate" "$CERT_STATE"
printf "%-28s %s\n" "[LB] State" "$LB_STATE"
printf "%-28s %s\n" "[LB] HTTP Redirect" "$HTTP_REDIRECT"
printf "%-28s %s\n" "[LB] Domains" "$DOMAINS"
printf "%-28s %s\n" "[LB] Route Pool" "$ROUTE_POOL"
printf "%-28s %s\n" "[Origin] Kind" "xXCSH_ORIGIN_KINDx"
printf "%-28s %s\n" "[Origin] Value" "$ORIGIN"
printf "%-28s %s\n" "[LB] CSD on LB" "$CSD_ON_LB"
printf "%-28s %s\n" "[CSD] Configured (tenant)" "$CSD_CONFIGURED"
printf "%-28s %s\n" "[CSD] Enabled" "$CSD_ENABLED"
printf "%-28s %s\n" "[CSD] JS Script Tag" "$JS_TAG"
printf "%-28s %s\n" "[Traffic] Requests (24h)" "$TRAFFIC"

FieldTypeDescription
@timestampstringRequest timestamp (ISO 8601). Note the @ prefix — access with .["@timestamp"] in jq
methodstringHTTP method (GET, POST, etc.)
req_pathstringRequest URI path
rsp_codestringHTTP response status code as a string (e.g., "200", "404")
rsp_code_classstringStatus code class (2xx, 3xx, 4xx, 5xx, or downstream_remote_disconnect)
src_ipstringClient source IP address
dst_ipstringDestination (VIP) IP address
domainstringRequest Host header value
user_agentstringClient User-Agent string
rsp_sizestringResponse body size in bytes (returned as string)
req_sizestringRequest body size in bytes (returned as string)
duration_with_data_tx_delaystringTotal request duration in seconds (returned as string, e.g., "0.024219")
csd_js_injectionstring"true" when CSD JavaScript was injected (only present when active)
FieldEndpointDescription
isConfiguredstatusCSD enabled at tenant level
isEnabledstatusCSD active for this namespace
scripts[]scriptsArray of detected script objects
.script_namescriptsFull URL of the JavaScript file
.risk_levelscriptsRisk level (No Risk, Low Risk, High Risk)
.statusscriptsAN (Action Needed) or NA (No Action Needed)
.form_fields_readscriptsNumber of form fields the script reads
.affected_users_countscriptsNumber of unique users/sessions affected
domain_summarydetected_domainsCounts by status: .actionNeededCount.count, .mitigatedDomains.count, .allowedDomains.count, .totalDomains.count (each has .count and .lastUpdated)
domains_list[]detected_domainsArray of detected domain objects with .domain, .status, .category, .firstSeenDate, .latestSeenDate (epoch seconds as strings)
form_fields[]formFieldsArray of detected form field objects
.analysisformFieldsSensitivity classification (Sensitive, Not Sensitive)

If TV-1 returns 0:

  1. Check DNS resolution — verify the domain resolves to the LB VIP:

    Terminal window
    dig +short xXCSH_DOMAINNAMEx A

    If empty, DNS is not configured. Follow Phase 1 — DNS, Certificate, and Origin Recovery.

  2. Check LB state — the unsuffixed load balancer must be VIRTUAL_HOST_READY and its certificate must be valid:

    Terminal window
    curl -s \
    -H "Authorization: APIToken xXCSH_API_TOKENx" \
    "xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/http_loadbalancers/xXCSH_LB_NAMEx" \
    | jq '{state: .spec.state, cert_state: .spec.cert_state, http_redirect: .spec.https_auto_cert.http_redirect}'
  3. Send protocol-specific test requests — HTTP must redirect and HTTPS must serve the application:

    Terminal window
    curl -sS -o /dev/null -D - "http://xXCSH_DOMAINNAMEx/" | grep -E '^HTTP/|^[Ll]ocation:'
    curl -sS -o /dev/null -w '%{http_code}\n' "https://xXCSH_DOMAINNAMEx/"

If DNS-4 returns false, automatic record creation is disabled even though F5 XC is the authoritative DNS provider. This is a common misconfiguration that causes both the A record and ACME CNAME to be missing, which blocks the load balancer from reaching VIRTUAL_HOST_READY and the certificate from being issued.

To enable managed records, have the established DNS-zone owner review and apply allow_http_lb_managed_records: true. See Phase 1 — DNS, Certificate, and Origin Recovery; this CSD workflow does not mutate the shared zone.

The load balancer is waiting for a DNS A record pointing to its VIP. See LB Stuck in VIRTUAL_HOST_PENDING_A_RECORD for detailed resolution steps.

Certificate Stuck in PreDomainChallengePending

Section titled “Certificate Stuck in PreDomainChallengePending”

The automatic TLS certificate requires an ACME challenge record. The method depends on your DNS provider:

F5 XC Managed DNS: Enable allow_http_lb_managed_records on the DNS zone (DNS-4). The platform creates a TXT-based ACME challenge record in the x-ves-io-managed RR set group.

For API-owned resources, follow the documented clean-recreation workflow if issuance remains stuck. For Terraform-owned resources, retain the same backend/state and create, review, and apply a fresh Terraform plan; do not delete the LB through the API.

External DNS: Create a CNAME record at your DNS provider:

_acme-challenge.xXCSH_DOMAINNAMEx CNAME *.autocerts.ves.volterra.io

Verify the record exists:

Terminal window
dig +short _acme-challenge.xXCSH_DOMAINNAMEx CNAME
dig +short _acme-challenge.xXCSH_DOMAINNAMEx TXT

If both are empty, the ACME record is not configured. Certificate provisioning takes 5–10 minutes after the record is in place. Check the LB error status for specific ACME validation errors:

Terminal window
curl -s \
-H "Authorization: APIToken xXCSH_API_TOKENx" \
"xXCSH_API_URLx/api/config/namespaces/xXCSH_NAMESPACEx/http_loadbalancers/xXCSH_LB_NAMEx" \
| jq '[.status[]? | select(.virtual_host_status != null) | .virtual_host_status.error_description]'

CSD must be enabled at the tenant level by an F5 XC administrator. This is a tenant-wide setting that cannot be configured via the namespace API. Contact your administrator to enable Client-Side Defense.

If TEL-1 returns an empty array:

  1. Check the protected domain — CSD monitors scripts only on registered domains:

    Terminal window
    curl -s \
    -H "Authorization: APIToken xXCSH_API_TOKENx" \
    "xXCSH_API_URLx/api/shape/csd/namespaces/xXCSH_NAMESPACEx/protected_domains" \
    | jq '.items[] | {name, namespace}'
  2. Verify JS injection — confirm the CSD script path is injected into the protected HTTPS response:

    Terminal window
    curl -fsS "https://xXCSH_DOMAINNAMEx/" | grep -q '__imp_apg__'

    If there is no match, check that the LB has client_side_defense with js_insert_all_pages.

  3. Allow processing time — after first enabling CSD or registering a new protected domain, script detection can take 5–15 minutes. Generate traffic to the site and wait before re-checking.

This procedure tests candidate coverage; it does not define F5’s monitored-header set. Page Tamper officially snapshots HTTP headers, uses a moving baseline built over multiple browser sessions and sequential cycles, and can produce ClientSideDefenseHttpHeaderModified or ClientSideDefenseHttpHeaderCompromised. See About Client-Side Defense and Configure Client-Side Defense.

Historical evidence must be interpreted narrowly. On 2026-09-23, a tenant ClientSideDefenseHttpHeaderModified record named x-content-type-options, x-frame-options, and cache-control, reported modification=Added, and included the exact protected path /. It proves detection for that event, not the Compromised semantic or an official monitored-header list. The 2026-09-24 six-header global-value campaign is INVALID TEST for Compromised-trigger conclusions: it had no controlled baseline/comparison cohorts and alert review initially searched current alerts rather than history. Its later bounded negative search remains an observation, not proof of unsupported coverage.

Issue #1241 owns the dedicated endpoint and deterministic controller. Use the permanent but inert-by-default /csd-page-tamper/payment endpoint; do not mutate the F5 Distributed Cloud load balancer or origin pool for an experiment.

  1. Bootstrap the control baseline. Require no Terraform drift, a ready load balancer, a valid certificate, healthy ordinary application traffic, and a healthy dedicated endpoint. From workstation and worker fresh browsers, prove HTTP 200, all 12 exact candidate headers, all five empty synthetic fields, CSD injection, and a dip POST. Run control-only traffic for 45 minutes while polling both current alerts and alert history. If deployment creates a correlated Modified/Added alert, wait for resolution and then require a 15-minute quiet period.
  2. Reinforce control for 15 minutes. Run 12 fresh profiles against the exact payment path without the selector. Every response must retain all 12 exact headers, CSD injection, and dip evidence.
  3. Run one mixed cohort for up to 45 minutes. Alternate fresh control profiles with tampered profiles at the identical URL. Only tampered profiles send X-CSD-Page-Tamper: <header-id>. Require the selected header absent, the other 11 exact, and at least 20 complete pairs unless the exact required alert arrives sooner.
  4. Poll current and history every 60 seconds. Parse JSON-string history entries. Correlate only records whose raw fields match the exact namespace, selected header case-insensitively, path /csd-page-tamper/payment, and experiment start window. Preserve sanitized alert name, modification, lifecycle state, timestamps, display name, and description. Generic, wrong-path, stale, future, or adjacent alerts do not qualify.
  5. Classify conservatively. Use COMPROMISED only for an exact Compromised correlation; MODIFIED_ONLY for an exact Modified correlation without Compromised; NO_ALERT_WITHIN_WINDOW only when the complete evidence chain is valid but neither arrives; and INVALID_TEST for any baseline, cohort, telemetry, overlap, correlation, recovery, cleanup, readiness, or drift failure.
  6. Apply the canary stop rule. Run X-Content-Type-Options first. If it does not reach COMPROMISED, complete recovery, stop the suite, and diagnose the Modified/Compromised distinction. Do not claim the hypothesis succeeded.
  7. Recover control-only traffic for 15 minutes. Stop selector traffic and re-prove all 12 exact headers from workstation and worker, CSD injection, dip, endpoint and ordinary application health, load-balancer readiness, run-owned cleanup, and final refresh-aware Terraform no drift. Only then may another header start.

Publish only sanitized receipts and measured outcomes. Until an executed case satisfies every gate, the dedicated mixed-cohort design is a hypothesis rather than proof of Compromised behavior.

The dedicated endpoint is deployed and bootstrap-proven. Exact-plan deployment made five additions, one task-definition replacement (1 change, 1 destroy), and zero F5 Distributed Cloud mutations. Bootstrap run dd91ea9b-3da3-4fc8-b24b-c32cdba2e863 ran from 2026-09-25T21:40:23.373Z to 2026-09-25T22:42:18.638Z with alerts=[]. Endpoint and root health, all 12 exact canonical headers, five empty payment fields, CSD script presence, both healthy ALB target groups, VIRTUAL_HOST_READY, CertificateValid, readiness, and Terraform no drift were all observed.

Canary run 69f30bd0-6dc2-4721-bb21-cae159032af2 ran from 2026-09-25T23:21:44.908Z to 2026-09-26T00:38:11.806Z. It completed 12/12 controls and 20/20 mixed control/tampered pairs with valid telemetry and alerts=[]. No exact Modified or Compromised correlation appeared; current alerts returned zero and alert history returned an empty array. Classify this run only as NO_ALERT_WITHIN_WINDOW.

Recovery proved 10 control pairs plus successful probes, readiness, cleanup, and Terraform no drift. The canary did not reach COMPROMISED, so the suite stopped as required and no remaining candidate header was run. The endpoint and test mechanics are validated; the Compromised hypothesis is not. Earlier controller-defect attempts were INVALID_TEST and are not product evidence.

PeriodEpoch offsetISO 8601 (Linux)ISO 8601 (macOS)
1 hour$(( $(date +%s) - 3600 ))date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZdate -u -v-1H +%Y-%m-%dT%H:%M:%SZ
24 hours$(( $(date +%s) - 86400 ))date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZdate -u -v-24H +%Y-%m-%dT%H:%M:%SZ
7 days$(( $(date +%s) - 604800 ))date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZdate -u -v-7d +%Y-%m-%dT%H:%M:%SZ
30 days$(( $(date +%s) - 2592000 ))date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZdate -u -v-30d +%Y-%m-%dT%H:%M:%SZ
LayerTestsWhat it covers
1. DNS ResolutionDNS-1 through DNS-4A record, ACME CNAME, nameserver authority, managed records
2. TLS CertificateTLS-1 through TLS-3Cert state, cert metadata, live handshake
3. HTTP Load BalancerLB-1 through LB-5One LB, HTTPS redirect, CSD flag, route pool, deployment
4. Origin PoolOP-1 through OP-4Hostname/IP oneOf, TLS mode, HC association, direct observation
5. Health CheckHC-1 through HC-2HC config, list all HCs
6. CSD ConfigurationCSD-1 through CSD-5Tenant status, JS tag, protected domains, live injection
7. Traffic VerificationTV-1 through TV-5Request count, status codes, samples, JS in logs, end-to-end test
8. CSD TelemetryTEL-1 through TEL-8Scripts, domains, policy, form fields, deep dive, users, risk, freshness
DiagnosticEndpointMethodTime Format
Request count/api/data/namespaces/\{ns\}/access_logs/aggregationPOSTISO 8601
Status codes/api/data/namespaces/\{ns\}/access_logsPOSTISO 8601
Recent requests/api/data/namespaces/\{ns\}/access_logsPOSTISO 8601
LB state/api/config/namespaces/\{ns\}/http_loadbalancers/\{name\}GETNone
Origin pool/api/config/namespaces/\{ns\}/origin_pools/\{name\}GETNone
Healthcheck/api/config/namespaces/\{ns\}/healthchecks/\{name\}GETNone
DNS zone/api/config/dns/namespaces/system/dns_zones/\{zone\}GETNone
CSD status/api/shape/csd/namespaces/\{ns\}/statusGETNone
JS configuration/api/shape/csd/namespaces/\{ns\}/js_configurationGETNone
Protected domains/api/shape/csd/namespaces/\{ns\}/protected_domainsGETNone
Script list/api/shape/csd/namespaces/\{ns\}/scriptsPOSTEpoch seconds
Detected domains/api/shape/csd/namespaces/\{ns\}/detected_domainsGETNone
Form fields/api/shape/csd/namespaces/\{ns\}/formFieldsGETEpoch seconds (query params)
Script details/api/shape/csd/namespaces/\{ns\}/scripts/\{id\}/dashboardGETNone
Script behaviors/api/shape/csd/namespaces/\{ns\}/scripts/\{id\}/behaviorsGETNone
Script network/api/shape/csd/namespaces/\{ns\}/scripts/\{id\}/networkInteractionsGETNone
Affected users/api/shape/csd/namespaces/\{ns\}/scripts/\{id\}/affectedUsersPOSTEpoch seconds