top of page
Oct 15
28 min read

Azure VNet Flow Logs — Find Unexpected Traffic with Traffic Analytics


Azure CLI · Azure Portal · KQL | 180–240 minutes | Standalone infrastructure workshop


Azure VNet Flow Logs and Traffic Analytics — conceptual workshop cover with two VMs and controlled network traffic
Conceptual Azure networking illustration — not execution evidence.

Build a private client/server exercise, investigate an unexpected maintenance-port connection, and prove a narrow network-rule correction with fresh traffic.



Download SHA-256:


c5e22bf465a28e5cf5fbf6e805317501f1c43c99e8c09050fe73edf3efa56d12

The incident


Your internal application belongs on TCP 8080. A maintenance listener exists on 8081, but clients should not reach it. When fresh requests to 8081 start succeeding, determine which control changed, prove the change from recorded traffic, and restore the intended boundary without taking down the application.


The goal is more than a successful request or a green VM status. Collect three kinds of evidence: the real connection result, the evaluated network control, and its corresponding flow records.


How to use this workshop


CLI commands are copyable blocks; measured sanitized results and genuine Portal images accompany the steps. The six Portal checkpoints cover logging configuration, baseline NSG, fault NSG, restored NSG, Traffic Analytics/KQL, and cleanup. Code explains what to run; measured results establish what happened.


What you will build


  • Component: Resource group

    Workshop configuration: One fresh, uniquely named, lab-owned group in West Europe

  • Component: VNet and subnet

    Workshop configuration: 10.117.0.0/24; workload 10.117.0.0/26; no default outbound access

  • Component: Client

    Workshop configuration: Ubuntu 24.04 LTS B1s; static private address 10.117.0.4; system-assigned identity

  • Component: Server

    Workshop configuration: Ubuntu 24.04 LTS B1s; static private address 10.117.0.5; unprivileged Python service

  • Component: Disks and outbound

    Workshop configuration: A 32 GiB Standard SSD OS disk and Standard static public IPv4 per VM; public inbound denied

  • Component: NSGs

    Workshop configuration: Client inbound denied; server permits client /32 to server /32 TCP 8080 at priority 200, then denies inbound at 4096

  • Component: Logging

    Workshop configuration: One VNet flow-log child under the existing regional Network Watcher

  • Component: Raw logs

    Workshop configuration: Standard_LRS GPv2 storage; flow-log user-assigned identity writes using RBAC

  • Component: Analytics

    Workshop configuration: Dedicated Log Analytics workspace; 31-day retention; 1 GiB daily cap; ten-minute Traffic Analytics processing


Application tests run inside Azure, so no stable-workstation-IP gate is needed. VM public addresses supply explicit outbound connectivity, not a public management path. Administer with Azure CLI Run Command, not SSH.


The storage account requires HTTPS/TLS 1.2, disables anonymous access and Shared Key, and denies unmatched network access. Its exceptions are the recorded service-endpoint subnet and the documented Azure trusted-services path. Two identities have different jobs: the flow-log identity writes at the account scope; the client identity reads only the service-created raw-log container. Managed-identity guidance.


1. Prepare the private run


Open PowerShell 7.5 or newer in the extracted package. Use a new private state path for each independent run. Keep using that exact path until cleanup is verified. Do not install or upgrade tools automatically to get past a failed capability check.


PowerShell / Azure CLI


$state = Join-Path $env:LOCALAPPDATA 'AzureWorkshopz-FlowLogs/run.json'
az version
az account show --query '{name:name,tenant:tenantId,subscription:id}' --output json
pwsh -File .\Start-FlowLogs.ps1 -Stage Preflight -StatePath $state
pwsh -File .\Start-FlowLogs.ps1 -Stage Preflight -StatePath $state -Execute

The executed preflight intentionally stops after writing preflight-private.json beside the private state path. Inspect that file privately, including effective permissions, inherited policy and locks. After the review, rerun:


PowerShell / Azure CLI


pwsh -File .\Start-FlowLogs.ps1 -Stage Preflight -StatePath $state -Execute -ReviewedGovernance

Do not add -ReviewedGovernance without the review. The review stop does not mark preflight passed. A completed preflight creates the manifest and records refreshed pricing. The chosen private folder has an existing parent; the script creates the new child with restrictive ACLs rather than using a shared download directory.


Review the selected subscription and tenant privately. Preflight also checks permissions, providers, applicable policy, locks, quota/SKU availability, address space, existing logging, and current prices; manually review any applicable external automation before confirming. Resolve failed checks; do not register providers, add operator roles, change policy, upgrade tools, or substitute a SKU just to continue. The guarded implementation currently requires full effective management permission, including the recorded lab role assignments; it does not grant that permission to the operator.


Require the refreshed active estimate to fit within EUR15 and preserve EUR5 cleanup reserve. The 1 GiB ingestion cap is a backstop, not a guaranteed spending cap. Set a five-hour exercise-stop reminder and a six-hour cleanup target.


CLI code block and result: retain tool versions and the actual sanitized preflight outcome as structured text. Exclude operator identity, tenant/subscription IDs, local user paths, and tokens. Do not use --debug in published results.


2. Provision the foundation


PowerShell / Azure CLI


pwsh -File .\Start-FlowLogs.ps1 -Stage Foundation -StatePath $state
pwsh -File .\Start-FlowLogs.ps1 -Stage Foundation -StatePath $state -Execute

The script records exact IDs before later operations. Read back the VM, NIC, NSG, disk, subnet, identity, and public-IP relationships. The client and server must be the recorded objects, not similar names found in a subscription search.


For read-only CLI inspection, derive names from the private manifest:


PowerShell / Azure CLI


$run = Get-Content -LiteralPath $state -Raw | ConvertFrom-Json -AsHashtable
$rg = ($run.Targets.Group -split '/')[-1]
$serverVm = ($run.Targets.ServerVm -split '/')[-1]
$serverNic = ($run.Targets.ServerNic -split '/')[-1]
$serverNsg = ($run.Targets.ServerNsg -split '/')[-1]
az vm get-instance-view --resource-group $rg --name $serverVm --output json
az network nic list-effective-nsg --resource-group $rg --name $serverNic --output json
az network nsg rule list --resource-group $rg --nsg-name $serverNsg --output table

These commands can expose real identifiers; keep their unredacted output private. Effective rules must include the exact client-to-server TCP 8080 allowance and the explicit inbound deny. Do not add an SSH exception.


Portal checkpoint: open the lab resource group, VNet/subnet, and each VM's Networking page. Verify the NIC, NSG and address relationships, explicit outbound design, denied SSH, and 32 GiB disk. Include the relevant server rule in the later Baseline image instead of requiring an additional foundation screenshot. Do not edit Portal settings during the CLI-controlled run.


3. Enable VNet flow logs and Traffic Analytics


PowerShell / Azure CLI


