Blog Azure Infrastructure Security & Compliance Cloud Native

7 Top Security Mistakes in Azure Bicep and How to Avoid Them

Azure Bicep has become the default for deploying Infrastructure as Code (IaC) in Azure, substituting the more complicated Azure Resource Manager (ARM) templates. 

This article outlines seven common security mistakes we see when using Bicep. 

Niels Kroeze

Author

Niels Kroeze IT Business Copywriter

Reading time 6 minutes Published: 19 November 2025

7 Security mistakes you should avoid 

When it comes to using Azure Bicep securely, we recommend you to avoid the following mistakes: 

  1. Leaving secrets in source control 
  2. Not securing inputs properly 
  3. Exposing secrets in outputs 
  4. Weak or default naming conventions 
  5. Not validating parameters  
  6. Over-permissive role assignments 
  7. Skipping policy enforcement 

 

1. Leaving secrets in source control 

One of the most obvious mistakes in Bicep is accidentally committing secrets, such as passwords or access keys, straight into source control. This exposes credentials to anyone with access to the repository, which we all know should be avoided. However, it’s very easy to leave them in, in particularly when testing your Bicep configurations locally. 

To avoid this, never hardcode secrets in your Bicep files. Instead: 

  • Pass parameters in through a command line 
  • Use a separate parameters JSON file and add it to .gitignore to stop Git from tracking it in future commits. Keep in mind this doesn’t remove any secrets already committed as you’ll need to delete them from history and rotate those credentials.
Iac Whitepaper

Want to learn more about Infrastructure as Code (IaC) in Azure?

Read our latest whitepaper about IaC in Azure and master Azure Bicep, Azure Verified Modules (AVM) and more!

Download the IaC whitepaper for free!

2. Not securing inputs properly 

Sensitive inputs (e.g. admin credentials) should always be treated with care. Without proper protection, secrets can end up being logged or exposed unintentionally. Bicep offers the @secure() decorator for string and object parameters that keep them safe.

For example: 

@secure() 
param adminPassword string   
@secure() 
param adminCredentials object 

Using these decorators ensures that sensitive inputs are hidden in outputs and deployment logs. 

 

3. Exposing secrets in outputs 

Outputs are handy for passing values between Bicep modules. However, they can also cause hidden risks if containing sensitive information. For instance, when you output a connection string directly, Bicep generates a warning you might be exposing a secret, like when doing this to a storage account:

output connectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageaccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageaccount.id, storageaccount.apiVersion).keys[0].value}'

That warning is useful, but it doesn’t always catch everything. If you assign the connection string to a variable first and then output that variable, no warning will appear – even though the output still contains a secret:

var connectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageaccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageaccount.id, storageaccount.apiVersion).keys[0].value}' 

output connectionString = connectionString 

Take the following example of deploying a storage account: 

deploy.bicep 
param location string = resourceGroup().location 
param tags object = {} 
param storageName string = 'stsecureteststore' 
param sku string = 'Standard_LRS' 

module storageModule 'modules/storage.bicep' = { 
  name: 'StorageDeploy' 
  params: { 
    location: location 
    storageName: storageName 
    tags: tags 
    sku: sku 
  } 
} 

modules/storage.bicep 

@description('The storage account name') 
@minLength(3) 
@maxLength(24) 
param storageName string 
@description('The storage account location') 
param location string 
@description('The tags for the storage account') 
param tags object 
@description('The storage account sku')  
@allowed(['Standard_LRS', 'Standard_GRS', 'Standard_GZRS', 'Standard_RAGRS', 'Standard_RAGZRS', 'Standard_ZRS', 'Premium_LRS', 'Premium_ZRS']) 
param sku string = 'Standard_LRS' 
@description('The access tier for the blob services')  
@allowed(['Hot', 'Cool'])  
param accessTier string = 'Hot'  
@description('Allow public access to blobs')  
param allowBlobPublicAccess bool = false  

resource storageaccount 'Microsoft.Storage/storageAccounts@2025-06-01' = { 
  name: storageName 
  location: location 
  kind: 'StorageV2' 
  tags: tags 
  sku: { 
    name: sku 
  } 
  properties: { 
    supportsHttpsTrafficOnly: true 
    minimumTlsVersion: 'TLS1_2' 
    accessTier: accessTier 
    allowBlobPublicAccess: allowBlobPublicAccess 
  } 
} 

var connectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageaccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageaccount.id, storageaccount.apiVersion).keys[0].value}' 

When this deployment runs, the connectionString output will be visible under Deployments in the Azure Portal.  

Because it contains the account key in plain text, anyone with permissions to view deployments in the resource group can see it. This creates an unnecessary security risk.  

