Security-Hardened Storage with Bicep
Write, lint, and harden an Azure Storage Account Bicep template.
Introduction
30 Second Summary
Every misconfigured cloud storage account is a data breach waiting to happen. One wrong setting and your files are exposed to the entire internet.
In this project, you will write an Azure Bicep template for a security-hardened Storage Account. You will start with intentionally insecure code, watch the built-in linter catch each violation, then fix them one by one until the template passes a strict security configuration.
What You'll Build
You will run a single build command and see it pass cleanly, proving that your storage account template enforces encryption, network lockdown, and secure parameter handling before it ever reaches Azure.
By the end of this project, you'll have:
- A Bicep template you can hand to any team that enforces HTTPS-only traffic, TLS 1.2 minimum, no public blob access, and deny-by-default networking on an Azure Storage Account.
- A bicepconfig.json that turns security warnings into build-breaking errors. Insecure code fails to compile instantly.
- A working Azure validation proving your template is accepted without deploying anything or spending a cent.
- Secret Mission: Add an Azure Key Vault and pass secrets securely into a Bicep module using the getSecret function.
Are there any prerequisites?
You need VS Code installed, an Azure account, and the Azure CLI already set up on your machine. No prior Bicep experience is required.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Bicep Environment
For Bicep's security linter to catch insecure code as you type, you need the Bicep VS Code extension installed. Without it, you would only see errors after running a CLI build command.
In this step, you will install the extension, confirm the Bicep CLI is available, and create the project folder where your security-hardened template will live.
In this step, get ready to:
- Install the Bicep VS Code extension for real-time linting.
- Verify the Bicep CLI is available via Azure CLI.
- Create the project folder and confirm the linter catches invalid syntax.
Install the Bicep VS Code extension
The Bicep extension gives VS Code the ability to understand .bicep files. It powers syntax highlighting, autocompletion, and the real-time linting that catches security issues before you ever run a build.
- In VS Code, open the Extensions sidebar by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
- Type Bicep in the search bar.
- Find the extension published by Microsoft (ID: ms-azuretools.vscode-bicep) and click Install.
What does this extension do?
The Bicep extension runs a language server in the background. It validates your code on every keystroke, highlights errors with red squiggles, and provides IntelliSense for Azure resource types and properties.
Now confirm the Bicep CLI is available. The CLI is what compiles your .bicep files into ARM templates and runs the linter from the terminal.
- Open the integrated terminal in VS Code by pressing Ctrl+` (both macOS and Windows).
- Check if the Bicep CLI is installed by running:
az bicep version
✔️ I see a version number
The Bicep CLI is ready. You will use this later to build and validate your template.
ⓧ Command not found or error
The Bicep CLI is not installed yet. Install it by running:
az bicep install
- After it finishes, verify the install by running:
az bicep version
You should now see a version number in the output.
Still seeing an error after running az bicep install?
Make sure your Azure CLI is version 2.20.0 or later. Run az --version to check. If it is older, update Azure CLI first, then retry az bicep install.
Help me fix my Bicep CLI installation
Create the project folder
You need a dedicated folder for your Bicep template and its configuration file. Creating it on your Desktop makes it easy to find.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir secure-storage
cd secure-storage
- Open this folder in VS Code. Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the secure-storage folder, and click Open.
Trust the authors prompt
VS Code may ask if you trust the authors of the files in this folder. Click Yes, I trust the authors to enable full functionality.
- In the VS Code Explorer sidebar, click the New File icon.
- Name the file main.bicep and press Enter.
Check the lower-right corner of VS Code. The language mode should display Bicep. This confirms the extension recognizes your file.
Language mode shows Plain Text instead of Bicep?
Click the language mode label in the lower-right corner, type Bicep in the dropdown, and select it. If Bicep is not listed, the extension may not have installed correctly. Return to the Extensions sidebar and confirm the Bicep extension shows as enabled.
Help me fix my language mode
Verify the linter is working
Before you write your real template, confirm that the linter catches invalid syntax in real time. This proves the extension is active and will flag security issues as you code.
- Click inside main.bicep in the editor.
- Type the following valid Bicep syntax and save the file:
param test string
- Open the Problems panel by pressing Cmd+Shift+M (macOS) or Ctrl+Shift+M (Windows).
The Problems panel should be empty. This is valid Bicep, so no errors or warnings appear.
- Now replace the content of main.bicep with this intentionally invalid syntax and save:
param 123invalid
Before you look at the Problems panel, what do you think will happen? Will the linter accept this or reject it?
You should see a red squiggle under 123invalid and an error in the Problems panel. The linter rejects it because parameter names cannot start with a number.
- Select all text in main.bicep and delete it.
- Save the now-empty file.
The Problems panel should be clear again. Your linter is working and your file is ready for real code.
No red squiggle appeared?
Make sure you saved the file after typing the invalid syntax. The linter only runs on saved content. Also confirm the language mode in the lower-right shows Bicep, not Plain Text.
Help me get the Bicep linter working
Your Bicep environment is ready. Next up, you will write an intentionally insecure storage account template and watch the linter react.
Write an Insecure Storage Account Template
Your Bicep environment is ready and the linter is active. Now you need something for it to lint.
In this step, you will deliberately write an insecure Storage Account template. The goal is to see what the default linter catches, and more importantly, what it lets through. Spoiler: the build succeeds anyway.
In this step, get ready to:
- Write a Bicep template with hardcoded secrets, public blob access, and weak TLS.
- Observe that the linter warns but does not block the build.
Write the insecure template
You are about to write code that violates multiple security best practices on purpose. This creates the baseline you will fix in later steps.
- In VS Code, open main.bicep (it should be empty from Step 1).
- Paste the following insecure template into the file:
// Hardcoded password parameter without @secure() decorator
param storageAccountPassword string = 'supersecret123'
// Storage account with insecure settings
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: 'mystorageaccount'
location: resourceGroup().location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
allowBlobPublicAccess: true // Allows anonymous public access
minimumTlsVersion: 'TLS1_0' // Allows outdated, insecure TLS
networkAcls: {
defaultAction: 'Allow' // Permits all network traffic
}
}
}
// Leaks the storage key in deployment output
output storageKey string = storageAccount.listKeys().keys[0].value
What does this code do wrong?
- storageAccountPassword has a hardcoded default value ('supersecret123') and no @secure() decorator. Anyone reading the deployment history can see it.
- allowBlobPublicAccess: true means anonymous users on the internet can read your blobs.
- minimumTlsVersion: 'TLS1_0' accepts connections using TLS 1.0, which has known vulnerabilities.
- networkAcls: { defaultAction: 'Allow' } lets any IP address connect to the storage account.
- The storageKey output exposes the full account key in the deployment output, which is logged and visible to anyone with Reader access.
- Save main.bicep.
- Look at your editor. You should see yellow squiggly underlines on the param line and the output line.
No yellow squiggles?
- Confirm the Bicep extension is installed and enabled. Check the language mode in the bottom-right corner of VS Code. It should say Bicep.
- Make sure you saved the file with the .bicep extension. Files without the correct extension do not activate the linter.
- help me debug why I don't see linting warnings in my Bicep file
Observe the warnings
The linter flagged the insecure code. The question is whether those flags actually stop you from building and deploying.
- Open the Problems panel by pressing Cmd+Shift+M (macOS) or Ctrl+Shift+M (Windows).
- Note the yellow warnings listed there. You should see entries referencing secure-secrets-in-params, outputs-should-not-contain-secrets, and secure-parameter-default.
What are these linter rules?
- secure-secrets-in-params fires because the parameter name contains "password" but lacks a @secure() decorator.
- outputs-should-not-contain-secrets fires because the output uses listKeys(), which exposes a secret value.
- secure-parameter-default fires because a parameter whose name suggests a secret has a hardcoded default.
Warnings exist, but do they prevent a build? Before you run the next command, predict: will the build succeed or fail?
- Test whether the linter stops the build by running this command in the VS Code terminal:
az bicep build --file main.bicep
The build succeeds. You now have a compiled main.json file sitting in your folder.
This is the problem. Warnings are advisory. Insecure code compiles, and compilable code can be deployed. Nothing stopped you from shipping a template that leaks keys and allows public blob access.
Build failed instead of succeeding?
- If you see a syntax error, double-check that you pasted the template exactly as shown. A missing brace or quote will cause a build failure.
- If you see the error bicep not found, run az bicep install to install the Bicep CLI, then retry.
- help me debug why my Bicep build is failing
✔️ Awesome, I've got everything!
Great. Double check you have saved main.bicep and see the yellow warnings in the Problems panel.
ⓧ I'd like to double check the full code
Your main.bicep should look like this:
param storageAccountPassword string = 'supersecret123'
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: 'mystorageaccount'
location: resourceGroup().location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
allowBlobPublicAccess: true
minimumTlsVersion: 'TLS1_0'
networkAcls: {
defaultAction: 'Allow'
}
}
}
output storageKey string = storageAccount.listKeys().keys[0].value
You have proven that the default linter is toothless. It warns, but it does not block. In the next step, you will promote those warnings to errors so insecure code can never compile again.
Enforce Security Rules with bicepconfig.json
In the previous step, you saw something alarming: your insecure template compiled successfully. The Bicep linter flagged the security issues as yellow warnings, but those warnings did nothing to stop the build.
In a real team, that means a developer could push insecure infrastructure to production without ever being blocked. You need a way to turn those polite warnings into hard errors that break the build. That is exactly what bicepconfig.json does.
In this step, get ready to:
- Create a bicepconfig.json that escalates security linter rules to build-breaking errors.
- Confirm the Bicep build now fails on your insecure template.
Create the security-enforcing config
The Bicep linter uses a file called bicepconfig.json to determine how it responds to rule violations. By default, security-related rules are set to warning, which means they surface in the editor but do not block the build.
- Create a new file called bicepconfig.json in your secure-storage folder (the same folder that contains main.bicep).
- Paste the following configuration into bicepconfig.json:
{
"analyzers": {
"core": {
"enabled": true,
"rules": {
"outputs-should-not-contain-secrets": {
"level": "error"
},
"secure-secrets-in-params": {
"level": "error"
},
"secure-parameter-default": {
"level": "error"
},
"adminusername-should-not-be-literal": {
"level": "error"
},
"use-secure-value-for-secure-inputs": {
"level": "error"
}
}
}
}
}
What does this configuration do?
Each rule listed here was previously set to warning by default. By changing the level to error, any violation now causes the build to fail completely.
- outputs-should-not-contain-secrets flags outputs that expose secrets via listKeys() or @secure() parameters.
- secure-secrets-in-params flags parameters with names containing "password" or "secret" that lack the @secure() decorator.
- secure-parameter-default flags hardcoded default values on secure parameters.
- adminusername-should-not-be-literal flags literal admin usernames.
- use-secure-value-for-secure-inputs flags passwords passed without @secure() on VMs and scale sets.
- Save bicepconfig.json.
- Switch to the main.bicep tab in VS Code and look at the editor. The yellow squiggles from earlier should now be red squiggles.
- Open the Problems panel (Ctrl+Shift+M on Windows, Cmd+Shift+M on macOS). You should see entries marked as errors rather than warnings.
Why did the squiggles change colour?
Bicep resolves the nearest bicepconfig.json in the directory tree and merges it with the default configuration. Because your config now lives in the same folder as main.bicep, the linter immediately picked up the escalated rule levels and re-evaluated the file.
Still seeing yellow warnings instead of red errors?
- Make sure bicepconfig.json is in the same folder as main.bicep, not in a subfolder or parent folder.
- Check the JSON is valid. A missing comma or bracket will prevent the config from loading. Look for a notification at the bottom of VS Code.
- Try closing and reopening main.bicep to force the extension to reload the config.
Why aren't my Bicep linter rules escalating to errors?
Confirm the build now fails
You saw the editor feedback, but the real test is whether the CLI build is now blocked. In the previous step, az bicep build succeeded despite the warnings.
Before you run the command again, predict: will the build succeed or fail this time?
- Run the build command in your terminal:
az bicep build --file main.bicep
The build fails. You should see red error text in the terminal output referencing the security rules you escalated. The command returns a non-zero exit code, which means it would also break a CI/CD pipeline automatically.
What changed between this run and the last one?
The template code is identical. The only difference is the presence of bicepconfig.json in the folder. That single file turned harmless warnings into build-breaking errors. This is the "guardrail" pattern that professional teams use to prevent insecure infrastructure from ever being deployed.
Build still succeeds with no errors?
- Run ls in your terminal and confirm bicepconfig.json appears in the same directory as main.bicep.
- Open bicepconfig.json and verify the rule levels say "error" (not "warning" or "Error" with a capital E).
- Ensure you saved the file before running the build command.
Why is my az bicep build still succeeding after I created bicepconfig.json?
✔️ Awesome, I've got everything!
Great. Double check you have saved bicepconfig.json and confirmed the build fails with error output.
ⓧ I'd like to double check the full code
Here is the complete bicepconfig.json for comparison:
{
"analyzers": {
"core": {
"enabled": true,
"rules": {
"outputs-should-not-contain-secrets": {
"level": "error"
},
"secure-secrets-in-params": {
"level": "error"
},
"secure-parameter-default": {
"level": "error"
},
"adminusername-should-not-be-literal": {
"level": "error"
},
"use-secure-value-for-secure-inputs": {
"level": "error"
}
}
}
}
}
Your main.bicep should still be unchanged from the previous step. It intentionally contains the insecure code that you will fix next.
Your guardrail is in place. Insecure code can no longer compile. Next up, you will fix each security violation one by one until the template passes cleanly.
Fix the Security Violations
Your bicepconfig.json made insecure code fail to compile. That is exactly the guardrail you want in a professional workflow.
Now you need to fix each violation so the template compiles again. This step teaches you the secure patterns that every Bicep template should follow: the @secure() decorator, removing secrets from outputs, and proper parameter handling.
In this step, get ready to:
- Replace the insecure password parameter with a properly decorated storage account name parameter.
- Remove the output that leaks the storage account key.
- Confirm the build passes with zero errors.
Fix parameter security
The linter flagged your storageAccountPassword parameter because its name contains the keyword "password" without an @secure() decorator. It also has a hardcoded default value, which defeats the purpose of marking something secure.
The fix is to remove this parameter entirely and replace it with a properly secured storage account name parameter.
- In main.bicep, find this line at the top of the file:
param storageAccountPassword string = 'supersecret123'
- Delete that entire line and replace it with the following secure parameter declarations:
// Mark the storage account name as secure and constrain its length
@secure()
@minLength(3)
@maxLength(24)
param storageAccountName string
// Use the resource group's location as a safe default
param location string = resourceGroup().location
What does this code do?
The @secure() decorator tells Azure Resource Manager to never log or display this parameter's value in deployment history, the portal, or CLI output.
The @minLength(3) and @maxLength(24) decorators enforce Azure's naming constraints for storage accounts at compile time instead of at deployment time.
The location parameter with resourceGroup().location as the default is a best practice so templates deploy to whatever region the resource group lives in.
Now you need to update the storage account resource to use these new parameters instead of hardcoded values.
- In the storageAccount resource block, find these two lines:
name: 'mystorageaccount'
location: resourceGroup().location
- Replace them with references to your new parameters, so they look like this:
name: storageAccountName
location: location
- Save main.bicep.
- Check the VS Code Problems panel (Ctrl+Shift+M). The secure-secrets-in-params and secure-parameter-default errors should be gone. You should still see one remaining error for the output line.
Still seeing parameter errors?
- Make sure you deleted the entire param storageAccountPassword line. If any trace of it remains, the linter will still flag it.
- Confirm your @secure() decorator sits directly above the param storageAccountName line with no blank lines between them.
- Check that you updated name: storageAccountName (no quotes). Wrapping it in quotes turns it into a literal string instead of a parameter reference.
Help me fix my Bicep parameter security errors.
Fix the output leak
The outputs-should-not-contain-secrets rule flags your output because listKeys() exposes the storage account key. Anyone with access to the deployment history could read it.
- At the bottom of main.bicep, find this line:
output storageKey string = storageAccount.listKeys().keys[0].value
- Replace it with a safe output that exposes only the resource ID:
output storageId string = storageAccount.id
Why is the resource ID safe to output?
A resource ID is not a secret. It is a public identifier that locates the resource within Azure's hierarchy. Knowing it does not grant access to the storage account's data.
The account key, on the other hand, grants full read/write access to every blob, queue, and table in the account. Outputting it means anyone who can view deployment history can steal your data.
- Save main.bicep.
- Check the Problems panel again. All red error squiggles should now be gone.
Still seeing an output error?
- Make sure you removed the entire old output line including listKeys(). If both lines exist, the linter will still flag the old one.
- Confirm the new output uses storageAccount.id (lowercase id). The property name is case-sensitive in Bicep.
Help me fix my Bicep output security error.
Run the build
Before you run this, predict: do you expect the build to succeed or fail now that you have fixed the parameter and output violations?
- Confirm the build succeeds cleanly by running this command:
az bicep build --file main.bicep
The build should complete with zero errors and zero warnings. If any warnings remain, fix them before continuing.
What does a successful build produce?
The az bicep build command compiles your .bicep file into an ARM JSON template (main.json). A clean build with no output messages means the linter found nothing to complain about. Your security guardrails are passing.
Build still failing?
- Read the error message carefully. It names the exact rule that is failing and the line number.
- If you see secure-secrets-in-params, a parameter name still contains a keyword like "password" or "secret" without @secure().
- If you see outputs-should-not-contain-secrets, an output still references listKeys() or another secret-exposing function.
Help me debug my Bicep build errors.
✔️ Awesome, I've got everything!
Great. Double check you have saved main.bicep and that the build produces zero errors.
ⓧ I'd like to double check the full code
Here is the full main.bicep as it should look at the end of this step:
@secure()
@minLength(3)
@maxLength(24)
param storageAccountName string
param location string = resourceGroup().location
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
allowBlobPublicAccess: true
minimumTlsVersion: 'TLS1_0'
networkAcls: {
defaultAction: 'Allow'
}
}
}
output storageId string = storageAccount.id
Your template now passes all five security linter rules. But passing the linter is necessary, not sufficient. The storage account itself still allows public blob access, old TLS versions, and unrestricted network traffic. Next up, you will harden those properties so the template is secure at both compile time and deployment time.
Harden the Storage Account Properties
Your template now passes the security linter with zero errors. That means the code structure is secure. But the actual storage account properties still leave the door wide open.
The resource still accepts unencrypted HTTP traffic, allows ancient TLS versions, and lets anyone on the internet reach it. In this step, you will harden the runtime properties that Azure evaluates at deployment time, then validate the finished template against Azure without deploying anything.
In this step, get ready to:
- Lock down transport to HTTPS-only with TLS 1.2 minimum.
- Block all public network access and add encryption configuration.
- Validate the hardened template against Azure without deploying resources.
Lock down transport and access
The properties block currently allows unencrypted HTTP, accepts TLS 1.0 connections, and exposes blobs publicly. You will flip each of these to their secure setting.
- In main.bicep, find the properties block inside your storage account resource.
- Find these lines:
allowBlobPublicAccess: true
minimumTlsVersion: 'TLS1_0'
- Replace them with the following three lines, which add HTTPS enforcement, upgrade TLS, and disable public blob access:
// Enforce HTTPS-only traffic
supportsHttpsTrafficOnly: true
// Reject connections using old TLS versions
minimumTlsVersion: 'TLS1_2'
// Prevent anonymous public access to blobs
allowBlobPublicAccess: false
What do these properties do?
Each line hardens a different attack surface. supportsHttpsTrafficOnly forces all traffic through encrypted HTTPS. minimumTlsVersion rejects any client trying to connect with the outdated TLS 1.0 or 1.1 protocols.
Setting allowBlobPublicAccess to false means no one can read your blobs without explicit authentication.
- Save main.bicep.
- Confirm the build still passes by running:
az bicep build --file main.bicep
You should see no error output and a new main.json file appears in your folder (the compiled ARM template).
Seeing a build error?
Check that you removed the old allowBlobPublicAccess: true and minimumTlsVersion: 'TLS1_0' lines entirely. Duplicating a property name causes a compile error.
Also confirm you used 'TLS1_2' with the underscore and no space. The value is case-sensitive.
help me fix a Bicep build error after changing my storage account properties
Add network deny-by-default rules and encryption
Transport is locked down, but the storage account still allows network traffic from anywhere. You will add a deny-by-default network rule and explicit encryption configuration.
- In main.bicep, find the current networkAcls block:
networkAcls: {
defaultAction: 'Allow'
}
- Replace the entire networkAcls block and add publicNetworkAccess above it plus an encryption block below it, so the section looks like this:
// Block all public network access
publicNetworkAccess: 'Disabled'
// Deny all traffic by default, allow Azure services only
networkAcls: {
bypass: 'AzureServices'
defaultAction: 'Deny'
}
// Explicitly configure encryption at rest
encryption: {
keySource: 'Microsoft.Storage'
services: {
blob: {
enabled: true
keyType: 'Account'
}
file: {
enabled: true
keyType: 'Account'
}
}
}
What does this code do?
Setting publicNetworkAccess: 'Disabled' completely blocks traffic from the public internet. The networkAcls block sets defaultAction: 'Deny' so even internal traffic is blocked unless it comes from an allowed Azure service.
The encryption block explicitly enables encryption at rest for blob and file services using Microsoft-managed keys. Azure enables this by default, but declaring it explicitly makes your security posture auditable in the template itself.
- Save main.bicep.
- Run the build to confirm everything compiles cleanly:
az bicep build --file main.bicep
You should see zero errors and zero warnings. The template is now fully hardened at the code level.
Getting a syntax error in the encryption block?
Bicep is sensitive to indentation and missing closing braces. Make sure every opening { has a matching }. Count the braces in your encryption block.
Also check that publicNetworkAccess sits inside the properties block (one level in from resource), not outside it.
help me fix a Bicep syntax error in my encryption or networkAcls block
✔️ Awesome, I've got everything!
Great. Double check you have saved main.bicep and the build outputs no errors or warnings.
ⓧ I'd like to double check the full code
Here is the complete main.bicep at this point. Compare yours line by line:
@secure()
@minLength(3)
@maxLength(24)
param storageAccountName string
param location string = resourceGroup().location
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
allowBlobPublicAccess: false
publicNetworkAccess: 'Disabled'
networkAcls: {
bypass: 'AzureServices'
defaultAction: 'Deny'
}
encryption: {
keySource: 'Microsoft.Storage'
services: {
blob: {
enabled: true
keyType: 'Account'
}
file: {
enabled: true
keyType: 'Account'
}
}
}
}
}
output storageId string = storageAccount.id
Validate against Azure
The template compiles locally. Now you will ask Azure itself whether it would accept this template for a real deployment. The validate command checks everything the linter cannot: quota, naming rules, and region availability.
- Log in to Azure by running:
az login
What does az login do?
This opens a browser window where you sign in with your Azure account credentials. Once authenticated, the CLI stores a token so subsequent commands can talk to your Azure subscription.
- Create a resource group for validation by running:
az group create --name rg-secure-bicep-test --location eastus
You should see JSON output with "provisioningState": "Succeeded" confirming the resource group was created.
Why create a resource group?
The validate command needs a target resource group to check naming constraints and region availability. No actual resources are deployed inside it. The group itself costs nothing.
Before you run the next command, predict: will Azure accept or reject this template?
- Run the validation command (this does NOT deploy, it only checks if Azure would accept the template):
az deployment group validate --resource-group rg-secure-bicep-test --template-file main.bicep --parameters storageAccountName=sectest$(date +%s)
What does this command do?
The --parameters flag passes a value for your @secure() parameter. The $(date +%s) suffix appends a timestamp to create a unique name that satisfies Azure's globally-unique naming requirement.
The validate subcommand sends the template to Azure for preflight checks without actually creating any resources.
You should see JSON output containing "provisioningState": "Succeeded". Azure has confirmed it would accept your hardened template for deployment.
Seeing a validation error?
If you see an error about the storage account name, make sure the @minLength(3) and @maxLength(24) decorators are present on your parameter. Storage account names must be 3-24 characters, lowercase letters and numbers only.
If you see a subscription or authorization error, confirm az login completed successfully and you have an active subscription by running az account show.
help me fix an Azure deployment validation error for my Bicep storage account template
Your template is production-ready. It passes a strict security linter, hardens every property that matters, and Azure itself has confirmed it would accept the deployment. Next up, stretch your skills with a Secret Mission that adds Key Vault and secure secret passing.
Secret mission
Add Key Vault and Secure Secret Passing
Real-world templates need secrets like connection strings and API keys. The professional pattern is to store them in Azure Key Vault and pass them to modules using getSecret. In this mission, you will create a multi-file Bicep module structure and use the getSecret function to pass a Key Vault secret into a child module.
Clean Up Your Resources
Clean Up Your Resources
This project costs nothing to keep running. The resource group you created contains no deployed resources, and your local files use negligible disk space.
Decide whether to keep your resources for future Bicep experiments, or delete them entirely.
Resources you used:
- Resource group rg-secure-bicep-test in your Azure subscription (empty, no deployed resources inside).
- Local secure-storage folder containing main.bicep, storageModule.bicep, and bicepconfig.json.
- Bicep VS Code extension (ms-azuretools.vscode-bicep).
Keep everything running
No action needed. Choose this if you plan to keep experimenting with Bicep templates or want to reuse the resource group for future validation runs.
- Your rg-secure-bicep-test resource group stays in Azure at $0 cost (it contains no resources).
- Your local secure-storage folder and Bicep extension remain ready for your next project.
Pause - I'll come back to this later
There are no running processes or billable resources to pause. Your resource group is already idle and costs nothing.
- Your environment is already in a paused state. Come back any time and pick up where you left off.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below.
- Delete the Azure resource group by running this command in your terminal:
az group delete --name rg-secure-bicep-test --yes --no-wait
The --no-wait flag returns control immediately. Azure deletes the group in the background.
- Delete your local project folder. On macOS or Linux, run:
rm -rf ~/Desktop/secure-storage
- On Windows, run:
rmdir /s /q "$HOME\Desktop\secure-storage"
- Uninstall the Bicep VS Code extension. Press Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows) to open the Extensions panel.
- Find Bicep in your installed extensions list, click the gear icon, and select Uninstall.
Nice Work!
Nice Work!
Nice work! You just built a production-ready, security-hardened Bicep template that enforces encryption, network lockdown, and secure parameter handling.
You've learned how to:
- Build a security-hardened Azure Storage Account template with HTTPS-only traffic, TLS 1.2 minimum, disabled public blob access, and deny-by-default network rules.
- Create a bicepconfig.json that escalates security linter rules to build-breaking errors so insecure code cannot compile.
- Apply the "write wrong, lint catches, fix it" workflow that security engineers use daily to iteratively harden infrastructure templates.
- (Secret Mission) Created a multi-file module structure with Azure Key Vault and secure secret passing via the getSecret function.
Ready to quiz yourself?