pwsh -File .\Start-FlowLogs.ps1 -Stage Logging -StatePath $state
pwsh -File .\Start-FlowLogs.ps1 -Stage Logging -StatePath $state -Execute

$run = Get-Content -LiteralPath $state -Raw | ConvertFrom-Json -AsHashtable
$flowLogName = ($run.Targets.FlowLog -split '/')[-1]
az network watcher flow-log show --location westeurope --name $flowLogName --output json

Verify the target is the recorded VNet, not its NSG. Read back storage, user-assigned identity, workspace and enabled analytics settings. Use the installed CLI capability check rather than copying unsupported flags from another CLI version. Flow-log CLI reference.


Traffic Analytics may create DCR/DCE dependencies in the workspace group. Preserve their returned relationships and operation provenance in private state. Do not edit these managed dependencies or claim that an NWTA name prefix proves ownership. Management guidance.


Portal image — Logging: search for Network Watcher → Logs → Flow logs, open the exact new child, and capture its target, enabled state, identity, storage and analytics configuration. Inspect storage security/network settings and workspace retention/cap separately; their actual CLI readbacks belong in sanitized structured evidence rather than extra required screenshots. Leave other flow logs and the shared Watcher unchanged.


Actual Portal flow-log configuration: VNet target, storage, identity and ten-minute Traffic Analytics processing. Names and identifiers are redacted.
Actual Portal flow-log configuration: VNet target, storage, identity and ten-minute Traffic Analytics processing. Names and identifiers are redacted.

4. Start the synthetic application


PowerShell / Azure CLI


pwsh -File .\Start-FlowLogs.ps1 -Stage Applications -StatePath $state
pwsh -File .\Start-FlowLogs.ps1 -Stage Applications -StatePath $state -Execute

The server listens on its private address on both ports, under an unprivileged service identity. It returns synthetic content and a content hash. Record the local 8080 and 8081 results, service identity and listeners before testing network access. A healthy local 8081 listener is deliberate: the NSG is the client-access boundary in this exercise.


CLI code block and result: retain the actual sanitized Run Command result showing the service and two local listeners. The source block explains implementation; only the real result establishes that it ran.


5. Capture a healthy baseline


PowerShell / Azure CLI


pwsh -File .\Invoke-FlowLogsTraffic.ps1 -Phase Baseline -StatePath $state -Execute
pwsh -File .\Test-FlowLogs.ps1 -Phase Baseline -StatePath $state -Execute

Each request uses a fresh connection and bounded timeout; there is no unlimited load loop. Expected observations are successful 8080 reads with the recorded hash and failed 8081 connections. Record the actual timeout/refusal/result rather than inventing an HTTP status for a network-level denial.


Validation is one attempt, not an automatic hour-long wait loop. If configuration/traffic is healthy but log ingestion is still pending, keep the original observation deadline and retry only within the remaining bounded window. A retry generates another small fresh batch; it does not reset the 60-minute ingestion allowance. Do not start the fault until the full Baseline gate, including raw and analytics evidence, passes.


Record the batch's UTC start/end and the server rule's configuration. Distinguish locally successful listener checks from client-to-server network checks. A VM shown as Running does not establish either path.


Portal image — Baseline: capture the configured 8080-only server application rule. Retain the independent NIC effective-rule readback, actual baseline connection results and matching raw/analytics observations in sanitized JSON/text alongside the CLI code blocks; the configured-rule image alone does not prove effective evaluation.


Actual Portal configured NSG rules before the exposure: the lab client can reach TCP 8080; the explicit inbound deny follows.
Actual Portal configured NSG rules before the exposure: the lab client can reach TCP 8080; the explicit inbound deny follows.

6. Introduce one controlled exposure


Proceed only after baseline acceptance and the configuration snapshot.


PowerShell / Azure CLI


pwsh -File .\Invoke-FlowLogsFault.ps1 -StatePath $state
pwsh -File .\Invoke-FlowLogsFault.ps1 -StatePath $state -Execute
pwsh -File .\Invoke-FlowLogsTraffic.ps1 -Phase Fault -StatePath $state -Execute
pwsh -File .\Test-FlowLogs.ps1 -Phase Fault -StatePath $state -Execute

The only intended configuration change is the recorded Allow rule's destination ports: 8080 becomes 8080-8081. Its source/destination prefixes, priority, protocol, direction and access stay unchanged. Fresh requests to 8081 now succeed; 8080 remains healthy. Service code, payload/hash, NIC and subnet associations must be unchanged.


For teaching purposes, the essential Azure CLI operation inside the guarded script is az network nsg rule update ... --destination-port-ranges 8080-8081. Do not execute a parallel manual update: the wrapper records the snapshot, lock and pending operation needed for safe recovery.


Portal image — Fault: capture the actual widened NSG rule. Keep the successful maintenance response in actual sanitized CLI-result evidence, with the observed UTC window; it does not need a terminal screenshot.


Actual Portal controlled fault: only the recorded application rule destination range is broadened to 8080–8081.
Actual Portal controlled fault: only the recorded application rule destination range is broadened to 8080–8081.

7. Investigate the traffic, not just the rule text


Read the raw source


The service-created insights-logs-flowlogflowevent container holds JSON flow records. The client reads this container using its managed identity and an in-memory OAuth token from IMDS. Do not open the storage firewall to the workstation, fetch keys, print tokens, or grant an operator data role to use Portal's blob browser.


Inspect only the workshop flow log, recorded NIC/MAC relationships, and phase windows. A raw tuple identifies time, endpoints, ports, protocol, direction and flow state; its ACL/rule parent supplies the evaluated control. A deny is a D flow state. These are Layer-4 records, not HTTP bodies or application request markers. Flow-log format.


Do not edit the active log blob. Preserve the original read privately and export only a sanitized excerpt with the matching tuple/rule context intact.


Query the workspace


Open the dedicated workspace's Logs page. Select KQL mode if the Portal starts in a graphical query view. Replace the two UTC timestamps below with one recorded phase window; do not retain the example dates. Query a small ingestion window as well as the flow interval, then compare raw tuples where aggregate boundaries overlap phases.


KQL


