# Azure lab verification: bounded observations

**Run date:** 2026-10-27, Australia/Sydney (UTC events on 2026-10-26).

**Environment:** one author-authorized disposable resource group in Australia East, public Azure cloud. Synthetic empty Blob container, two Ubuntu VMs with separate system-assigned identities, dedicated private endpoint and private DNS, explicit NAT egress. No production incident, customer data or exploitation.

This report records the authoring assistant's execution under the author's explicit lab authorization. It is not independent human/multi-model assurance, a certification or proof of universal isolation. Raw token-free records remain in restricted session storage outside the repository; subscription/principal IDs and endpoint names are omitted here.

## Preflight and actual scope

- Subscription enabled; Compute, Network and Storage providers registered.
- Regional VM-core and B-series quota sufficient; `Standard_B1s` had no reported subscription/location restriction.
- Ubuntu image resolved to `Canonical:ubuntu-24_04-lts:server:24.04.202610210`, x64 Gen2. Guest Python reported 3.12.3.
- Current subscription permission inspection allowed group/deployment/compute/network/storage writes, action Run Command, role assignment and group deletion. ARM deployment validation then succeeded. This is not a portable least-privilege role definition.
- The visible subscription policy assignment concerned SQL VM/Arc protection; deployment succeeded under actual policies. This does not certify that another subscription has equivalent policy.
- Public USD retail checks returned B1s Linux at 0.0132/hour each, NAT Gateway at 0.045/hour, Standard static IPv4 at 0.005/hour and a Standard private endpoint at 0.01/hour. These fixed components total 0.0864/hour; disks, DNS, storage and usage are additional. The private-zone listing was 0.50 for the first pricing tier; actual billing/proration must be checked separately.
- These are retail estimates, not the final Azure invoice or a measured charge. No hard resource-group spending cap is claimed. Deployment began at 21:00:27 UTC; immediate cleanup followed the bounded experiment.

The lab deployed from `main.bicep`. Only the worker NSG was subsequently changed through `containment.bicep`; the peer, DNS, identities, Storage grants and routing were not intentionally changed during containment/restoration.

## Actual request observations

Each row is one fresh read-only container-list request from a new Python process. The probe recorded the Blob name resolving to **10.77.2.4** in all six cases and did not print tokens or Blob bodies.

| Phase | Source | Started UTC, 2026-10-26 | Finished UTC | Observed service result |
|---|---|---|---|---|
| Baseline | Worker | 21:06:13.772821 | 21:06:14.677300 | HTTP 200 |
| Baseline | Healthy peer | 21:06:51.113493 | 21:06:51.494642 | HTTP 200 |
| Contained | Worker | 21:12:41.076355 | 21:12:56.150492 | NO_RESPONSE after the configured service timeout |
| Contained | Healthy peer | 21:13:12.531270 | 21:13:12.643204 | HTTP 200 |
| Restored | Worker | 21:15:25.763363 | 21:15:25.860586 | HTTP 200 |
| Restored | Healthy peer | 21:15:57.188738 | 21:15:57.309212 | HTTP 200 |

During containment, collected effective NSG evidence included `ContainStorageHttps` on the worker and no matching rule on the peer. The configured rule was outbound Deny, TCP/443, destination `10.77.2.4/32`, priority 100. Effective-route snapshots were also retained privately. Before cleanup, the worker rule inventory confirmed that the temporary deny had been removed.

These observations support a bounded changed outcome while the healthy peer's sampled operation remained available. They do **not** establish packet-level causation, absence of alternate paths, continuous availability or termination of an existing session. No packet trace, service-side denial log or independent identity attestation was collected.

## Decision and validator evidence

D1 was recorded after successful baseline observations and before applying containment, under the author's disposable-lab authorization. It named accepted worker impact, preserved peer access, rejected broad shared-service controls, a review deadline, rollback triggers and an evidence owner.

A separate restoration record/time was preserved after inspecting the contained outcomes and before removing the synthetic deny. The same operator acted throughout; no independent approver, separate recovery channel or cross-shift acceptance is invented.

The six real probe records plus the actual decision/watch record produced:

```json
{
  "status": "INCONCLUSIVE",
  "record_structure": "VALID",
  "evidence_class": "OBSERVED",
  "comparison": "BOUNDED_WORKER_NO_RESPONSE; inspect healthy controls and gaps",
  "errors": [],
  "gaps": [
    "Worker received no service response: compatible with network containment, NOT proof of a network denial or its cause"
  ]
}
```

Exit code was **2**, by design. The validator worked with the actual six-cell record without converting a timeout into proof of isolation. The record is a historical snapshot, not an assertion of current health after the lab is removed.

## Runtime issues found and repaired

| Finding | Observed failure | Fix and verification |
|---|---|---|
| Windows multiline CLI argument delivery | VM agent and Python were healthy, but inline heredoc invocation returned no probe output | Send an LF-only, BOM-free shell script with Azure CLI `@file` input. All six actual probes then emitted structured output; offline tests enforce file input and line endings |
| Human-formatted what-if output | JSON parsing failed on explanatory output; no change applied | Use `--no-pretty-print` and require an explicit machine-readable change list |
| Full-template what-if noise | Defaults and unresolved references produced modifications outside the worker NSG; the guard refused deployment | Add a separate incremental worker-NSG-only response template instead of relaxing the guard. Both containment and restoration then succeeded |
| PowerShell JSON array enumeration | Cleanup treated the returned resource array as one item and refused the expected inventory | Explicitly enumerate parsed items in the CLI adapter; test known multi-resource cleanup as well as unexpected-resource refusal |

No failed attempt was reclassified as a successful probe or denial. No broad controls were removed, no what-if bypass was used and no unrelated resource was adopted to get a green result. The initial run was an exercised-and-repaired development run, not evidence that the original package worked unchanged.

## Corrected-workflow clean replay

After the first group was confirmed deleted, a second dedicated group exercised the corrected deployment, baseline, scoped containment, restoration and evidence validator **without code edits or bypasses during the replay**. The original group's resources were not reused.

| Phase | Source | Started UTC, 2026-10-26 | Finished UTC | Observed service result |
|---|---|---|---|---|
| Baseline | Worker | 21:26:38.241630 | 21:26:38.898547 | HTTP 200 |
| Baseline | Healthy peer | 21:27:10.957244 | 21:27:11.358498 | HTTP 200 |
| Contained | Worker | 21:29:04.870636 | 21:29:19.949398 | NO_RESPONSE after the configured service timeout |
| Contained | Healthy peer | 21:30:07.621620 | 21:30:07.748534 | HTTP 200 |
| Restored | Worker | 21:32:01.559330 | 21:32:01.661945 | HTTP 200 |
| Restored | Healthy peer | 21:32:40.294033 | 21:32:40.394630 | HTTP 200 |

The corrected replay again produced record structure VALID, evidence class OBSERVED, status INCONCLUSIVE, no validation errors and the explicit no-response/causation gap. This establishes a second bounded observation, not a general repeatability guarantee. Its full deployment-to-cleanup window was 21:23:52-21:39:35 UTC.

## Cleanup and remaining limits

For the first run, Azure confirmed the dedicated group's deletion at **21:22:22 UTC**. A separate subscription-filtered check at **21:23:11 UTC** returned group-exists false and **zero remaining resources** for that exact group. Querying inventory directly inside the deleted group returned the expected ResourceGroupNotFound error; it was not silently treated as an empty successful query.

The corrected replay also completed automated cleanup: at **21:39:35 UTC**, the second group was confirmed absent and a subscription-filtered inventory contained **zero remaining resources** for that group. Azure briefly reported the group as Deleting even after its resource inventory was empty; completion was not claimed until group absence was confirmed.

Both dedicated SSH key pairs were removed after confirmed cloud cleanup. A private SHA-256 manifest preserves provenance for token-free raw JSON evidence; neither those records nor subscription/principal identifiers are bundled into the reader ZIP. Public downloads and final billing still require separate verification; no site publication occurred during these tests.

Not tested: token revocation, identity rebuild/federation repair, established sessions, shared identities, competing reconciliation, automatic drift alerts, DNS/route failure and repair, independently trusted recovery channels, real incident outcomes or workshop metrics. Those limitations remain in the articles and toolkit guide.
