Zero-Trust Web Platform on Azure — a Bicep lab¶
A hands-on lab that builds a production-shaped Azure platform entirely in Bicep, where the web application has exactly one public entry point and every other component — database, storage, secrets — is unreachable from the internet.
It is not a tutorial that ends with a green checkmark. Every control the architecture claims comes with a command that tries to break it, and the chapter is only finished when the attempt fails for the reason the design says it should.
Read it as a site: ozmvege.github.io/azure-bicep-lab — same content, with search and a chapter sidebar.
graph TB
Internet(["Internet"]) --> AGW["Application Gateway WAF_v2<br/>OWASP CRS 3.2 · the only public IP"]
subgraph spoke["spoke VNet — 10.20.0.0/16"]
AGW -.->|"service endpoint<br/>Microsoft.Web"| APP["App Service · Linux<br/>public access: gateway subnet only<br/>outbound: VNet integration"]
APP --> PEP["private endpoints<br/>10.20.2.0/24"]
PEP --> KV["Key Vault"]
PEP --> ST["Storage · blob"]
PEP --> PG["PostgreSQL Flexible"]
end
subgraph hub["hub VNet — 10.10.0.0/16"]
DNS["4 × privatelink DNS zones"]
BAS["Bastion + jumpbox<br/>optional"]
LAW["Log Analytics"]
end
spoke <-->|"peering"| hub
APP -.->|"managed identity<br/>no secrets"| KV
AGW --> LAW
What you actually learn¶
Azure, by having to make it work rather than by reading about it:
| Hub-and-spoke networking | two VNets, peering in both directions, NSGs written out rule by rule |
| Private Link | six private endpoints, four private DNS zones, and why the zone is the part that matters |
| Application Gateway WAF v2 | OWASP CRS, Detection versus Prevention, exclusions, and reading the firewall log |
| Identity over secrets | managed identity, RBAC data-plane roles, Key Vault references, shared keys switched off |
| Azure Policy | three custom definitions, an initiative, and an assignment that denies a real deployment |
| Deployment stacks | lifecycle management, deny assignments, and a teardown that cannot leave anything behind |
| Log Analytics | resource-specific tables, the KQL that finds a blocked request, two alerts worth having |
Bicep, past the point where most examples stop:
- user-defined types with
@export(), imported across every module —infra/types.bicep - user-defined functions encoding the naming convention —
infra/naming.bicep - subscription-scope orchestration that creates the resource groups it then fills
.bicepparamwithusing none,extendsfor shared values, andaz.getSecret()so a password reaches the deployment without ever touching the repositoryloadJsonContent()for policy rules, because[parameters('effect')]inside a Bicep string does not survive compilation- offline assertions —
bicep testrunning real checks with no subscription attached - a
bicepconfig.jsonwhose linter rules are errors, not suggestions
Prerequisites¶
- An Azure subscription where you are Owner — role assignments and policy definitions are not available to Contributor
- Azure CLI 2.60 or newer, and the Bicep CLI:
az bicep install - PowerShell 7 or bash
- About EUR 1 of Azure spend, and the discipline to run the teardown afterwards
Quickstart¶
The bootstrap prints three values. Paste them into
infra/main.dev.bicepparam, then:
bash equivalents of all four live beside them in scripts/.
The workflows do not deploy anything until you set them up.
DeployandDestroyare manual-trigger only, and CI's what-if job skips itself while the Azure secrets are absent — so a fresh clone is green and costs nothing. Chapter 09 turns them on.
Cost¶
Application Gateway WAF_v2 is the expensive component: roughly USD 0.44 per hour in fixed cost before any traffic, which is around USD 320 a month if it is left running. The lab is designed to be deployed, verified and destroyed in one sitting.
| Component | ~USD/hour | Note |
|---|---|---|
| Application Gateway WAF_v2 | 0.44 + capacity units | teardown is not optional |
| App Service B1 | 0.018 | P1v3 only in the prod profile |
| PostgreSQL Flexible B1ms | 0.017 | burstable, 32 GB |
| Private endpoints × 6 | 0.06 | USD 0.01 each |
| Azure Bastion Basic | 0.19 | off by default |
| Key Vault, Storage, Log Analytics | ~0 | per-use, negligible at lab volume |
A full run: about USD 0.60–1.50. Set a budget alert before you start —
docs/cost-and-cleanup.md shows how, and how to prove nothing
survived the teardown.
The chapters¶
| 00 | Architecture | the threat model, and every design decision with the alternative it beat |
| 01 | Toolchain | CLI versions, providers, the naming convention, the bootstrap vault |
| 02 | Network | hub, spoke, peering, NSGs, private DNS |
| 03 | Platform services | Key Vault, Storage, PostgreSQL — all six private endpoints |
| 04 | Compute | App Service, VNet integration, managed identity, Key Vault references |
| 05 | Edge and WAF | Application Gateway, OWASP rules, Detection → Prevention |
| 06 | Observability | diagnostic settings, the KQL that matters, two alerts |
| 07 | Governance | custom policy, an initiative, and watching Deny actually deny |
| 08 | Deployment stacks | lifecycle, deny assignments, tampering with it from the portal |
| 09 | CI/CD with OIDC | federated credentials, what-if on a pull request, PSRule |
| 10 | Verification | the proof matrix — every claim, its command, its expected output |
| 11 | Testing Bicep | offline assertions, and the bug they caught in this repository |
| 12 | Azure Verified Modules | swap a hand-written module for the Microsoft-maintained one |
| — | Troubleshooting | the errors this build actually produces, and what they mean |
| — | Cost and cleanup | budget alerts, teardown, and proving it worked |
Repository layout¶
infra/
main.bicep subscription scope: three resource groups, then modules
bootstrap.bicep phase zero: the vault holding the database password
types.bicep exported type contracts
naming.bicep exported naming functions
main.shared.bicepparam values dev and prod both use
main.dev.bicepparam cheap profile — what the lab runs
main.prod.bicepparam the same platform without the cost compromises
bicepconfig.json linter rules raised to error
modules/
network/ hub, spoke, peering, NSGs, private DNS, Bastion, jumpbox
platform/ Key Vault, Storage, PostgreSQL, bootstrap vault
app/ App Service plan and site
edge/ WAF policy, Application Gateway
observability/ Log Analytics, alerts
governance/ policy definitions (+ rules as JSON), assignment
shared/ private endpoint, RBAC
tests/ offline assertions
.github/workflows/ ci (build, lint, test, PSRule, what-if) · deploy · destroy
scripts/ bootstrap · deploy · verify · teardown · validate
.ps-rule/ PSRule for Azure configuration
Before you commit anything¶
Compiles every template and parameter file, lints them against the elevated rules, and runs the offline assertions. No subscription, no login, no cost.
License¶
MIT — see LICENSE.