let PhaseStart = datetime(2026-01-01T10:00:00Z);
let PhaseEnd = datetime(2026-01-01T10:05:00Z);
let RecordedFlowLogId = "REPLACE_WITH_RECORDED_FLOW_LOG_ID";
let FlowLogParts = split(RecordedFlowLogId, "/");
let RecordedFlowLogShortId = strcat(tostring(FlowLogParts[4]), "/", tostring(FlowLogParts[8]), "/", tostring(FlowLogParts[10]));
let RecordedFlowLogQualifiedId = strcat(tostring(FlowLogParts[2]), "/", RecordedFlowLogShortId);
let TargetVnetId = "REPLACE_WITH_RECORDED_VNET_ARM_ID";
let TargetParts = split(TargetVnetId, "/");
let TargetShortId = strcat(tostring(TargetParts[4]), "/", tostring(TargetParts[8]));
let TargetQualifiedId = strcat(tostring(TargetParts[2]), "/", TargetShortId);
let ServerNsgId = "REPLACE_WITH_RECORDED_SERVER_NSG_ARM_ID";
let ServerNsgParts = split(ServerNsgId, "/");
let RecordedAclGroup = strcat(tostring(ServerNsgParts[2]), "/", tostring(ServerNsgParts[4]), "/", tostring(ServerNsgParts[8]));
let ServerMac = "REPLACE_WITH_RECORDED_SERVER_MAC_WITHOUT_SEPARATORS";
NTANetAnalytics
| where TimeGenerated > ago(6h)
| where SubType == "FlowLog"
| where FlowLogResourceId in~ (RecordedFlowLogId, RecordedFlowLogShortId, RecordedFlowLogQualifiedId)
| where TargetResourceId in~ (TargetVnetId, TargetShortId, TargetQualifiedId)
| where AclGroup in~ (ServerNsgId, RecordedAclGroup)
| where replace_regex(tolower(MacAddress), @"[:-]", "") == tolower(ServerMac)
| where SrcIp == "10.117.0.4" and DestIp == "10.117.0.5"
| where DestPort in (8080, 8081)
| where FlowDirection in~ ("I", "Inbound") and L4Protocol in~ ("T", "TCP")
| where FlowIntervalStartTime < PhaseEnd and FlowIntervalEndTime > PhaseStart
| summarize Allowed = sum(AllowedInFlows), Denied = sum(DeniedInFlows)
    by DestPort, FlowStatus, AclRule
| order by DestPort asc, FlowStatus asc

Replace all placeholders using the private manifest, including the exact VNet/server NSG ARM IDs and server MAC. The live table returned FlowLogResourceId as subscription/resourceGroup/networkWatcher/flowLog, TargetResourceId as subscription/resourceGroup/vnet, and AclGroup as the complete NSG ARM ID. The queries accept those exact observed forms plus the documented full/short alternatives, all derived from recorded IDs. The shared Network Watcher and workload NSG are in different groups. Do not substitute suffix/substring matching or paste real identifiers into public artifacts. The live schema uses case-sensitive MacAddress, direction Inbound, and protocol TCP; the queries also recognize documented I/T aliases. An empty result can reflect a schema-encoding mismatch, not absent traffic. The raw JSON reader separately retains its exact full-ID and NSG ARM-path checks. Inspect first/last flow times alongside interval start/end and processing time; the time a record arrived is not necessarily the time the connection occurred.


Use summed flow counters, not count() of aggregated rows. Inbound filtering avoids counting the same conversation again at the other interface. Inspect the schema returned by the live table before interpreting empty fields. NTANetAnalytics column reference.


Compare the phases


Look for inbound 8081 denies before the change, allowed 8081 flows during exposure, and fresh denies after correction. Confirm the matched rule/ACL against the captured NSG and effective rules. Do not infer a cause from a timeout alone, or treat missing data as proof of denial.


Traffic Analytics reduces many flows into fewer records. The ten-minute setting controls processing frequency, not end-to-end latency; allow bounded ingestion waits of up to 60 minutes, subject to the operating deadline. An aggregate can cover more than one phase. Use raw timestamps for precise attribution. Aggregation guidance.


Portal image — Analytics: capture the actual relevant Traffic Analytics view or KQL result; retain the full measured query results as sanitized structured evidence. New resources can initially appear as Unknown: resource discovery runs on a six-hour cycle, separately from flow processing. Correlate the recorded addresses/IDs rather than altering labels to make the picture look complete. Do not extend the lab's operating deadline while waiting for friendly topology names. A late view remains late evidence, not an assumed pass. Microsoft Traffic Analytics FAQ.


Actual Portal KQL results over 16:15–17:28 UTC: 8080 Allowed 20, 8081 Allowed 8 and 8081 Denied 36. This broad window combines baseline and fault, not phase-exclusive totals.
Actual Portal KQL results over 16:15–17:28 UTC: 8080 Allowed 20, 8081 Allowed 8 and 8081 Denied 36. This broad window combines baseline and fault, not phase-exclusive totals.

Optional dashboard context: a Last 24 Hours Traffic Analytics view shows broader workspace totals, including incidental outbound traffic and blocked public attempts. Those totals are not the controlled client/server phase measurements. A provider-enriched Malicious label is not evidence that this workshop demonstrated a compromise; investigate the underlying flows rather than inferring an incident from that label alone.


8. Restore the intended access boundary


PowerShell / Azure CLI


pwsh -File .\Restore-FlowLogs.ps1 -StatePath $state
pwsh -File .\Restore-FlowLogs.ps1 -StatePath $state -Execute
pwsh -File .\Invoke-FlowLogsTraffic.ps1 -Phase Recovery -StatePath $state -Execute
pwsh -File .\Test-FlowLogs.ps1 -Phase Recovery -StatePath $state -Execute
pwsh -File .\Restore-FlowLogs.ps1 -StatePath $state -Execute

Restore the captured 8080-only destination setting using Azure CLI while preserving unrelated settings. Verify new 8081 connections fail, 8080 succeeds, both local listeners remain healthy, and the original hash is unchanged. The second recovery must detect the already-restored state and make no update.


NSGs are stateful. An existing connection may continue after a rule changes. This is why recovery uses new connections and new phase windows rather than reusing a keep-alive session. NSG connection behavior.


Collect the corresponding raw and analytics evidence before stopping logging. A successful CLI update without those observed results is not a completed recovery gate.


Portal image — Recovery: capture the restored 8080-only rule. Pair it with actual new-connection results, unchanged hashes and the repeated-recovery no-op in sanitized JSON/text.


Actual Portal recovered NSG grid: the captured TCP 8080-only rule is restored. A separate genuine recovery capture, not a reused baseline image.
Actual Portal recovered NSG grid: the captured TCP 8080-only rule is restored. A separate genuine recovery capture, not a reused baseline image.

9. Handle interruptions and export evidence


PowerShell / Azure CLI


pwsh -File .\Reconcile-FlowLogs.ps1 -StatePath $state
pwsh -File .\Test-FlowLogsSafety.ps1
pwsh -File .\Export-FlowLogsEvidence.ps1 -StatePath $state -Execute

For actual bounded CLI-result checkpoints, use a command such as:


PowerShell / Azure CLI


pwsh -File .\Show-FlowLogsCheckpoint.ps1 -StatePath $state -Checkpoint Traffic -Phase Fault -Execute

Checkpoint values are Configuration, Traffic, RawRecords, Analytics, and Cleanup; phases are Baseline, Fault, and Recovery. The runner executes actual CLI operations and presents selected JSON/text. Traffic checkpoints generate fresh bounded requests; they are not a replay of saved output. Retain sanitized structured results, pair them with real Portal views and the reviewed Portal-capture manifest, and do not convert the text into a simulated terminal screenshot.


