演示
本指南将引导您在 F5 Distributed Cloud 上使用 API 完成一个完整的客户端防御练习 — 分为四个阶段,AI 助手或人工操作员可以端到端执行。每个步骤都包含一个可直接运行的 curl 命令,其中包含占位符值,您可以使用页面顶部的表单、.env 文件或任何自动化工具进行自定义。
| 阶段 | 目标 | 步骤 |
|---|---|---|
| 阶段 1 — 构建 | 部署并验证完整的 CSD 基础设施 | 步骤 1–7 |
| 阶段 2 — 攻击 | 生成模拟攻击流量并确认 CSD 已检测到 | 步骤 8–9 |
| 阶段 3 — 缓解 | 缓解前后对比证明 — 运行攻击、应用缓解、重新运行攻击、比较结果 | 步骤 1–6 |
| 阶段 4 — 拆除 | 在明确确认后移除所有部署对象 | 拆除 |
在开始阶段 1 之前,请验证环境是否干净。运行以下 API 检查以确定是否存在上次运行遗留的对象:
# Check all Phase 1 objects and compute environment statusHTTP_LB=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-http")HTTPS_LB=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-https")ORIGIN=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools/xF5XC_ORIGIN_POOLx")HC=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks/xF5XC_HC_NAMEx")PD_COUNT=$(curl -s -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains" \ | jq '[.items // [] | .[] | select(.metadata.name != null)] | length')MD_COUNT=$(curl -s -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/mitigated_domains" \ | jq '[.items // [] | .[] | select(.metadata.name != null)] | length')
# If HTTPS LB exists, fetch body to detect skeleton stateHTTPS_IS_SKELETON="false"if [ "$HTTPS_LB" = "200" ]; then HTTPS_LB_BODY=$(curl -s \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-https") HTTPS_IS_SKELETON=$(echo "$HTTPS_LB_BODY" | jq ' ((.spec.default_route_pools // []) | length == 0) and (.spec.client_side_defense == null) ')fi
# Compute deterministic environment statusjq -n \ --argjson http_lb "$HTTP_LB" \ --argjson https_lb "$HTTPS_LB" \ --argjson origin "$ORIGIN" \ --argjson hc "$HC" \ --argjson pd "$PD_COUNT" \ --argjson md "$MD_COUNT" \ --argjson https_skeleton "$HTTPS_IS_SKELETON" \'{ objects: [ { name: "http_lb", http_code: $http_lb, exists: ($http_lb == 200) }, { name: "https_lb", http_code: $https_lb, exists: ($https_lb == 200), is_skeleton: $https_skeleton }, { name: "origin_pool", http_code: $origin, exists: ($origin == 200) }, { name: "healthcheck", http_code: $hc, exists: ($hc == 200) }, { name: "protected_domains", count: $pd, exists: ($pd > 0) }, { name: "mitigated_domains", count: $md, exists: ($md > 0) } ], any_infra_exists: ($http_lb == 200 or ($https_lb == 200 and ($https_skeleton | not)) or $origin == 200 or $hc == 200), any_csd_exists: ($pd > 0 or $md > 0), status: ( if ($http_lb == 404 and $https_lb == 404 and $origin == 404 and $hc == 404 and $pd == 0 and $md == 0) then "CLEAN" elif ($https_lb == 200 and $https_skeleton and $http_lb == 404 and $origin == 404 and $hc == 404 and $pd == 0 and $md == 0) then "HTTPS_SKELETON" elif ($http_lb == 200 and $origin == 200) then "ALL_EXIST" elif ($http_lb == 200 or ($https_lb == 200 and ($https_skeleton | not)) or $origin == 200 or $hc == 200) then "TEARDOWN_NEEDED" elif ($md > 0 and $http_lb == 404 and ($https_lb == 404 or ($https_lb == 200 and $https_skeleton)) and $origin == 404 and $hc == 404) then "MITIGATIONS_ONLY" else "TEARDOWN_NEEDED" end ), action: ( if ($http_lb == 404 and $https_lb == 404 and $origin == 404 and $hc == 404 and $pd == 0 and $md == 0) then "Proceed to Phase 1" elif ($https_lb == 200 and $https_skeleton and $http_lb == 404 and $origin == 404 and $hc == 404 and $pd == 0 and $md == 0) then "Proceed to Phase 1 (HTTPS LB skeleton will be restored via PUT)" elif ($http_lb == 200 and $origin == 200) then "All Phase 1 objects exist — verify health, optionally skip to Phase 2" elif ($http_lb == 200 or ($https_lb == 200 and ($https_skeleton | not)) or $origin == 200 or $hc == 200) then "Run Phase 4 Teardown first, then re-check" elif ($md > 0 and $http_lb == 404 and ($https_lb == 404 or ($https_lb == 200 and $https_skeleton)) and $origin == 404 and $hc == 404) then "Delete mitigated domains inline, then proceed" else "Run Phase 4 Teardown first, then re-check" end )}'当所有阶段 1 对象都存在(200)且您计划跳到阶段 2 时,
请运行阶段 1 步骤 7 验证命令以确认基础设施
健康状况,然后再跳过。使用
阶段 1 — 步骤 7:验证中的确切命令:
- DNS 解析:
dig +short xF5XC_DOMAINNAMEx A - HTTP LB 状态:
GET .../http_loadbalancers/xF5XC_LB_NAMEx-http通过管道传递给jq '{state: .spec.state}'— 必须显示VIRTUAL_HOST_READY - CSD JS 配置:
GET .../js_configuration— 必须包含scriptTag - CSD 状态:
GET .../status通过管道传递给jq '{configured: .isConfigured, enabled: .isEnabled}'— 两者都必须为true
在跳到阶段 2 之前,所有必需的检查(DNS-1、LB-1、CSD-1、CSD-2)都必须通过。 如果任何检查失败,请从失败的步骤开始执行阶段 1。
就绪验证矩阵
Section titled “就绪验证矩阵”上述飞行前检查验证环境是否干净。下面的就绪矩阵验证环境是否具备能力 — 即所有前提条件、配额、连接和平台服务是否已就位,以确保演示成功。在每次会议前作为准备阶段的一部分运行此矩阵。
每个检查都有一个测试 ID、一个层级(T0–T5)、通过/失败/警告标准以及修复路径。层级是顺序的 — 较早层级的失败会阻止后续层级的运行。
| 层级 | 类别 | 阻止演示? | 目的 |
|---|---|---|---|
| T0 | 连接与认证 | 是 | 我们能否访问平台并进行认证? |
| T1 | 配额与容量 | 是(达到限制时) | 是否有空间创建演示对象? |
| T2 | 平台前提条件 | 是 | 租户级服务是否已配置? |
| T3 | 源站健康 | 警告 | 后端应用程序是否正在响应? |
| T4 | 环境清洁 | 自动修复 | 是否有上次运行遗留的对象? |
| T5 | 证书就绪 | 信息性 | HTTPS 是否可用,还是应该计划仅使用 HTTP? |
T0:连接与认证
Section titled “T0:连接与认证”这些检查确认执行主机能够访问 F5 XC API 且凭据有效。
PF-T0-1:API 连接
Section titled “PF-T0-1:API 连接”HTTP_CODE=$(curl -s -o /dev/null -w '%\{http_code\}' --connect-timeout 10 --max-time 15 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/web/namespaces")echo "{\"http_code\": $HTTP_CODE}" | jq '{ check: "PF-T0-1", http_code: .http_code, status: ( if .http_code == 200 then "PASS" elif .http_code == 401 then "FAIL" else "FAIL" end ), detail: ( if .http_code == 200 then "API reachable, token valid" elif .http_code == 401 then "Token expired or invalid — regenerate under Administration > Credentials > API Credentials" elif .http_code == 0 then "Network unreachable — check connectivity, VPN, or TLS compatibility (try --tlsv1.2 --tls-max 1.2)" else "Unexpected HTTP \(.http_code)" end )}'PF-T0-2:命名空间访问
Section titled “PF-T0-2:命名空间访问”HTTP_CODE=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers")echo "{\"http_code\": $HTTP_CODE}" | jq '{ check: "PF-T0-2", http_code: .http_code, status: ( if .http_code == 200 then "PASS" elif .http_code == 404 then "WARN" else "FAIL" end ), detail: ( if .http_code == 200 then "Token has namespace access" elif .http_code == 403 then "Token lacks permissions for namespace — check role bindings" elif .http_code == 404 then "Namespace does not exist — will be created in Phase 1 Step 0" else "Unexpected HTTP \(.http_code)" end )}'PF-T0-3:CSD API 访问
Section titled “PF-T0-3:CSD API 访问”HTTP_CODE=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/status")echo "{\"http_code\": $HTTP_CODE}" | jq '{ check: "PF-T0-3", http_code: .http_code, status: ( if .http_code == 200 then "PASS" elif .http_code == 404 then "WARN" else "FAIL" end ), detail: ( if .http_code == 200 then "Token has CSD/Shape API permissions" elif .http_code == 403 then "Token lacks CSD role binding — contact tenant administrator" elif .http_code == 404 then "Namespace does not exist — CSD access will be verified after namespace creation in Phase 1" else "Unexpected HTTP \(.http_code)" end )}'PF-T0-4:RBAC 权限矩阵
Section titled “PF-T0-4:RBAC 权限矩阵”非破坏性探测测试演示所需每种对象类型的读写权限。读权限通过对列表端点的 GET 测试。写权限通过对已知不存在的对象执行 DELETE 测试 — 如果 RBAC 拒绝操作,API 返回 403,如果操作被允许但对象不存在则返回 404。这种零副作用技术避免了创建临时探测对象。
PROBE_NAME="rbac-probe-nonexistent"
# Read probesNS_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/web/namespaces/xF5XC_NAMESPACEx")HC_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks")OP_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools")LB_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers")CSD_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/status")PD_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains")MD_R=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/mitigated_domains")
# Write probes (non-destructive: DELETE/cascade_delete on non-existent objects)NS_W=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -X POST -H "Authorization: APIToken xF5XC_API_TOKENx" \ -H "Content-Type: application/json" \ -d "{\"name\":\"$PROBE_NAME\"}" \ "xF5XC_API_URLx/api/web/namespaces/$PROBE_NAME/cascade_delete")HC_W=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks/$PROBE_NAME")OP_W=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools/$PROBE_NAME")LB_W=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/$PROBE_NAME")PD_W=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains/$PROBE_NAME.example.com")MD_W=$(curl -s -o /dev/null -w '%\{http_code\}' --max-time 10 \ -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/mitigated_domains/$PROBE_NAME.example.com")
# Compute deterministic permission matrixjq -n \ --argjson ns_r "$NS_R" --argjson ns_w "$NS_W" \ --argjson hc_r "$HC_R" --argjson hc_w "$HC_W" \ --argjson op_r "$OP_R" --argjson op_w "$OP_W" \ --argjson lb_r "$LB_R" --argjson lb_w "$LB_W" \ --argjson csd_r "$CSD_R" \ --argjson pd_r "$PD_R" --argjson pd_w "$PD_W" \ --argjson md_r "$MD_R" --argjson md_w "$MD_W" \'{ check: "PF-T0-4", permissions: [ { object: "namespace", read: ($ns_r != 403), write: ($ns_w != 403), required: false, note: "conditional — only if ns must be created" }, { object: "healthcheck", read: ($hc_r != 403), write: ($hc_w != 403), required: false, note: "optional for CSD" }, { object: "origin_pool", read: ($op_r != 403), write: ($op_w != 403), required: true, note: "" }, { object: "http_loadbalancer", read: ($lb_r != 403), write: ($lb_w != 403), required: true, note: "" }, { object: "csd_status", read: ($csd_r != 403), write: true, required: true, note: "read-only check" }, { object: "protected_domain", read: ($pd_r != 403), write: ($pd_w != 403), required: true, note: "" }, { object: "mitigated_domain", read: ($md_r != 403), write: ($md_w != 403), required: false, note: "Phase 3 only" } ], status: ( if [ ($op_r == 403), ($op_w == 403), ($lb_r == 403), ($lb_w == 403), ($csd_r == 403), ($pd_r == 403), ($pd_w == 403) ] | any then "FAIL" elif ($ns_w == 403 or $hc_w == 403 or $md_w == 403) then "WARN" else "PASS" end ), detail: ( [ (if ($op_r == 403 or $op_w == 403) then "Origin pool: permission denied" else null end), (if ($lb_r == 403 or $lb_w == 403) then "Load balancer: permission denied" else null end), (if $csd_r == 403 then "CSD API: permission denied — CSD may not be enabled for this namespace" else null end), (if ($pd_r == 403 or $pd_w == 403) then "Protected domain: permission denied" else null end), (if $ns_w == 403 then "Namespace: write denied — namespace must already exist (cannot create)" else null end), (if $hc_w == 403 then "Healthcheck: write denied — will skip healthcheck creation" else null end), (if $md_w == 403 then "Mitigated domain: write denied — Phase 3 mitigation will be skipped" else null end) ] | map(select(. != null)) | join("; ") )}'T1:配额与容量
Section titled “T1:配额与容量”这些检查查询租户的配额使用 API,以确定演示所需的每种对象类型的限制、当前使用量和剩余容量。这用单个只读 API 调用替代了探测-删除测试,该调用报告精确的数字。
PF-T1-0:配额使用关卡
Section titled “PF-T1-0:配额使用关卡”查询租户级配额使用端点,并为演示所需的每种对象类型计算确定性的 PASS/WARN/FAIL 状态。此端点需要 system 命名空间。单个 API 调用可同时检查所有平台级配额。
关卡定义了一个 demo_needs 数组,指定演示将消耗每种对象类型的数量、该类型是否为必需,以及演示继续所需的最低数量。jq 过滤器将 remaining 与 needed 进行比较,并确定性地计算 status 字段 — 无需操作员解读。
# Step 1: Fetch quota datacurl -s \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/web/namespaces/system/quota/usage?namespace=system" \ > /tmp/quota.json
# Step 2: Compute gate statusjq ' . as $data | [ { kind: "healthcheck", needed: 1, required: false, min_proceed: 0 }, { kind: "origin_pool", needed: 1, required: true, min_proceed: 1 }, { kind: "endpoint", needed: 1, required: true, min_proceed: 1 }, { kind: "http_loadbalancer", needed: 2, required: true, min_proceed: 1 } ] | map( . as $req | $data.objects[$req.kind] as $obj | $obj.limit.maximum as $limit | $obj.usage.current as $usage | (if $limit == -1 then null else ($limit - $usage) end) as $remaining | { kind: $req.kind, limit: (if $limit == -1 then "unlimited" else $limit end), usage: $usage, remaining: (if $remaining == null then "unlimited" else $remaining end), needed: $req.needed, status: ( if $remaining == null then "PASS" elif $remaining >= $req.needed then "PASS" elif $remaining >= $req.min_proceed then "WARN" else (if $req.required then "FAIL" else "WARN" end) end ) } ) | { checks: ., gate: (if any(.[]; .status == "FAIL") then "FAIL" elif any(.[]; .status == "WARN") then "WARN" else "PASS" end) }' /tmp/quota.json关卡输出 — gate 字段是单一的确定性判定结果:
PASS— 所有对象类型都有remaining >= needed。演示可以继续。WARN— 至少一种类型容量减少但满足继续的最低要求(例如,只有 1 个 LB 槽位而非 2 个,或健康检查配额已耗尽)。演示可以在有限制的情况下继续。FAIL— 至少一种必需类型的remaining < min_proceed。在释放配额之前演示无法继续。
示例输出(WARN — 端点已达容量,健康检查接近满额):
{ "checks": [ { "kind": "healthcheck", "limit": 150, "usage": 149, "remaining": 1, "needed": 1, "status": "PASS" }, { "kind": "origin_pool", "limit": "unlimited", "usage": 420, "remaining": "unlimited", "needed": 1, "status": "PASS" }, { "kind": "endpoint", "limit": 500, "usage": 500, "remaining": 0, "needed": 1, "status": "FAIL" }, { "kind": "http_loadbalancer", "limit": "unlimited", "usage": 116, "remaining": "unlimited", "needed": 2, "status": "PASS" } ], "gate": "FAIL"}gate 值 | 操作 |
|---|---|
| PASS | 继续到 PF-T1-4(受保护域检查),然后到 T2 |
| WARN | 在就绪报告中记录限制,以降低的能力继续 |
| FAIL | 停止 — 报告哪些类型已耗尽以及下面的修复步骤 |
按类型的修复措施:
| 类型 | 修复措施 |
|---|---|
healthcheck | 删除未使用的健康检查以释放容量。演示可以在没有健康检查的情况下继续(CSD 不需要健康检查)。 |
origin_pool | 删除未使用的源站池或联系管理员增加租户限额。 |
endpoint | 删除其他命名空间中未使用的源站池以释放端点容量(端点是源站池的子对象),或联系管理员。 |
http_loadbalancer | 删除未使用的负载均衡器或联系管理员。如果只有 1 个槽位可用,将创建 HTTP LB(主要),但跳过 HTTPS LB(次要)。 |
PF-T1-4:受保护域配额
Section titled “PF-T1-4:受保护域配额”CSD 受保护域不会出现在平台配额使用 API 中。使用基于探测的检查:创建并立即删除一个探测受保护域。
# Create probe and capture both HTTP code and response bodyPROBE_BODY=$(curl -s -w '\n%\{http_code\}' -X POST \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ -H "Content-Type: application/json" \ -d '{ "metadata": { "name": "preflight-probe.example.com", "namespace": "xF5XC_NAMESPACEx" }, "spec": { "protected_domain": "example.com" } }' \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains")PROBE_HTTP=$(echo "$PROBE_BODY" | tail -1)PROBE_JSON=$(echo "$PROBE_BODY" | sed '$d')
# Compute statusecho "$PROBE_JSON" | jq --argjson http "$PROBE_HTTP" '{ check: "PF-T1-4", http_code: $http, status: ( if $http == 409 then "PASS" elif (.code // 0) == 8 then "FAIL" elif .metadata.name then "PASS" else "FAIL" end ), detail: ( if $http == 409 then "example.com already registered — quota not exhausted" elif (.code // 0) == 8 then "Protected domain quota exhausted — delete unused protected domains" elif .metadata.name then "Probe created — quota available" else "Unexpected response" end )}'
# Cleanup probe (404 is expected if 409 occurred)curl -s -X DELETE \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains/preflight-probe.example.com" \ > /dev/null回退:基于探测的配额检查
Section titled “回退:基于探测的配额检查”如果 PF-T1-0 失败(配额使用 API 返回 403、404 或意外格式),请回退到基于探测-删除的检查来验证健康检查、源站池、端点和负载均衡器配额。这些检查创建一个临时对象并立即删除它 — 如果创建返回错误代码 8 且带有”exhausted limits”消息,则配额已满。
每个回退探测使用相同的模式:创建一个临时对象,从响应中计算确定性状态,然后删除探测。如果对象已创建(.metadata.name 存在),status 字段为 PASS;如果错误代码为 8(限制已耗尽),根据该类型是否为必需,为 WARN 或 FAIL。
健康检查探测(耗尽时为 WARN — 健康检查对 CSD 是可选的):
RESULT=$(curl -s -X POST \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ -H "Content-Type: application/json" \ -d '{ "metadata": { "name": "preflight-quota-probe", "namespace": "xF5XC_NAMESPACEx" }, "spec": { "http_health_check": { "use_origin_server_name": {}, "path": "/", "use_http2": false }, "timeout": 3, "interval": 15, "unhealthy_threshold": 1, "healthy_threshold": 3 } }' \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks")echo "$RESULT" | jq '{ check: "fallback-healthcheck", status: (if .metadata.name then "PASS" elif (.code // 0) == 8 then "WARN" else "FAIL" end), detail: (if .metadata.name then "Quota available" elif (.code // 0) == 8 then "Quota full — healthcheck optional, demo proceeds" else "Unexpected: \(.message // "unknown error")" end)}'curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks/preflight-quota-probe" \ > /dev/null源站池和端点探测(耗尽时为 FAIL — 两者都是必需的):
RESULT=$(curl -s -X POST \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ -H "Content-Type: application/json" \ -d '{ "metadata": { "name": "preflight-origin-probe", "namespace": "xF5XC_NAMESPACEx" }, "spec": { "origin_servers": [{ "public_ip": { "ip": "192.0.2.1" }, "labels": {} }], "no_tls": {}, "port": 80, "same_as_endpoint_port": {}, "healthcheck": [], "loadbalancer_algorithm": "LB_OVERRIDE", "endpoint_selection": "LOCAL_PREFERRED" } }' \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools")echo "$RESULT" | jq '{ check: "fallback-origin-pool", status: (if .metadata.name then "PASS" elif (.code // 0) == 8 then "FAIL" else "FAIL" end), detail: (if .metadata.name then "Quota available" elif (.code // 0) == 8 then "Quota exhausted — \(.message // "limit reached")" else "Unexpected: \(.message // "unknown error")" end)}'curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools/preflight-origin-probe" \ > /dev/nullHTTP 负载均衡器探测(耗尽时为 FAIL — LB 是必需的):
RESULT=$(curl -s -X POST \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ -H "Content-Type: application/json" \ -d '{ "metadata": { "name": "preflight-lb-probe", "namespace": "xF5XC_NAMESPACEx", "disable": false }, "spec": { "domains": ["preflight-probe.example.com"], "http": { "dns_volterra_managed": false, "port": 80 }, "advertise_on_public_default_vip": {}, "default_route_pools": [], "disable_rate_limit": {}, "no_service_policies": {}, "round_robin": {}, "disable_waf": {}, "no_challenge": {}, "disable_bot_defense": {}, "disable_api_definition": {}, "disable_api_discovery": {}, "disable_ip_reputation": {}, "disable_malicious_user_detection": {}, "single_lb_app": { "disable_discovery": {}, "disable_ddos_detection": {}, "disable_malicious_user_detection": {} }, "disable_trust_client_ip_headers": {}, "user_id_client_ip": {}, "disable_threat_mesh": {}, "l7_ddos_action_default": {}, "system_default_timeouts": {}, "default_sensitive_data_policy": {}, "disable_malware_protection": {}, "disable_api_testing": {} } }' \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers")echo "$RESULT" | jq '{ check: "fallback-http-lb", status: (if .metadata.name then "PASS" elif (.code // 0) == 8 then "FAIL" else "FAIL" end), detail: (if .metadata.name then "Quota available" elif (.code // 0) == 8 then "Quota exhausted — \(.message // "limit reached")" else "Unexpected: \(.message // "unknown error")" end)}'curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/preflight-lb-probe" \ > /dev/nullT2:平台前提条件
Section titled “T2:平台前提条件”这些检查验证演示所依赖的租户级服务。
PF-T2-1:CSD 租户状态
Section titled “PF-T2-1:CSD 租户状态”curl -s \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/status" \ | jq '{ check: "PF-T2-1", configured: .isConfigured, enabled: .isEnabled, status: (if .isConfigured and .isEnabled then "PASS" else "FAIL" end), detail: ( if .isConfigured and .isEnabled then "CSD is active" elif (.isConfigured | not) then "CSD not enabled at tenant level — contact F5 XC administrator" else "CSD configured but not active — contact administrator" end ) }'PF-T2-2:DNS 区域存在
Section titled “PF-T2-2:DNS 区域存在”HTTP_CODE=$(curl -s -o /dev/null -w '%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/dns/namespaces/system/dns_zones/xF5XC_ROOT_DOMAINx")echo "{\"http_code\": $HTTP_CODE}" | jq '{ check: "PF-T2-2", http_code: .http_code, status: ( if .http_code == 200 then "PASS" elif .http_code == 404 then "WARN" elif .http_code == 403 then "WARN" else "FAIL" end ), detail: ( if .http_code == 200 then "DNS zone exists in F5 XC" elif .http_code == 404 then "No F5 XC DNS zone — external DNS may be in use" elif .http_code == 403 then "Token lacks DNS zone read access (system namespace)" else "Unexpected HTTP \(.http_code)" end )}'PF-T2-3:DNS 托管记录已启用
Section titled “PF-T2-3:DNS 托管记录已启用”仅在 PF-T2-2 返回 200(F5 XC DNS 区域存在)时运行。
检查当前状态:
curl -s \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/dns/namespaces/system/dns_zones/xF5XC_ROOT_DOMAINx" \ | jq '{ check: "PF-T2-3", managed_records: (.spec.primary.allow_http_lb_managed_records // false), status: (if .spec.primary.allow_http_lb_managed_records == true then "PASS" else "WARN" end), detail: (if .spec.primary.allow_http_lb_managed_records == true then "LB-managed DNS records enabled" else "Managed records not enabled — auto-remediation may be needed" end) }'如需要则自动修复: 如果 status 为 WARN 且 PF-T2-4 显示 F5 XC 域名服务器(ns1.f5clouddns.com、ns2.f5clouddns.com),使用 GET+PUT 自动启用托管记录:
# Get current zone configZONE_CONFIG=$(curl -s \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/dns/namespaces/system/dns_zones/xF5XC_ROOT_DOMAINx")
# Update with managed records enabledecho "$ZONE_CONFIG" \ | jq '.spec.primary.allow_http_lb_managed_records = true' \ | curl -s -X PUT \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ -H "Content-Type: application/json" \ -d @- \ "xF5XC_API_URLx/api/config/dns/namespaces/system/dns_zones/xF5XC_ROOT_DOMAINx" \ | jq .然后重新检查以确认更新已生效:
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'| 结果 | DNS 权威 | 状态 | 修复措施 |
|---|---|---|---|
true | 任何 | PASS | LB 托管 DNS 记录将自动创建 |
false/null | F5 XC | 自动修复 | 通过 GET+PUT 启用,验证,报告结果 |
false/null | 外部 | 信息 | 托管记录不适用于外部 DNS — 阶段 1 步骤 4 将使用选项 B(手动记录创建) |
| 自动修复失败 | F5 XC | FAIL | 令牌可能缺少系统命名空间写入权限 — 联系租户管理员 |
PF-T2-4:DNS 域名服务器权威
Section titled “PF-T2-4:DNS 域名服务器权威”NS_RECORDS=$(dig +short NS xF5XC_ROOT_DOMAINx)echo "$NS_RECORDS" | jq -Rs '{ check: "PF-T2-4", nameservers: (split("\n") | map(select(length > 0))), status: ( if (split("\n") | map(select(length > 0)) | length) == 0 then "FAIL" elif test("f5clouddns\\.com") then "PASS" else "INFO" end ), detail: ( if (split("\n") | map(select(length > 0)) | length) == 0 then "No NS records — DNS is broken for this domain" elif test("f5clouddns\\.com") then "F5 XC is authoritative — automatic DNS management available" else "External DNS provider — Phase 1 Step 4 will use Option B (manual record creation)" end )}'T3:源站健康
Section titled “T3:源站健康”这些检查验证后端应用程序是否可达。
跳过条件检查: 在运行连接测试之前,计算源站 IP 是否为 RFC 5737 TEST-NET 地址:
echo "xF5XC_ORIGIN_IPx" | jq -Rs '{ check: "PF-T3-skip", origin_ip: (rtrimstr("\n")), is_test_net: (rtrimstr("\n") | test("^192\\.0\\.2\\.|^198\\.51\\.100\\.|^203\\.0\\.113\\.")), status: (if (rtrimstr("\n") | test("^192\\.0\\.2\\.|^198\\.51\\.100\\.|^203\\.0\\.113\\.")) then "SKIP" else "CONTINUE" end), detail: (if (rtrimstr("\n") | test("^192\\.0\\.2\\.|^198\\.51\\.100\\.|^203\\.0\\.113\\.")) then "RFC 5737 TEST-NET address — not routable, connectivity testing skipped" else "Routable IP — proceed with connectivity tests" end)}'如果 status 为 SKIP,将 PF-T3-1 和 PF-T3-2 记录为 SKIP 并转到 T4。否则,运行以下检查。
PF-T3-1:源站服务器连接
Section titled “PF-T3-1:源站服务器连接”HTTP_CODE=$(curl -s -o /dev/null -w '%\{http_code\}' --connect-timeout 10 --max-time 15 \ "http://xF5XC_ORIGIN_IPx:xF5XC_ORIGIN_PORTx/")echo "{\"http_code\": $HTTP_CODE}" | jq '{ check: "PF-T3-1", http_code: .http_code, status: (if .http_code >= 200 and .http_code < 600 then "PASS" elif .http_code == 0 then "WARN" else "WARN" end), detail: ( if .http_code >= 200 and .http_code < 600 then "Origin responding with HTTP \(.http_code)" elif .http_code == 0 then "Origin unreachable from this network — LB may use a different path" else "Unexpected response code \(.http_code)" end )}'PF-T3-2:源站提供 HTML 内容
Section titled “PF-T3-2:源站提供 HTML 内容”仅在 PF-T3-1 返回有效 HTTP 状态时运行:
curl -s --max-time 10 "http://xF5XC_ORIGIN_IPx:xF5XC_ORIGIN_PORTx/" \ | grep -qi '</html>' && echo "PASS: HTML content" || echo "WARN: No HTML detected"| 结果 | 状态 | 修复措施 |
|---|---|---|
PASS: HTML content | PASS | 源站提供 HTML 页面(CSD JS 注入所需) |
WARN: No HTML detected | WARN | 源站可能是纯 API 服务或返回非 HTML 内容 — CSD JS 注入需要 HTML 页面响应 |
T4:环境清洁
Section titled “T4:环境清洁”这些检查验证没有命名的 F5 XC 配置对象从 先前的演示运行中残留 — HTTP 负载均衡器、HTTPS 负载均衡器、 源站池、健康检查、受保护域和缓解域。 T4 关注的是对象级清理:可能与阶段 1 创建冲突的 API 对象是否仍然存在。它不测试 IP 地址、网络连接或源站健康(这些属于 T3 的关注范围)。
运行上面飞行前检查部分的六个飞行前命令。应用决策逻辑确定后续步骤。如果对象存在,在准备阶段执行自动拆除(无需确认)。
同时检查并删除先前中断的飞行前运行遗留的过时探测对象。这些探测仅在配额使用 API 不可用且使用了回退基于探测的检查时创建,或用于始终使用基于探测检查的受保护域探测(PF-T1-4):
# Stale probe cleanup (delete in any order — probes have no dependencies)curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/healthchecks/preflight-quota-probe" \ > /dev/null
curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/preflight-lb-probe" \ > /dev/null
curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/origin_pools/preflight-origin-probe" \ > /dev/null
curl -s -X DELETE -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/shape/csd/namespaces/xF5XC_NAMESPACEx/protected_domains/preflight-probe.example.com" \ > /dev/null每个 DELETE 如果对象存在则返回 200(空 {}),如果不存在则返回 404 — 两者都是预期的。
T5:证书就绪
Section titled “T5:证书就绪”这些检查评估 HTTPS 是否可用或演示是否应计划仅使用 HTTP。
PF-T5-1:近期证书签发历史
Section titled “PF-T5-1:近期证书签发历史”检查是否最近为演示域签发了 Let’s Encrypt 证书。频繁的创建/销毁周期可能耗尽每周速率限制(每个域每周 5 个重复证书)。
PF-T5-2:现有 HTTPS LB 证书状态
Section titled “PF-T5-2:现有 HTTPS LB 证书状态”仅在先前运行中存在 HTTPS LB 时运行(PF-T4 发现了对象):
CERT_BODY=$(curl -s -w '\n%\{http_code\}' \ -H "Authorization: APIToken xF5XC_API_TOKENx" \ "xF5XC_API_URLx/api/config/namespaces/xF5XC_NAMESPACEx/http_loadbalancers/xF5XC_LB_NAMEx-https")CERT_HTTP=$(echo "$CERT_BODY" | tail -1)CERT_JSON=$(echo "$CERT_BODY" | sed '$d')
if [ "$CERT_HTTP" = "404" ]; then echo '{"check":"PF-T5-2","cert_state":null,"status":"SKIP","detail":"No HTTPS LB — certificate state assessed after Phase 1 Step 3"}'else echo "$CERT_JSON" | jq '{ check: "PF-T5-2", cert_state: .spec.cert_state, status: ( if .spec.cert_state == "CertificateValid" then "PASS" elif .spec.cert_state == "AutoCertDomainRateLimited" then "INFO" elif (.spec.cert_state | test("Pending|Started")) then "INFO" else "INFO" end ), detail: ( if .spec.cert_state == "CertificateValid" then "Certificate healthy — HTTPS will work" elif .spec.cert_state == "AutoCertDomainRateLimited" then "Let'\''s Encrypt rate limit hit — plan for HTTP-only demo" elif (.spec.cert_state | test("Pending|Started")) then "Certificate provisioning in progress" else "Certificate state: \(.spec.cert_state // "unknown")" end ) }'fi就绪报告格式
Section titled “就绪报告格式”运行所有层级后,呈现一份综合就绪报告:
## 演示就绪状态:就绪 / 未就绪 / 有警告的就绪
### T0:连接与认证| 检查 | 结果 | 状态 ||---|---|---|| PF-T0-1: API 连接 | 200 | PASS || PF-T0-2: 命名空间访问 | 200 | PASS || PF-T0-3: CSD API 访问 | 200 | PASS |
### T1:配额与容量| 检查 | 类型 | 限制 | 使用量 | 剩余 | 需要 | 状态 ||---|---|---|---|---|---|---|| PF-T1-0: 配额使用关卡 | `healthcheck` | 150 | 148 | 2 | 1 | PASS || PF-T1-0: 配额使用关卡 | `origin_pool` | unlimited | 420 | unlimited | 1 | PASS || PF-T1-0: 配额使用关卡 | `endpoint` | 500 | 498 | 2 | 1 | PASS || PF-T1-0: 配额使用关卡 | `http_loadbalancer` | unlimited | 116 | unlimited | 2 | PASS || PF-T1-0: 配额使用关卡 | **关卡** | — | — | — | — | **PASS** || PF-T1-4: 受保护域 | — | — | — | — | 1 | PASS(探测) |
### T2:平台前提条件| 检查 | 结果 | 状态 ||---|---|---|| PF-T2-1: CSD 租户状态 | 已配置 + 已启用 | PASS || PF-T2-2: DNS 区域存在 | 200 | PASS || PF-T2-3: DNS 托管记录 | true | PASS || PF-T2-4: DNS 域名服务器权威 | f5clouddns.com | PASS |
### T3:源站健康| 检查 | 结果 | 状态 ||---|---|---|| PF-T3-1: 源站连接 | TEST-NET 地址 (192.0.2.1) | SKIP || PF-T3-2: HTML 内容 | TEST-NET 地址 | SKIP |
### T4:环境清洁| 检查 | 结果 | 状态 ||---|---|---|| HTTP LB | 404 | PASS || HTTPS LB | 404 | PASS || 源站池 | 404 | PASS || 健康检查 | 404 | PASS || 受保护域 | 0 | PASS || 缓解域 | 0 | PASS |
### T5:证书就绪| 检查 | 结果 | 状态 ||---|---|---|| PF-T5-2: 证书状态 | SKIP(无 HTTPS LB) | INFO |
### 警告- (列出任何带有上下文的 WARN 或 INFO 条目)整体状态规则:
| 条件 | 状态 |
|---|---|
| 所有 T0–T4 检查通过 | 就绪 |
| 所有 T0–T4 检查通过但 T3 或 T5 有 WARN/INFO | 有警告的就绪 |
| 任何 T0、T1 或 T2 检查为 FAIL | 未就绪 — 在继续之前解决 |
| T4 有遗留对象 | 自动修复(拆除),然后重新检查 |
AI 助手执行协议
Section titled “AI 助手执行协议”本节定义了 AI 助手(Claude Code、Copilot 等)执行 API 自动化步骤的确定性工作流。遵循此协议可消除猜测 — 每个决策点都有定义的解决路径。
变量解析协议
Section titled “变量解析协议”按以下确切顺序解析每个变量。在第一个提供非占位符值的来源处停止:
- 检查
.env文件 — 在仓库根目录查找.env。如果存在,解析所有KEY=VALUE对。 - 检查 shell 环境 — 运行
env | grep F5XC_查找当前会话中已导出的任何值。 - 识别缺失值 — 将已解析的值与下面必需/可选表进行比较。如果值不存在、为空或仍设置为占位符默认值(例如
example-api-token、example-tenant、example-namespace、app.example.com、user@example.com),则视为”缺失”。 - 提示人工操作员 — 对于每个缺失的必需变量,要求操作员提供值。在所有必需变量解析完成之前不要继续。
- 应用默认值 — 对于每个缺失的可选变量,使用下表中的默认值,无需提示。
空字符串 = 缺失: 以空值导出的变量 (例如
F5XC_HC_NAME="")与未设置的变量视为相同 — 应用默认值。使用 shell 参数扩展 (${F5XC_HC_NAME:-csd-hc})一步应用默认值。
- 显示确认 — 向操作员显示最终解析的变量表,等待批准后再执行任何 API 调用。
准备阶段覆盖: 在第 1 阶段准备期间,跳过 步骤 6 中的等待。显示已解析的变量表作为记录,然后 立即继续。步骤 1–5 仍然适用 — 如果在检查
.env和 shell 后 任何必需变量仍然缺失,则停止并报告 缺失的变量。
必需与可选变量
Section titled “必需与可选变量”| 变量 | 必需 | 默认值 | 占位符(视为缺失) |
|---|---|---|---|
F5XC_API_TOKEN | 是 | — | example-api-token |
F5XC_API_URL | 是 | — | https://example-tenant.console.ves.volterra.io |
F5XC_NAMESPACE | 是 | — | example-namespace |
F5XC_DOMAINNAME | 是 | — | app.example.com |
F5XC_ROOT_DOMAIN | 是 | — | example.com |
F5XC_LB_NAME | 是 | — | example-lb-name、example-lb |
F5XC_EMAIL | 是 | — | user@example.com |
F5XC_HC_NAME | 可选 | csd-hc | — |
F5XC_ORIGIN_IP | 可选 | 44.232.69.192 | — |
F5XC_ORIGIN_POOL | 可选 | csd-origin | — |
F5XC_ORIGIN_PORT | 可选 | 3000 | — |
AI 助手在演示期间以三种模式之一运行:
| 模式 | 激活时机 | 行为 |
|---|---|---|
| 正常 | 准备、执行、拆除期间的默认模式 | 仅使用逐字记录的命令 |
| 调试 | 失败时自动激活 | 创造性故障排除,更新文档 |
| 问答 | 问答阶段 | 即兴发挥 — 允许临时命令回答观众问题 |
正常模式(默认):
- 每个 API 调用、验证查询和 shell 命令必须逐字来自阶段文件(阶段 1–4)或上面的飞行前检查部分
- 仅将
xTOKENx占位符替换为已解析的变量值 - 不要根据通用知识或推理构造 API 端点、jq 过滤器或 cURL 命令
- 如果所需命令未记录在文档中,停止并向操作员报告:“此验证步骤未在阶段文档中涵盖”
调试模式(失败时自动激活):
- 当记录的命令产生意外结果时自动激活:非 2xx HTTP 响应、jq 解析错误、命令超时或与证据表矛盾的响应正文
- 在调试模式下,AI 助手可以构造诊断命令、检查原始 API 响应、测试端点变体,并使用创造性故障排除来找到根本原因
- 所有调试输出以
[DEBUG]为前缀,以便操作员能够区分诊断活动和正常执行 - 记录您学到的内容:在解决问题后,使用以下内容更新相关阶段文件或故障排除部分:
- 失败场景(发生了什么问题)
- 尝试过但未奏效的方法(以便将来的运行不会重复)
- 有效的解决方案(解决问题的命令或修复方法)
- 调试模式的目标是消除自身 — 每次调试会话都应产生文档更新,使下次执行完全确定
- 文档更新且问题解决后,返回正常模式并从最后一个成功的记录步骤恢复
- 如果调试模式无法解决问题,向操作员报告发现并停止 — 不要继续到下一个阶段
问答模式(问答阶段期间):
- 仅在问答会议阶段(演示结论之后)活动
- AI 助手可以构造临时 API 调用、运行诊断命令、导航到未脚本化的页面,并修改实时演示环境以说明对观众问题的回答
- 没有
[DEBUG]前缀 — 这是故意的即兴行为,不是错误恢复 - 使用
DEMO_EXECUTOR.md中的 CSD 产品专业知识部分作为产品问题的知识库
浏览器上下文管理
Section titled “浏览器上下文管理”initScript 累积是演示失败的常见原因。每次使用 initScript 参数的 navigate_page 调用都会将脚本添加到一个持久列表中,该列表在每次后续文档加载时运行。请遵循以下规则:
- 在使用
initScript进行任何导航之前,始终导航到about:blank,以清除先前运行累积的脚本 - 在演示阶段之间切换时(例如阶段 2 → 阶段 3),使用带有
isolatedContext的new_page以确保完全干净的浏览器状态 - 从无响应页面恢复 — 如果
take_screenshot或take_snapshot超时,浏览器上下文资源已耗尽。使用带有isolatedContext的new_page创建新的上下文,然后从about:blank导航步骤重试 - 请参阅阶段 2 攻击模拟附注,了解有关瞬态源站故障和资源耗尽恢复的详细指导
证据展示协议
Section titled “证据展示协议”在每次 API 调用后,AI 助手必须使用以下格式向人工操作员呈现结构化证据:
创建步骤(POST):
| 字段 | 值 | 状态 |
|---|---|---|
| HTTP 状态 | 200 | PASS |
| 对象名称 | csd-origin | — |
| 关键属性 | (通过 jq 提取) | — |
在每个创建步骤后,运行 GET 确认对象存在并显示其关键属性。如果 GET 返回 404,报告 FAIL 并停止。
验证步骤(GET/dig):
| 测试 | 结果 | 状态 |
|---|---|---|
| DNS-1: A 记录 | 198.51.100.10 | PASS |
| LB-1: HTTP LB 状态 | VIRTUAL_HOST_READY | PASS |
| LB-2: HTTPS LB 状态 | VIRTUAL_HOST_READY | INFO(可选) |
| TLS-1: 证书状态 | CertificateValid | INFO(可选) |
| CSD-1: JS 标签 | scriptTag 存在 | PASS |
参考诊断与验证测试用例 ID(DNS-1、TLS-1、LB-1、CSD-1 等)作为每一层的验证标准。
执行流程摘要
Section titled “执行流程摘要”阶段执行是顺序且有关卡的: 每个阶段必须在所有必需检查上达到 PASS 后才能开始下一阶段。演示遵循四阶段会议生命周期 — 请参阅 DEMO_EXECUTOR.md 中的会议阶段部分了解触发短语和行为规则。
AI 助手遵循以下序列:
- 准备 — 解析变量,运行飞行前检查,确认干净环境(可在会议前单独运行)
- 介绍 — SE 自我介绍并阐述成果目标(对客户端威胁的可见性、PCI 合规性、实时检测)
- 执行阶段 1(步骤 1–7)— 基础设施创建和验证;在继续之前所有阶段 1 检查必须通过
- 执行阶段 2(步骤 8–9)— 通过 API 进行攻击模拟和检测验证;具有浏览器自动化功能的 AI 助手直接执行浏览器步骤,没有浏览器工具的操作员手动执行
- 执行阶段 3 — 对所有检测到的域应用缓解,重新运行攻击,验证阻止是否有效
- 结论 — 重申成果目标,总结每个阶段的证据,突出关键检测和缓解
- 问答 — 即兴阶段,演示保持实时状态,SE 回答观众问题并提出回馈问题
- 拆除(会议后)— 阶段 4,需要操作员明确确认,按反向依赖顺序删除所有对象,确认环境干净
如果任何步骤返回 FAIL,在继续之前停止并报告失败及相关故障排除部分链接。
- 一个 F5 XC API 令牌 — 在 Administration → Credentials → API Credentials 下生成
- 本地安装
curl和jq - 一个具有创建健康检查、源站池和 HTTP 负载均衡器权限的命名空间
使用您的环境值创建 .env 文件。仓库中提供了一个模板:
cp .env.example .env使用您的实际值编辑 .env:
# Required — your environmentF5XC_API_TOKEN=example-api-tokenF5XC_API_URL=https://example-tenant.console.ves.volterra.ioF5XC_DOMAINNAME=app.example.comF5XC_EMAIL=user@example.comF5XC_LB_NAME=example-lb-nameF5XC_NAMESPACE=example-namespaceF5XC_ROOT_DOMAIN=example.com
# Optional — Juice Shop demo defaults are used when not set# F5XC_HC_NAME=csd-hc# F5XC_ORIGIN_IP=44.232.69.192# F5XC_ORIGIN_POOL=csd-origin# F5XC_ORIGIN_PORT=3000将文件加载到 shell 会话中:
set -a && source .env && set +acurl 命令中的每个 xTOKENx 占位符直接对应一个环境变量 — 例如,xF5XC_API_TOKENx 对应 $F5XC_API_TOKEN。您可以使用页面顶部的交互式表单替换这些值,或让 Claude Code 等 AI 助手读取您的 .env 并为您构建命令。
| 令牌 | 默认值 | 描述 |
|---|---|---|
xF5XC_API_URLx | https://example-tenant.console.ves.volterra.io | XC 控制台 API URL |
xF5XC_API_TOKENx | example-api-token | API 凭据令牌 |
xF5XC_EMAILx | user@example.com | CSD 通知邮箱地址 |
xF5XC_NAMESPACEx | example-namespace | 命名空间 |
xF5XC_LB_NAMEx | example-lb | HTTP 负载均衡器基础名称(创建 ${name}-http 和 ${name}-https) |
xF5XC_DOMAINNAMEx | app.example.com | 要保护的 FQDN |
xF5XC_ROOT_DOMAINx | example.com | 根域(eTLD+1),用于 CSD 受保护域 |
xF5XC_ORIGIN_POOLx | csd-origin | 源站池名称 |
xF5XC_ORIGIN_IPx | 44.232.69.192 | 源站服务器 IP |
xF5XC_ORIGIN_PORTx | 3000 | 源站服务器端口 |
xF5XC_HC_NAMEx | csd-hc | 健康检查名称 |
本节总结了用于脚本或自动化的完整练习工作流。
- 克隆仓库并复制环境模板:
cp .env.example .env - 使用您的租户 URL、API 令牌、命名空间和域值编辑
.env - 加载环境:
set -a && source .env && set +a - 按顺序执行每个阶段,在继续到下一阶段之前验证每个证据块中的 PASS
值使用AI 助手执行协议中定义的确定性协议进行解析:
.env文件 — 从仓库根目录解析KEY=VALUE对- Shell 环境 — 检查
env | grep F5XC_获取已导出的值 - 占位符检测 — 将匹配占位符默认值(例如
example-api-token、example-namespace)的任何值标记为缺失 - 提示操作员 — 询问每个缺失的必需变量
- 应用默认值 — 对缺失的可选变量使用内置默认值
- 确认 — 显示已解析的变量表并等待操作员批准
- 阶段 1 — 构建:部署基础设施(健康检查、源站池、HTTP LB + HTTPS LB),配置 DNS,启用 CSD,注册受保护域,验证所有组件。HTTP LB 是主要演示目标;HTTPS LB 是可选的。
- 阶段 2 — 攻击:使用
http://URL 在浏览器中运行攻击模拟,等待 5–10 分钟,通过 API 验证检测(/scripts、/detected_domains、/formFields) - 阶段 3 — 缓解:确认干净基线,运行攻击(之前的证明),将每个域 POST 到
/mitigated_domains,验证缓解已应用,使用http://URL 重新运行攻击(之后的证明),展示前后对比 - 阶段 4 — 拆除 (需要人工明确确认):删除 HTTPS LB → HTTP LB → 源站池 → DNS 区域清理(仅手动记录) → 健康检查 → 受保护域。不要删除 DNS 区域。
| 令牌 | 描述 | 默认值 |
|---|---|---|
xF5XC_API_URLx | XC 控制台 API URL | https://example-tenant.console.ves.volterra.io |
xF5XC_API_TOKENx | API 凭据令牌 | (用户提供) |
xF5XC_EMAILx | CSD 通知邮箱 | user@example.com |
xF5XC_NAMESPACEx | 命名空间 | example-namespace |
xF5XC_LB_NAMEx | HTTP 负载均衡器基础名称(创建 ${name}-http 和 ${name}-https) | example-lb |
xF5XC_DOMAINNAMEx | 要保护的 FQDN | app.example.com |
xF5XC_ROOT_DOMAINx | 根域(eTLD+1) | example.com |
xF5XC_ORIGIN_POOLx | 源站池名称 | csd-origin |
xF5XC_ORIGIN_IPx | 源站服务器 IP | 44.232.69.192 |
xF5XC_ORIGIN_PORTx | 源站服务器端口 | 3000 |
xF5XC_HC_NAMEx | 健康检查名称 | csd-hc |
oneOf 选择组
Section titled “oneOf 选择组”HTTP 负载均衡器规范使用 oneOf 选择组,其中每个组必须恰好设置一个选项。在一个组中设置零个或多个选项会导致 422 错误。
与 CSD 相关的关键选择:
| 选择组 | 选项 | CSD 默认值 |
|---|---|---|
client_side_defense_choice | client_side_defense、disable_client_side_defense | client_side_defense |
java_script_choice(CSD 中嵌套) | disable_js_insert、js_insert_all_pages、js_insert_all_pages_except、js_insertion_rules | js_insert_all_pages |
监听器类型选择(HTTP 与 HTTPS):
演示创建两个具有不同监听器配置的 LB:
| LB | 监听器选择 | 配置 |
|---|---|---|
${F5XC_LB_NAME}-http(主要) | http | "http": { "dns_volterra_managed": true, "port": 80 } |
${F5XC_LB_NAME}-https(次要) | https_auto_cert | "https_auto_cert": { "http_redirect": true, "port": 443, ... } |
http 和 https_auto_cert 键是互斥的 — 每个 LB 恰好使用一个。
HTTPS 自动证书嵌套选择(仅次要 LB):
| 选择组 | 选项 | 默认值 |
|---|---|---|
| port | port(数字) | 443 |
server_header_choice | default_header、server_name、append_server_name | default_header |
path_normalize_choice | enable_path_normalize、disable_path_normalize | enable_path_normalize |
mtls_choice | no_mtls、use_mtls | no_mtls |
default_loadbalancer_choice | default_loadbalancer、non_default_loadbalancer | default_loadbalancer |
single_lb_app 嵌套选择(ML 配置):
single_lb_app 对象有自己的必需 oneOf 组。设置 single_lb_app: \{\} 而不包含这些嵌套选择会导致 400 错误。
| 选择组 | 选项 | 默认值 |
|---|---|---|
api_discovery_choice | disable_discovery、enable_discovery | disable_discovery |
ddos_detection_choice | disable_ddos_detection、enable_ddos_detection | disable_ddos_detection |
malicious_user_detection_choice | disable_malicious_user_detection、enable_malicious_user_detection | disable_malicious_user_detection |
其他 LB 级选择(全部设置为禁用/默认):
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 令牌无效或已过期。在 Administration → Credentials 下重新生成。
- 403 Forbidden — 令牌缺少该命名空间的权限。检查角色绑定。
- 404 Not Found — 命名空间或对象名称不正确。使用
GET /api/config/namespaces/\{namespace\}/\{object_type\}列出对象。 - 409 Conflict — 对象已存在。对于受保护域,带有”domain already exists (in uriList)“的
409表示根域已在租户上注册 — 这是成功条件,而非错误。对于其他对象,先删除或使用PUT更新。 - 422 Unprocessable Entity — JSON 模式验证失败。常见原因:缺少
oneOf选择、在同一组中设置了多个选择、字段类型错误。检查错误消息以获取具体字段。 - 对象限制已耗尽(错误代码
8,消息"Object kind {kind} has exhausted limits({N})")— 租户已达到对象配额限制。API 返回带有 JSON 错误正文(包含"code": 8)的 HTTP 200,而非 HTTP 429。您可以在遇到此错误之前使用配额使用 API(GET /api/web/namespaces/system/quota/usage?namespace=system)主动检查配额容量。行为取决于对象类型:- 健康检查(限制 ~150):非阻塞 — 跳过阶段 1 步骤 1 并在没有健康检查引用的情况下创建源站池。CSD 不依赖健康监控。
- 端点(限制 ~500):阻塞 — 端点是在源站池内创建的子对象。如果达到此限制,源站池创建将失败。删除未使用的源站池(这将释放其端点子对象)或联系管理员增加租户限额。
- 源站池:阻塞 — 删除未使用的源站池或联系管理员。
- HTTP 负载均衡器:阻塞 — 删除未使用的负载均衡器或联系管理员。
- 受保护域:阻塞 — 删除未使用的受保护域或联系管理员。
健康检查未链接到源站池
Section titled “健康检查未链接到源站池”如果阶段 1 步骤 2(验证健康检查已链接)显示空数组 []:
- 删除源站池:
DELETE /api/config/namespaces/{namespace}/origin_pools/{pool_name} - 验证健康检查存在:
GET /api/config/namespaces/{namespace}/healthchecks/{hc_name} - 使用正确的健康检查引用重新创建源站池
LB 卡在 VIRTUAL_HOST_PENDING_A_RECORD
Section titled “LB 卡在 VIRTUAL_HOST_PENDING_A_RECORD”如果负载均衡器 state 在阶段 1 步骤 4 之后仍为 VIRTUAL_HOST_PENDING_A_RECORD:
-
验证 DNS 区域存在(仅 F5 XC 托管 DNS):
Terminal window curl -s \-H "Authorization: APIToken xF5XC_API_TOKENx" \"xF5XC_API_URLx/api/config/dns/namespaces/system/dns_zones/xF5XC_ROOT_DOMAINx" \| jq '.metadata.name'404表示 DNS 区域尚未在 F5 XC 中创建。 -
检查
allow_http_lb_managed_records— 如果区域存在但 LB 处于挂起状态,托管记录可能已被禁用: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'如果为
false或null,使用阶段 1 步骤 4,选项 A 中的PUT命令启用它。 -
外部 DNS — 如果 F5 XC 不是权威的,请在您的 DNS 提供商处手动创建 A 和 ACME CNAME 记录(参见阶段 1 步骤 4,选项 B)。
-
验证解析 — 修复 DNS 后,确认:
Terminal window dig +short xF5XC_DOMAINNAMEx A如果 A 记录解析成功,LB 将转换为
VIRTUAL_HOST_READY。每 30 秒轮询一次,最多 4 次迭代(共 2 分钟)。如果 2 分钟后仍为VIRTUAL_HOST_PENDING_A_RECORD,重新检查 DNS 传播并向操作员报告。
CSD 状态显示 isConfigured: false
Section titled “CSD 状态显示 isConfigured: false”CSD 必须在租户级别启用。联系您的 F5 XC 管理员为您的租户启用客户端防御。这是一个租户级设置,无法通过 API 配置。
JS 注入不工作
Section titled “JS 注入不工作”- 验证 CSD 在租户级别已启用(阶段 1 步骤 5)
- 确认负载均衡器的规范中设置了
client_side_defense - 检查 JS 配置端点是否返回
scriptTag - 在浏览器中访问受保护域并查看页面源代码以确认脚本已注入
受保护域注册返回 409
Section titled “受保护域注册返回 409”带有”domain already exists (in uriList)“的 409 表示根域已在租户上注册。受保护域是租户范围的 — 它们不属于任何单一命名空间。这是成功条件:域已受保护,无需进一步操作。继续到阶段 1 步骤 7。
AutoCertDomainRateLimited
Section titled “AutoCertDomainRateLimited”如果 HTTPS LB cert_state 显示 AutoCertDomainRateLimited,这意味着 Let’s Encrypt 已对此域的证书签发进行了速率限制。这在频繁创建和销毁基础设施的演示环境中是预期的。
影响: HTTPS LB 在速率限制重置(通常为 1 小时)之前将无法服务流量。HTTP LB 完全不受影响 — 所有演示流量通过 http:// 正常进行。
解决方案: 演示推进无需任何操作。HTTP LB(${F5XC_LB_NAME}-http)是主要演示目标,不依赖于证书配置。如果需要 HTTPS,等待速率限制重置,证书将自动配置。
证书卡住 — 清洁重建(可选)
Section titled “证书卡住 — 清洁重建(可选)”当 HTTPS LB 证书在 DomainChallengePending 或 PreDomainChallengePending 状态超过 15 分钟时,最快的恢复路径是删除 HTTPS LB 并重新创建。在 DNS 区域已配置(托管记录已启用)的情况下,清洁重建通常在 5-7 分钟内产生 CertificateValid — 除非被速率限制。
- 删除 HTTPS 负载均衡器:
DELETE .../http_loadbalancers/${F5XC_LB_NAME}-https - 等待 30 秒让平台清理
- 重新创建 HTTPS 负载均衡器(阶段 1 步骤 3)
- 监控证书状态(阶段 1 步骤 7)— 预期在 5-10 分钟内达到
CertificateValid
OpenAPI 参考
Section titled “OpenAPI 参考”F5 Distributed Cloud 的权威 API 规范可在以下位置获取:
https://docs.cloud.f5.com/docs-v2/downloads/f5-distributed-cloud-open-api.zip
此 ZIP 包含所有 API 组的 OpenAPI 3.0 规范,包括 ves.io.schema.views.http_loadbalancer、ves.io.schema.healthcheck、ves.io.schema.origin_pool 和 ves.io.schema.shape.csd。使用这些规范验证 JSON 负载并发现其他字段。