The best practice is to always avoid exposing secrets in outputs altogether and limit outputs to non-sensitive values, such as resource names or IDs.  

Consider using Azure Key Vault instead, and always review outputs carefully during template design to make sure you aren’t unintentionally leaking access keys or credentials. Bicep has native support for pulling secrets directly from Key Vault, and you can pass them into modules securely.  

Note:

The Key Vault must be configured to allow access through Azure Resource Manager for template deployment.

One caveat is that the getSecret method only works when it’s assigned to a module parameter marked with the @secure decorator.  

deploy.bicep 
param location string = resourceGroup().location 
param tags object 
param sqlServerName string 
param keyVaultName string 
param keyVaultResourceGroupName string 
param subscriptionId string = subscription().subscriptionId 

resource vaultResource ' Microsoft.KeyVault/vaults@2025-05-01' existing = { 
  name: keyVaultName  
  scope: resourceGroup(subscriptionId, keyVaultResourceGroupName  ) 
} 
module sqlModule 'modules/sql.bicep' = { 
  name: 'SqlDeploy' 
  params: { 
    location: location 
    tags: tags 
    sqlServerName: sqlServerName 
    administratorLogin: vaultResource.getSecret('sqlUser') 
    administratorLoginPassword: vaultResource.getSecret('sqlPassword') 
  }   
} 

modules/sql.bicep 
@description('The resource location') 
param location string 
@description('The tags for the resources') 
param tags object 
@description('The name for the SQL Server') 
param sqlServerName string 
@secure() 
@description('The SQL Administrator Login') 
param administratorLogin string 
@secure() 
@description('The SQL Administrator password') 
param administratorLoginPassword string 

resource sqlServerResource 'Microsoft.Sql/servers/databases/extensions@2024-11-01-preview' = { 
  name: sqlServerName 
  location: location 
  tags:tags 
  properties: { 
    administratorLogin: administratorLogin 
    administratorLoginPassword: administratorLoginPassword 

  } 
} 

This way, secrets stay in Key Vault and aren’t exposed in outputs or logs. 

 

4. Weak or default naming conventions 

Another “easy” mistake is to use predictable names for resources. Similar to those who have these “easy-to-guess” family passwords, using obvious naming such as teststorage or productionvm isn’t recommended. It’s just too risky, as these names indicate too much details about your environment, inviting attackers to guess resource usage and exploit their chances. 

Example: 

resource storageaccount 'Microsoft.Storage/storageAccounts@2025-06-01' = { 
  name: 'prodstorage' 
  ... 
} 

Better: Use names with parameters with random suffixes or unique IDs like: 

param storageName string = 'st${uniqueString(resourceGroup().id)}' 

resource storageaccount ''Microsoft.Storage/storageAccounts@2025-06-01' = { 
  name: storageName 
  ... 
} 

 

5. Not validating parameters  

Accepting any input values for parameters without restrictions is another, yet unnecessary mistake, happening often with Bicep. Think of typos, unexpected values, or even malicious inputs. These can all cause insecure or broken deployments, which you rather avoid.

Example (too open):  

param sku string 

Better: Add @allowed constraints or use parameter validation. 

@allowed(['Standard_LRS', 'Standard_ZRS', 'Premium_LRS']) 
param sku string = 'Standard_LRS' 

 

6. Over-permissive role assignments 

Assigning broad roles with too much access, like the “owner” or “contributor” role, when only limited access is required, increases the chance of accidental or malicious changes.  

Example:

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 
  name: guid(resourceGroup().id, principalId, roleDefinitionId) 
  properties: { 
    principalId: principalId 
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') // Contributor 
  } 
} 

Better: Assign the smallest possible role, like Storage Blob Data Reader if the user only needs read access. 

 

7. Skipping policy enforcement 

Last but not least, deploying resources without guardrails like Azure Policy or template validation is a serious missout. It risks non-compliant configurations, slipping through open storage accounts, public IPs, you name it.  

Example: A developer deploys a storage account with allowBlobPublicAccess = true.

 param allowBlobPublicAccess bool = true 

Better: Enforce a policy that denies public access at the subscription or management group level. You can also set secure defaults in Bicep: 

param allowBlobPublicAccess bool = false 
Note:

At Intercept, we always recommended avoiding public access, like public blob access in Azure Storage, to prevent accidental data exposure. Only do this if there is a really specific use case. 

Closing thoughts

These mistakes are frequent, but with the right practices outlined here, they’re pretty easy to avoid. By managing secrets securely, protecting inputs, reviewing outputs, and applying strong governance, you’re already well underway to prevent misconfigurations from becoming serious trouble. 

Deni visual

Get in Touch!

Let's join forces on your cloud journey.