After a timeout or interrupted operation, inspect its exact recorded target and result before retrying. Never delete a lock file while another process is using the manifest. Offline guard simulations are not Azure tests; report the two classes separately.


Retain private raw evidence. Publish only selected sanitized CLI-result data and cropped/redacted Portal captures that preserve the actual result and its meaning. Remove operator/user paths, tenant/subscription IDs, resource identifiers, addresses not needed for teaching, tokens, key material and private state. Generated illustrations may explain architecture but never stand in for Portal evidence or actual CLI execution results.


10. Clean up and verify twice


PowerShell / Azure CLI


pwsh -File .\Remove-FlowLogs.ps1 -StatePath $state
pwsh -File .\Remove-FlowLogs.ps1 -StatePath $state -Execute
pwsh -File .\Remove-FlowLogs.ps1 -StatePath $state -Execute

The cleanup sequence disables Traffic Analytics and logging, deletes the exact recorded flow-log child, reconciles managed dependencies, removes the recorded role assignments and owned resources, and verifies the group is safe to remove. The existing regional Network Watcher is outside the lab group and must survive unchanged.


Flow-log deletion alone does not remove previously stored log data. Deleting this disposable storage account removes that copy; do not do so before evidence collection. The dedicated workspace follows normal soft deletion, not permanent purge; report its retained recovery window separately from absence of active resources. Workspace deletion guidance.


Run cleanup as two distinct completed invocations. Each invocation performs two absence checks covering the recorded child under the shared Watcher, assignments, managed dependencies and lab-group resources. The repeated invocation must make no Azure mutation requests. Unexpected contents or uncertain deletion block release; never force deletion by prefix.


Operator-assisted contingency: recorded managed telemetry remains


Traffic Analytics normally manages its DCR/DCE resources. Normal cleanup waits ten minutes for the recorded pair to disappear after deleting the flow log. If the script stops at that bound, the VM/storage resources can still be active and billable. Keep the private manifest, do not delete its lock file, and wait for the failed cleanup invocation to exit. This is an optional, separately reviewed contingency—not an automatic instruction to force-delete a dependency.


First create a private, read-only inventory:


PowerShell / Azure CLI


$review = Join-Path (Split-Path $state -Parent) 'managed-orphan-review-01.json'
pwsh -File .\Review-FlowLogsManagedOrphans.ps1 -StatePath $state
pwsh -File .\Review-FlowLogsManagedOrphans.ps1 -StatePath $state `
    -IncludePrivateInventory -OutputPath $review -Execute

Use a new artifact filename; the reviewer will not overwrite a file. The output stays beside the ACL-protected manifest and contains real resource IDs and properties: never upload it or put it in a screenshot/download. It binds the exact run, context, pending cleanup and state hash. Its checks require the recorded flow log to be absent; the exact DCR/DCE pair and creation times to match; their workspace/endpoint relationships to match; no associations, other DCR consumers, same-workspace flow logs or Private Link references; and the shared Network Watcher to remain unchanged. The reference inventory includes enabled subscriptions visible in the current tenant. A failed/inaccessible inventory is a blocker, not permission to skip a check.


Independently inspect the private JSON and the actual resource relationships. Only after that review, edit its ManualReview object: set Approved to true, identify the real Reviewer, set ReviewedUtc to the actual UTC review time, and list only the two exact recorded IDs in ApprovedExactIds. Do not copy an approval from another run or automate these approval fields. The inventory expires after ten minutes; changed state or resource properties require a new inventory and review.


The guarded companion is explicit:


PowerShell / Azure CLI


pwsh -File .\Remove-FlowLogsManagedOrphans.ps1 -StatePath $state
pwsh -File .\Remove-FlowLogsManagedOrphans.ps1 -StatePath $state `
    -ReviewedOrphanInventory $review -Execute

It takes the exclusive manifest lock, rechecks all references before each operation, journals the request, deletes only the exact recorded DCR then DCE using supported API version 2024-03-11, and polls their absence. It never cascade-deletes associations, adopts an object by name, deletes the shared Watcher, or sets the final cleanup gates. If interrupted, obtain a fresh inventory/review and rerun this companion; an already absent exact object is verified rather than deleted again. A reappeared or changed object blocks execution.


Only after both absences are verified, resume the ordinary lifecycle:


PowerShell / Azure CLI


pwsh -File .\Reconcile-FlowLogs.ps1 -StatePath $state -Execute
pwsh -File .\Remove-FlowLogs.ps1 -StatePath $state -Execute
pwsh -File .\Remove-FlowLogs.ps1 -StatePath $state -Execute

The parent cleanup remains pending until ordinary reconciliation; the completed orphan substeps are retained in the private audit. The subsequent cleanup invocations must still prove resource/assignment absence, shared-Watcher preservation, and a zero-mutation repeat. The offline Test-FlowLogsManagedOrphans.ps1 fixtures test these guards, not Azure deletion. Microsoft documents the supported DCR DELETE and DCE DELETE operations; the VNet flow-log management guidance explains that Traffic Analytics normally creates and manages these dependencies.


Portal image — Cleanup: show the actual absence of the lab resources/flow-log child using the appropriate filtered list or deleted-resource view. Keep both completed invocation records, their exact absence checks, and shared-Watcher preservation results in the sanitized CLI evidence.


Actual Portal filtered resource list after cleanup: zero results. The private filter is redacted. Exact-ID CLI checks independently prove all recorded resources and assignments absent while preserving the shared Network Watcher.
Actual Portal filtered resource list after cleanup: zero results. The private filter is redacted. Exact-ID CLI checks independently prove all recorded resources and assignments absent while preserving the shared Network Watcher.

Completion criteria


The actual validation report must show passing local checks; observed baseline/exposure/recovery results; matched raw and analytics records; unchanged service content; no-op repeated recovery; copyable CLI code with actual sanitized results; six genuine Portal images; two distinct completed cleanup invocations with passing absence checks and a mutation-free repeat; and verified package/download hashes. A timeout is recorded as incomplete, not rewritten as success.


Additional Portal context


These supplementary genuine views provide configuration, broad dashboard or retained-workspace context. They do not replace the six required checkpoints or the exact controlled-flow and cleanup measurements. Dashboard workspace totals are broader than the exercise phases; provider enrichment alone does not establish compromise. A recycle-bin row is not proof that every lab resource has been removed.


Actual Azure Portal ConnectedDevices checkpoint, with private identifiers redacted
Actual Azure Portal connected-devices grid: two network interfaces on the workload subnet, with synthetic private addresses 10.117.0.4 and 10.117.0.5. Unique interface names are redacted; this is configuration context, not traffic proof.

Actual Azure Portal TrafficAnalyticsDashboard checkpoint, with private identifiers redacted
Actual VNet Traffic Analytics dashboard for Last 24 Hours. Workspace-wide totals include incidental outbound and blocked public traffic, not just the controlled client/server phases. The provider’s Malicious label is enrichment, not a demonstrated compromise. Exact phase queries and raw records supply controlled attribution.

