# Validation record — Application Gateway WAF workshop **Status: PASSED — live Azure acceptance and verified cleanup completed on 6 November 2026.** All three fault/recovery cycles, final correlated validation and repeated cleanup passed. The [sanitized acceptance export](docs/evidence/sanitized-evidence.json) records complete=true, missing_required_checks=[] and status=cleaned. All 20 genuine captures have reviewed provenance. ## Tested configuration and evidence boundaries | Item | Recorded configuration | |---|---| | Live run date / region | 6 November 2026 / West Europe | | Gateway / policy | Application Gateway WAF_v2; Microsoft Default Rule Set 2.2, Prevention | | Backend | Private Standard_B2s Windows Server 2025 IIS VM; no public VM IP | | TLS | Separate frontend/backend certificates from a disposable private CA; CA-chain and hostname checks enabled | | Local operator | PowerShell 7.6.9, Azure CLI 2.88.0, Terraform 1.15.8 | | Infrastructure provider | AzureRM 5.8.0 with committed checksum lockfile | | Screenshot execution context | Genuine Portal/Cloud Shell; captions distinguish retained local reports from fresh remote commands | | Operating allowance | €20 total, €5 reserved for cleanup; immutable Preflight-based exercise/deletion deadlines | Raw logs, certificate private keys, credentials, state/plans, full resource identities and unredacted captures remain private. The public reports contain allowlisted observations and source hashes. A screenshot of configuration cannot prove a successful request, and a successful delete request cannot prove absence. ## Local checks — separate from Azure acceptance | Check | Observed local result | Record | |---|---|---| | Controller/common tests | 54 passed, zero failed/skipped/not-run under Pester 5.7.1 | [Controller tests](docs/evidence/local-controller-tests.json) | | PowerShell analysis | Zero Warning/Error findings under PSScriptAnalyzer 1.25.0; the internal-helper execution-gate suppression is documented | [Controller tests and analysis](docs/evidence/local-controller-tests.json) | | Terraform | Formatting and validation passed against AzureRM 5.8.0 | [Infrastructure audit](docs/evidence/local-infrastructure-validation.json) | | Certificates/bootstrap | Leaf chain, server-auth EKU, root PEM/DER agreement and Windows PowerShell 5.1 compatibility checks passed; operator trust stores unchanged | [Infrastructure audit](docs/evidence/local-infrastructure-validation.json) | | Documentation | PowerShell snippets parse; arguments match controller; relative links, anchors and external references checked | [Documentation audit](docs/evidence/docs-quality.json) | | Release screenshot gates | Ten synthetic local tests passed for required/supplemental capture acceptance and rejection conditions | [Capture gate tests](docs/evidence/release-capture-gate-tests.json) | The controller-stage tests stub Azure calls. Native transport tests run local processes. These checks do not replace live deployment, HTTP, health, rule-decision, recovery or cleanup observations. Synthetic release-gate fixtures are not screenshots of Azure. ## Observed Azure checks | Group | Current observed result | |---|---| | Initial deployment/bootstrap | Completed; the private IIS backend and trusted HTTPS path were established | | Calibrated healthy baseline | Legitimate labSnippet HTTP 200 at 18:06:54 UTC; the same payload in untrustedSnippet HTTP 403 at 18:06:55 UTC; baseline Terraform no-drift at 18:07:28 UTC | | Probe path fault | Passed: only probe-settings became Unhealthy with a missing-path 404 diagnostic; its route returned 502 while control/TLS routes remained healthy and returned 200 | | Probe recovery / repeated repair | Passed: restored route HTTP 200 after each of two separate repair/validation windows | | Backend TLS hostname fault | Passed: actual leaf-certificate/expected-name diagnostic and route HTTP 502; control and probe paths remained healthy | | Backend TLS recovery / repeated repair | Passed: restored expected name, healthy backend and HTTP 200 after each of two repair/validation windows | | WAF deliberate fault / recovery | Passed: actual managed-rule HTTP 403 fault, first repair and HTTP 200 recovery, repeated repair and a second HTTP 200 recovery; the nonexcluded field remained blocked | | Final access/firewall transaction correlation and no drift | Passed at 18:27:50 UTC after all scenarios; 55 access rows included 200/403/502 and both exercise 502 paths; 27 firewall rows included a correlated custom block and DRS 2.2 managed-rule evidence. Fresh Terraform detailed exit code was 0 at 18:27:44 UTC | | Cleanup / repeated cleanup / protected baseline | Passed: exact group absence at 18:40:20.915 UTC; all 27 pre-existing resources preserved; repeated no-op cleanup confirmed absence at 18:41:18.416 UTC | | Genuine capture set | Passed: all 18 required images plus two supplemental Portal views, 20 total (11 Portal / nine CLI), with matching hashes and privacy-reviewed provenance | ### Fault and recovery observation timeline Times below are UTC and come from the retained live reports. The separately exported backend-health record retains source hashes and nearest healthy observations around each actual fault. | Scenario | Observation | UTC | |---|---|---| | Probe | Intended 502 fault validation completed | 18:10:33.6441912 | | Probe | First repair completed | 18:11:34.9472283 | | Probe | First recovery validation completed | 18:12:16.9821773 | | Probe | Repeated repair completed | 18:12:37.2245734 | | Probe | Second recovery validation completed | 18:13:39.6495125 | | TLS | Intended 502 fault validation completed | 18:17:53.8308976 | | TLS | First repair completed | 18:18:54.6941367 | | TLS | First recovery validation completed | 18:20:02.4918540 | | TLS | Repeated repair completed | 18:20:19.7786435 | | TLS | Second recovery validation completed | 18:22:21.6778135 | | WAF | Intended managed-rule 403 fault validation completed | 18:23:49.1500689 | | WAF | First repair completed | 18:24:11.3830842 | | WAF | First recovery validation completed | 18:24:53.4472292 | | WAF | Repeated repair completed | 18:25:27.1005623 | | WAF | Second recovery validation completed | 18:26:12.6432686 | Records: [Probe repair report](docs/evidence/probe-repair-report.txt), [TLS repair report](docs/evidence/tls-repair-report.txt), [WAF repair report](docs/evidence/waf-repair-report.txt), and [actual backend-health observations](docs/evidence/backend-health-observations.json). The latter records the real missing-path and certificate-name diagnostics. The downloadable [backend-health export helper](scripts/Export-BackendHealthEvidence.ps1) extracts only its allowlisted observations from protected native Azure CLI journals; it does not generate or simulate health results. ## WAF fixture calibration and exclusion scope At 2026-11-06T17:57:01Z, the unexcluded payload `` in labSnippet returned HTTP 403. DRS 2.2 matched XSS rules 941100, 941110 and 941160; aggregation rule 949110 then blocked at anomaly score 15. The [fixture record](fixtures/waf-fixture.json) and [map](fixtures/waf-exclusions.json) pin the request and three contributors. Rule 949110 is not excluded. The exclusion is on the gateway-associated WAF policy. It applies to labSnippet values across **all paths associated with that policy**, not only /docs. Its boundary is the exact field name and three selected XSS rules, not a path restriction or a payload-content allowlist. Other fields and rules, including aggregation rule 949110, remain inspected. All workshop routes are static and do not execute or reflect query input. The calibrated baseline and the later deliberate removal/restoration cycle both passed. The [WAF repair report](docs/evidence/waf-repair-report.txt) records its distinct fault and two recovery windows. The same payload in the nonexcluded field remained blocked after repair. ## Final validation and log correlation The [retained final Validate/CollectLogs report](docs/evidence/final-validation-report.txt) records fresh trusted marker responses, the custom-marker block, legitimate-field HTTP 200, nonexcluded-field HTTP 403, and Terraform detailed exit code 0 at 18:27:44.3956667 UTC. Log acceptance completed at 18:27:50 UTC. The observed 55 access rows include HTTP 200, 403 and 502, including failures for both /probe-fault/ and /tls-fault/. The 27 firewall rows include an access-correlated custom block and managed rules 941100, 941110 and 941160 from DRS 2.2. Six matching managed-rule log rows were correlated to denied access transactions; that row count does not imply six distinct transactions. Aggregation rule 949110 remained enabled and appears in the firewall evidence. The [seven curated request/log examples](docs/evidence/request-log-correlations.json) link representative real access outcomes to their retained firewall decisions using transaction digests; they publish neither full transaction IDs nor private request values. ## Verified cleanup, baseline preservation and operating limits The reviewed Terraform destruction plan covered 19 owned managed objects. Exact group absence was observed at 18:40:20.9150020 UTC. The protected-baseline check at 18:40:23.7323968 UTC found zero missing resources out of the 27 pre-existing resources. Repeated Cleanup was a safe no-op: another baseline check found zero missing resources at 18:41:18.4017685 UTC, followed by confirmed group absence at 18:41:18.4164284 UTC. Fresh independent Azure CLI queries in genuine Cloud Shell confirmed GroupExists=False and zero resources in the exact workshop scope at approximately 18:41:12 UTC and again at 18:42:06 UTC. Records: [first cleanup](docs/evidence/cleanup-report.txt), [repeated cleanup](docs/evidence/repeated-cleanup-report.txt), [capture 17](docs/screenshots/17-cleanup.png) and [capture 18](docs/screenshots/18-repeated-cleanup.png). Cleanup completed in less than 1 hour 35 minutes from the original Preflight, within the immutable exercise and deletion deadlines. The refreshed retail estimate at 18:03:44 UTC was €11.15 for six hours of active resources, plus the €5 cleanup reserve, within the €20 operating allowance. This is a conservative operational estimate, not an actual invoice total or an Azure-enforced spending cap. ## Authoring findings and TLS client control The initial Terraform apply succeeded at approximately 17:32 UTC, after which the controller encountered an empty-array output-ledger type error. Dictionary initialization and safe empty-array migration were corrected. The already-created resources were reconciled from exact Terraform state with a new zero-change Plan/Deploy rather than blindly creating another deployment. Dedicated regression cases now pass. This was an authoring/controller failure, separate from the three deliberate workshop incidents. The first Windows client readiness attempt encountered Schannel's unknown revocation status because the disposable CA has no CRL distribution service. Windows curl was adjusted to best-effort handling of missing/offline CRL distribution points while retaining explicit CA-chain and hostname verification. This was a client precondition finding, separate from the gateway-to-IIS TLS exercise. At 2026-11-06T17:54:01.0655782+00:00, the actual frontend negative control failed with curl exit 60 when wrong-client.lab.test did not match the certificate names. The [sanitized observation](docs/evidence/frontend-tls-negative.json) records the error, supplied trusted CA and enabled chain/name checks. The best-effort adjustment did not make a wrong frontend hostname succeed. ## Captures and archive integrity All 20 genuine published PNGs are reconciled with the [capture inventory](docs/capture-plan.json) and [provenance](docs/screenshots/provenance.json). Captions distinguish actual local execution, later display of retained reports in Cloud Shell, and fresh remote commands. Only cropping and opaque privacy redactions were applied to genuine captures; no reconstructed Portal or terminal UI is used. After the archive is built, its actual extraction, file-hash and private-value scan results are recorded in the [external archive receipt](../application-gateway-waf-workshop.zip.receipt.json). The [archive SHA-256 receipt](../application-gateway-waf-workshop.zip.sha256) is kept outside the ZIP so it identifies the final archive bytes without a circular self-hash. This run establishes the observed workshop behavior and cleanup above. Actual invoice totals, production capacity/availability and behavior in every Azure tenant are outside this short-lived training run's claims.