Actual Azure Portal WorkspaceSoftDelete checkpoint, with private identifiers redacted
Actual Log Analytics Recycle bin: the workshop row in West Europe shows Deleted date Oct 15, 2026. The Portal explains the 14-day recovery window but provides no exact expiry. A retained workspace tombstone is separate from full cleanup confirmation.

Measured CLI and analytics results


The following compact JSON is selected from the actual sanitized evidence export. Traffic results use the latest passing batch; analytics values sum the latest nonempty recorded inbound rows. Aggregates may overlap phase boundaries, so exact raw timestamps supply fine phase correlation. Tuple events, aggregated flows and HTTP attempts are not interchangeable counts.


Baseline — measured results


{
  "phase": "Baseline",
  "trafficWindowUtc": [
    "2026-10-15T17:07:25.740Z",
    "2026-10-15T17:07:41.778Z"
  ],
  "freshConnections": true,
  "contentSha256": "67714115d1e3130b6aec4cba616c1d12362491b981c387570175490c19a1d9bf",
  "portResults": {
    "8080": {
      "outcomes": {
        "Success": 4
      },
      "httpStatusCounts": {
        "200": 4
      }
    },
    "8081": {
      "outcomes": {
        "Timeout": 4
      },
      "httpStatusCounts": {}
    }
  },
  "analyticsObservedUtc": "2026-10-15T17:09:07.2377440+00:00",
  "inboundFlowTotals": [
    {
      "destinationPort": 8080,
      "allowedFlows": 8,
      "deniedFlows": 0,
      "rules": [
        "allowlabapp"
      ]
    },
    {
      "destinationPort": 8081,
      "allowedFlows": 0,
      "deniedFlows": 24,
      "rules": [
        "denyotherinbound"
      ]
    }
  ]
}

Fault — measured results


{
  "phase": "Fault",
  "trafficWindowUtc": [
    "2026-10-15T17:24:39.715Z",
    "2026-10-15T17:24:43.763Z"
  ],
  "freshConnections": true,
  "contentSha256": "67714115d1e3130b6aec4cba616c1d12362491b981c387570175490c19a1d9bf",
  "portResults": {
    "8080": {
      "outcomes": {
        "Success": 4
      },
      "httpStatusCounts": {
        "200": 4
      }
    },
    "8081": {
      "outcomes": {
        "Success": 4
      },
      "httpStatusCounts": {
        "200": 4
      }
    }
  },
  "analyticsObservedUtc": "2026-10-15T17:25:45.1717503+00:00",
  "inboundFlowTotals": [
    {
      "destinationPort": 8080,
      "allowedFlows": 8,
      "deniedFlows": 0,
      "rules": [
        "allowlabapp"
      ]
    },
    {
      "destinationPort": 8081,
      "allowedFlows": 8,
      "deniedFlows": 0,
      "rules": [
        "allowlabapp"
      ]
    }
  ]
}

Recovery — measured results


{
  "phase": "Recovery",
  "trafficWindowUtc": [
    "2026-10-15T17:34:05.848Z",
    "2026-10-15T17:34:21.886Z"
  ],
  "freshConnections": true,
  "contentSha256": "67714115d1e3130b6aec4cba616c1d12362491b981c387570175490c19a1d9bf",
  "portResults": {
    "8080": {
      "outcomes": {
        "Success": 4
      },
      "httpStatusCounts": {
        "200": 4
      }
    },
    "8081": {
      "outcomes": {
        "Timeout": 4
      },
      "httpStatusCounts": {}
    }
  },
  "analyticsObservedUtc": "2026-10-15T17:35:14.3348641+00:00",
  "inboundFlowTotals": [
    {
      "destinationPort": 8080,
      "allowedFlows": 8,
      "deniedFlows": 0,
      "rules": [
        "allowlabapp"
      ]
    },
    {
      "destinationPort": 8081,
      "allowedFlows": 0,
      "deniedFlows": 12,
      "rules": [
        "denyotherinbound"
      ]
    }
  ]
}

Repeated recovery — measured no-op


{
  "check": "RecoveryNoOp",
  "observedUtc": "2026-10-15T17:36:39.2207054+00:00",
  "passed": true,
  "mutation": false
}

Repeated cleanup — measured separate invocation


{
  "check": "CleanupRepeat",
  "observedUtc": "2026-10-15T18:07:38.2381490+00:00",
  "invocation": "cleanup-2",
  "priorInvocation": "cleanup-1",
  "passed": true,
  "mutation": false,
  "azureMutationRequests": 0,
  "absenceChecks": [
    {
      "check": 1,
      "utc": "2026-10-15T18:07:20.1360044+00:00",
      "absent": true,
      "sharedWatcherPreserved": true
    },
    {
      "check": 2,
      "utc": "2026-10-15T18:07:38.2357031+00:00",
      "absent": true,
      "sharedWatcherPreserved": true
    }
  ]
}

Challenge — investigate before opening the solution


Symptoms


The internal client can read the approved application endpoint. It can now also read a maintenance endpoint that should not be available to it. Both server listeners are healthy, and the application content is unchanged.


Your task is to explain the unexpected connection using network evidence, correct its cause, and demonstrate that approved access still works.


Available evidence


  • Recorded client/server identities and private addresses.

  • Actual connection results and their UTC observation windows.

  • Current and captured network configuration.

  • VNet flow-log JSON and the dedicated Traffic Analytics workspace.

  • CLI and Portal views of the lab's owned resources.


Constraints


  • Do not stop or replace either VM or application service.

  • Do not delete an NSG, detach it, open public inbound access, or change addresses.

  • Do not change payload files, logging configuration, or identity permissions to hide the symptom.

  • Use fresh, bounded connections. Preserve evidence before cleanup.

  • Operate only on exact recorded lab objects through the guarded scripts.


Deliverables


  1. A short diagnosis supported by the actual evaluated control and connection records.

  2. Evidence that the maintenance endpoint is blocked again for new connections.

  3. Evidence that the approved endpoint and original content remain healthy.

  4. A safe repeated recovery and verified cleanup.


Use HINTS.md only when needed. The full diagnosis is in the separate SOLUTION.md.


Progressive hints


Open one hint at a time, then return to the investigation.


Compare a local server request with a new client-to-server request. A healthy listener does not mean that every client should be permitted to reach it.


Use the recorded source, destination, TCP port and UTC phase windows. A workspace-wide traffic chart can include unrelated outbound traffic and both directions of a conversation.


Find the matching raw tuple and its ACL/rule parent. Compare it with the server NIC's effective security rules and the captured baseline. A configured NSG rule and its effective application are distinct checks.


Compare the application Allow rule's destination-port setting. Its source, destination, priority, direction and action may all look correct while a broadened port range admits an additional listener.


Restore the captured destination-port value through Restore-FlowLogs.ps1, then generate a new Recovery batch. Check both endpoints and the original content hash. Run recovery again to establish that it makes no further change.


NTANetAnalytics aggregates connections; sum the direction-specific allowed/denied counters. If an interval straddles two phases, use raw tuple timestamps. A delayed or absent analytics record is not proof of a denied connection.



Separate solution


SOLUTION.md is supplied separately in the download. Reveal the solution here only after attempting the challenge.


Diagnosis


The controlled fault broadens only the recorded server application Allow rule's destination setting from TCP 8080 to 8080-8081. Both listeners were already running. The maintenance endpoint becomes reachable because the client-to-server Allow rule now includes it, not because its code changed or the VM became unhealthy.


Prove this with the actual run's evidence: a successful new 8081 connection during the fault window, the widened rule, its effective association, and matching allowed flow records under the responsible ACL/rule. A timeout or a broad traffic chart alone would not establish the cause.


Evidence map


  • Phase: Baseline

    Approved 8080: Succeeds; original hash

    Maintenance 8081: Fresh connection fails

    Required supporting evidence: Captured 8080-only rule; inbound 8081 deny records

  • Phase: Fault

    Approved 8080: Still succeeds; original hash

    Maintenance 8081: Fresh connection succeeds

    Required supporting evidence: Only destination ports widened; allowed 8081 records

  • Phase: Recovery

    Approved 8080: Still succeeds; original hash

    Maintenance 8081: New connection fails again

    Required supporting evidence: Captured rule restored; fresh 8081 deny records


The table defines expected acceptance results, not a statement that a particular run passed. Use the validation report for measured status.


Recovery


Run from the extracted package in PowerShell 7, using the same private manifest as setup:


PowerShell / Azure CLI


$state = Join-Path $env:LOCALAPPDATA 'AzureWorkshopz-FlowLogs/run.json'
pwsh -File .\Restore-FlowLogs.ps1 -StatePath $state
pwsh -File .\Restore-FlowLogs.ps1 -StatePath $state -Execute
pwsh -File .\Invoke-FlowLogsTraffic.ps1 -Phase Recovery -StatePath $state -Execute
pwsh -File .\Test-FlowLogs.ps1 -Phase Recovery -StatePath $state -Execute
pwsh -File .\Restore-FlowLogs.ps1 -StatePath $state -Execute

The guarded recovery restores the recorded 8080-only setting via Azure CLI and preserves unrelated rule fields. It verifies context, exact objects, ownership, snapshot and pending operation first. Do not rewrite the whole NSG or remove its explicit deny.


The equivalent core operation is az network nsg rule update ... --destination-port-ranges 8080, but using it directly during the scripted run would bypass the lock and recovery record. Inspect the script and actual CLI output to learn the command; use the wrapper to execute it safely.


Why fresh connections matter


An NSG evaluates connection state; modifying its rules does not guarantee that an already-established session immediately stops. Close previous sessions and start new bounded requests for each phase. Preserve phase UTC windows to correlate the resulting records. Microsoft NSG behavior.


Raw records versus analytics


Raw flow tuples provide precise connection timing with their ACL and rule context. Traffic Analytics gives a reduced, enriched view; a single row can represent many flows. Filter to the lab's source/destination, inbound direction, relevant ports, recorded flow-log identity and UTC interval, then sum AllowedInFlows and DeniedInFlows. Do not equate an aggregate row count with connection count. Traffic Analytics schema.


Wait only within the documented bounds. If analytics never arrives, preserve the raw evidence and mark that gate incomplete. Do not turn off logging, change firewall exceptions, or relabel synthetic output as evidence to force a passing report.


Finish


Verify unchanged service content and associations, retain actual recovery CLI JSON/text alongside copyable commands, capture the genuine Portal recovery view, export sanitized evidence, and run guarded cleanup twice. Code examples describe commands; measured results provide execution evidence. Preserve the existing shared Network Watcher and unrelated resources. The second cleanup must safely confirm absence rather than attempt adoption of similarly named resources.



Knowledge check — eight questions


Try each question before expanding its answer.


It proves the application listener can serve that local request. It does not establish that the internal client is allowed through the NIC's effective network controls.

VNet flow logs monitor supported traffic at the VNet scope and supply the modern NTANetAnalytics experience. This is a new implementation, not an exercise maintaining a legacy NSG logging deployment. VNet flow-log overview.

NSGs are stateful; existing established flows can survive a rule change. A new connection tests the new policy evaluation instead of an old session's state. NSG behavior.

Analytics aggregates multiple flows. Sum the relevant allowed/denied counters, filter the direction, and inspect raw records when precise timing is needed. Aggregation guidance.

Combine a healthy local listener, the actual client result, effective NSG/rule information, and a matching denied raw flow/analytics record. A generic timeout alone cannot identify the cause.

The logging service needs write access to its destination account; the investigation client needs only read access to the exact service-created container. Separate identities prevent unnecessarily giving the reader account-wide write access. Managed-identity guidance.

No. Processing frequency is not end-to-end ingestion latency. Use bounded waits, recorded flow intervals and actual arrival results; a timeout remains incomplete.

The flow-log child sits below the shared regional Network Watcher outside the lab group. Exact assignments and service-managed dependencies also need reconciliation. Preserve the shared Watcher, delete only the owned child/dependencies, then verify absence twice. Previously stored raw logs also outlive deletion of their flow-log configuration unless their storage is removed. Management guidance.


Core validation report — pre-package freeze


This measured core report is the immutable pre-package snapshot included in the download. Later package verification is recorded separately below.


Workshop: Azure VNet Flow Logs — Find Unexpected Traffic with Traffic Analytics


Release status: READY FOR PUBLICATION


The core live, cleanup, privacy and evidence gates passed on 2026-10-15. This report separates those measured results from offline simulations and the external package/publication receipts that follow archive creation. It does not claim that a public download or website layout has already been verified.


Core acceptance gates


  • Gate: PowerShell parsing and CLI-only AST checks

    Status: Passed locally

    Actual evidence: static-results.json, 18:11:44 UTC; 21 PowerShell source files; PowerShell 7.6.6

  • Gate: Guest application and reader tests

    Status: Passed locally

    Actual evidence: 16 offline fixture/loopback tests, none skipped; Python 3.14.5; no Azure exercise claim

  • Gate: Analytics schema and phase-window fixtures

    Status: Passed locally

    Actual evidence: 24 offline cases covering observed encodings, exact identity isolation and single-/multiple-batch UTC windows

  • Gate: Managed-orphan contingency fixtures

    Status: Passed locally

    Actual evidence: 10 offline cases for the separately reviewed exact-target fallback; distinct from normal service-cleanup grace

  • Gate: Guard and preview simulations

    Status: Passed locally

    Actual evidence: safety-results.json, 18:11:44 UTC; 191 cases, zero failed; wrong-context/ID, concurrency, interruption and narrow workspace-absence cases; no Azure calls

  • Gate: Package/report rejection fixtures

    Status: Passed locally

    Actual evidence: 41 offline package guard checks; path/secret/hash/report rejection is not release-archive verification

  • Gate: Azure preflight and operational allowance

    Status: Passed

    Actual evidence: 15:51:39 UTC; matching context and reviewed governance/capability inventory; conservative active estimate EUR7.40 plus EUR5 cleanup reserve

  • Gate: Foundation and network controls

    Status: Passed

    Actual evidence: Foundation 16:06:14 UTC; client network checks 16:20:55 UTC and server 16:21:37 UTC; explicit outbound addresses verified privately, public SSH blocked and effective NIC NSGs retained

  • Gate: Flow-log configuration and actual ingestion

    Status: Passed

    Actual evidence: Configuration 16:11:30 UTC; exact-filtered raw writes and matching NTANetAnalytics records subsequently observed; configuration alone was not used as ingestion proof

  • Gate: Unprivileged application and local listeners

    Status: Passed

    Actual evidence: 16:13:45 UTC; both local listeners and original service/content hashes recorded

  • Gate: Complete Baseline

    Status: Passed

    Actual evidence: 17:09:13.5368919 UTC; fresh 8080 HTTP 200, fresh 8081 timeouts, matching raw/analytics evidence and unchanged independent controls

  • Gate: Controlled NSG exposure

    Status: Passed

    Actual evidence: 17:25:50.2326080 UTC; only recorded destination ports widened to 8080–8081; fresh 8081 HTTP 200 with original content hash; unrelated controls unchanged

  • Gate: Recovery

    Status: Passed

    Actual evidence: 17:35:19.8385506 UTC; captured 8080-only rule restored, fresh 8081 timeouts, 8080 HTTP 200, matching raw/analytics evidence and unchanged content

  • Gate: Repeated recovery

    Status: Passed

    Actual evidence: 17:36:39.2207054 UTC; Mutation=false; no additional configuration update

  • Gate: First completed cleanup

    Status: Passed

    Actual evidence: 18:07:03.1224974 UTC; absence checks at 18:06:46.3282357 and 18:07:03.1091419; exact recorded active resources/assignments absent and shared Watcher preserved

  • Gate: Distinct repeated cleanup

    Status: Passed

    Actual evidence: 18:07:38.2381490 UTC; absence checks at 18:07:20.1360044 and 18:07:38.2357031; zero Azure mutation requests and shared Watcher preserved

  • Gate: Retained workspace tombstone

    Status: Passed

    Actual evidence: Fresh exact deleted-workspace/customer-identity reconciliation at 18:10:22.5454149 UTC; normal 14-day recovery semantics, no permanent purge; no service expiry returned

  • Gate: Genuine Portal evidence

    Status: Passed

    Actual evidence: Six mandatory images independently reviewed and registered at 18:11:36 UTC in evidence/genuine-captures.json; three supplementary genuine views reviewed separately; private originals and deterministic redaction provenance preserved

  • Gate: Public source/evidence privacy

    Status: Passed

    Actual evidence: Actual post-cleanup evidence/sanitized-evidence.json, exported 18:11:36 UTC; final known-private-value scan returned zero matches; credentials, signed URLs, raw resource identifiers, public addresses and private state excluded


All times below are UTC on 2026-10-15. Local checks are simulations or static execution, not live Azure tests. Source changes require fresh local checks before rebuilding.


Measured traffic and correlation


Original synthetic content SHA-256, unchanged across the accepted phases:


text


67714115d1e3130b6aec4cba616c1d12362491b981c387570175490c19a1d9bf

Each bounded batch used four fresh attempts per port with proxy use disabled. Client attempts, raw tuple-state events and aggregated flow counters measure different things; their counts are not interchangeable.


  • Phase: Baseline

    Recorded client batches: 16:17:59.404–16:18:15.446; 16:21:52.354–16:22:08.397; 17:07:25.740–17:07:41.778

    Actual per-batch outcomes: Each: four 8080 HTTP 200 responses and four 8081 timeouts; original hash

  • Phase: Fault

    Recorded client batches: 17:14:01.235–17:14:05.281; 17:16:39.887–17:16:43.933; 17:24:39.715–17:24:43.763

    Actual per-batch outcomes: Each: four 8080 and four 8081 HTTP 200 responses; original hash

  • Phase: Recovery

    Recorded client batches: 17:29:14.825–17:29:30.863; 17:34:05.848–17:34:21.886

    Actual per-batch outcomes: Each: four 8080 HTTP 200 responses and four 8081 timeouts; original hash


The latest accepted raw reads retained the exact recorded flow-log, server MAC and NSG ACL relationships and server-inbound direction:


  • Phase: Baseline

    Raw selection window, including two-second end margin: 16:17:59.404–17:07:43.778

    Observed tuple-state events: 60 events: 8080 Begin 12 / End 12; 8081 Deny 36

  • Phase: Fault

    Raw selection window, including two-second end margin: 17:14:01.235–17:24:45.763

    Observed tuple-state events: 32 events: each port Begin 8 / End 8; no matched deny events

  • Phase: Recovery

    Raw selection window, including two-second end margin: 17:29:14.825–17:34:23.886

    Observed tuple-state events: 20 events: 8080 Begin 4 / End 4; 8081 Deny 12


These are available observed events, not unique connection totals or a claim that every generated attempt had been published by the read. Denied TCP retries can create multiple deny entries; an allowed connection can have both Begin and End records. The matching rules were the application Allow rule and explicit other-inbound Deny rule, not an inferred timeout cause.


Latest nonempty exact-identity analytics observations:


  • Selected phase: Baseline

    Query observation time: 17:09:07.2377440

    Actual inbound counters and interval context: 8080 Allowed 8; 8081 Denied 24. Events correspond to the earlier two baseline batches; returned processing intervals span 16:00–16:50. Not totals for all three batches

  • Selected phase: Fault

    Query observation time: 17:25:45.1717503

    Actual inbound counters and interval context: 8080 Allowed 8 and 8081 Allowed 8 under the application rule; processing interval 17:10–17:20; event times 17:14–17:16

  • Selected phase: Recovery

    Query observation time: 17:35:14.3348641

    Actual inbound counters and interval context: 8080 Allowed 8 and 8081 Denied 12; processing interval 17:20–17:30. The 8080 aggregate spans fault and recovery; denied 8081 events begin at 17:29:16.849


The recovery-selected 8080 aggregate starts at 17:24:39.719 and ends at 17:29:22.123, crossing the correction boundary. It is not eight recovery-only successful flows. Fresh recovery client responses and raw timestamps establish post-restoration 8080 success. The denied 8081 events run from 17:29:16.849 through 17:29:29.413, within the first recovery batch, and map to the recorded deny rule.


An actual analytics record's ingestion field was observed at approximately 16:51:52 UTC, about 34 minutes after the first baseline traffic. The later first complete exact-filtered Baseline query observation at 17:09 is not itself an ingestion-latency measurement. Baseline acceptance remained within the original 60-minute allowance. Ten-minute processing did not mean ten-minute end-to-end availability.


Genuine visual evidence and its scope


CLI commands are copyable blocks paired with actual sanitized JSON/text. The six required Portal captures cover Logging, Baseline NSG, Fault NSG, Recovery NSG, Analytics and Cleanup. Generated cover artwork is conceptual and is not execution evidence.


  • NSG images show the configured grid. Independent effective NIC NSG readbacks and real client traffic establish evaluation. Baseline and restored crops are pixel-identical because the configuration was restored; separate genuine originals and provenance were retained.

  • The Analytics KQL image covers 16:15–17:28 UTC, combining baseline and exposure. Its three rows are broad-window counters, not phase-exclusive results.

  • The optional Last 24 Hours dashboard includes incidental outbound and blocked public traffic. Its provider-enriched Malicious label does not demonstrate compromise.

  • The optional connected-devices image shows real interface/private-address relationships. The optional workspace Recycle bin image shows retained soft-delete metadata.

  • The Cleanup image shows an actual filtered zero-result Portal list. The run filter is privacy-redacted; exact-ID CLI checks independently establish the deletion scope and shared-Watcher preservation.


Private originals remain unchanged. Public copies use deterministic cropping and solid-black privacy rectangles only; no results, labels, counts or missing text were invented, reconstructed or AI-edited.


Implementation findings — not extra infrastructure incidents


  1. Raw ACL representation. The first raw-reader assumption equated aclID with the NSG resource GUID. Actual records used the exact NSG ARM path. After verifying the relationship, the reader required that exact path rather than using a suffix or arbitrary match. The correction was recorded at 16:32:15 UTC; an empty initial selection was not treated as no traffic.

  2. Live analytics encodings. The table used case-sensitive MacAddress, direction Inbound, protocol TCP, subscription-qualified flow-log/target paths and a full NSG ARM ACL path. Exact recorded alternatives support documented short IDs and I/T aliases without broadening identity scope. Schema mismatch and ingestion delay were investigated separately from the controlled NSG fault.

  3. CLI query capability. The installed optional Log Analytics extension failed with ModuleNotFoundError: pkg_resources. No upgrade, reinstall or automatic extension installation was attempted. Core az rest supplied the documented workspace query using CLI-managed authorization and strict structured-response validation; the wrapper did not print or persist an operator token.

  4. Single-batch UTC indexing. A one-result PowerShell pipeline can become a scalar string, so [0] can select a character. The window helper now forces arrays before selecting complete timestamp strings. Single- and multiple-batch regression fixtures passed; this was an orchestration correction, not an Azure outage.

  5. Workspace deletion response. An active-workspace read returned only ERROR: Not Found, without the exact target ID. Supported deleted-workspace inventory verified the recorded workspace/customer identity, type and region. The narrow reconciliation path does not make generic NotFound sufficient for arbitrary resources. Its modifiedDate is not a deletion or expiry field.


Completed cleanup ledger


Two distinct completed invocations verified no active recorded lab resources or assignments remained, including the owned flow-log child and managed dependencies. The shared West Europe Network Watcher was preserved. The final manifest records Cleaned=true and no pending operation.


The first completed resumed invocation finished at 18:07:03.1224974 UTC. Its recorded one Azure mutation request was the final empty-resource-group deletion, not the entire cleanup workflow's mutation count. Earlier deletions and reconciliations occurred in paused attempts. The separate repeat finished at 18:07:38.2381490 UTC with zero Azure mutation requests. Each completed invocation performed two absence checks, giving four distinct checks in total.


Cleanup paused safely on three conditions rather than treating uncertain state as success:


  1. Managed Traffic Analytics DCR/DCE objects remained after the normal ten-minute service-cleanup grace. A separate read-only review checked exact ownership relationships, other consumers and configuration. The reviewed orphan contingency then removed only those verified objects; its audit spans 17:52:13.9758643–17:53:37.5314052 UTC. No prefix-based adoption or deletion was used.

  2. A generic compute post-delete response lacked exact target identity. Independent inventory found no owned VMs/disks, and the exact server-VM read subsequently returned ResourceNotFound. Normal reconciliation resumed without weakening the generic absence guard.

  3. A plain workspace NotFound response required supported tombstone/customer-identity reconciliation, not a generic absence bypass.


The genuine Recycle bin row gives Deleted date Oct 15, 2026 and location West Europe. Fresh exact tombstone reconciliation was recorded at 18:10:22.5454149 UTC. Normal workspace soft deletion retains its documented 14-day recovery window; no permanent purge was requested. The returned 17:59:55.9553176 UTC value is modifiedDate, not a server-confirmed deletion timestamp. No service expiry was returned, so no exact expiry is invented. Retained data/configuration is reported separately from absence of active resources. Workspace deletion guidance.


The run completed cleanup well before the five-hour exercise-stop and six-hour cleanup targets. The refreshed estimate is not an instantaneous final invoice; no final-cost claim is inferred from delayed billing.


Packaging and publication verification


The core gates above permit guarded archive creation. The following external release-verification receipts follow archive creation and must be completed before final delivery. A passing local package guard fixture is not a receipt for a release ZIP or public download.


  • Check: Release ZIP extraction and per-file hashes

    Status at report freeze: Pending archive creation

    Required receipt: Fresh safe extraction, SHA256SUMS.json, verifier output and external whole-archive SHA-256

  • Check: Published download integrity

    Status at report freeze: Pending upload and fresh download

    Required receipt: Unsigned direct public URL and fresh bytes matching the approved archive SHA-256

  • Check: Published blog/card links

    Status at report freeze: Pending publication

    Required receipt: Intended standalone article and Workshopz card resolve without changing series ordering

  • Check: Desktop layout and Wix mobile preview

    Status at report freeze: Pending publication review

    Required receipt: Actual layout/link checks; no actual-phone testing claim


These pending publication receipts do not stand in for, or alter, the completed live Azure and cleanup results. Do not claim archive, public-download or layout verification until the corresponding receipt exists.


External package verification — actual post-build receipt


The core report above was frozen before archive creation and is retained unchanged inside the verified ZIP. The separate actual receipt below records the later fresh public download, safe extraction and per-file hash comparison. This package check did not rerun Azure exercises and does not claim website or mobile-preview verification.


{
  "verifiedUtc": "2026-10-15T18:17:57.8208763+00:00",
  "publicDownloadVerified": true,
  "archiveSha256": "c5e22bf465a28e5cf5fbf6e805317501f1c43c99e8c09050fe73edf3efa56d12",
  "freshExtraction": true,
  "extractedFiles": 51,
  "requiredPortalScreenshots": 6,
  "allPerFileHashesMatched": true,
  "verificationRanAzureExercises": false,
  "directDownloadUrl": "https://136c3e71-702b-495c-b214-d18f0f6b829a.usrfiles.com/archives/32fce1_ee12b6dec4d34328b43befc100fe8e3b.zip"
}



 
 
 

Comments


bottom of page