DevSecOps

Azure Bicep vs ARM: Security Comparison

Bicep and ARM templates produce the same deployments, but their security properties diverge — in module provenance, what-if analysis, registry trust, and review experience.

Shadab Khan
Security Engineer
8 min read

Teams picking between Bicep and ARM for a new Azure platform usually frame the decision in terms of authoring ergonomics: "Bicep is easier to read, ARM is more verbose." That is true and mostly uninteresting, because both languages compile to the same ARM JSON that the Azure Resource Manager control plane ingests. What the ergonomics framing misses is that Bicep has meaningfully different security properties from hand-authored ARM, and those differences matter for supply chain integrity, review rigour, and deployment predictability.

I have shipped both at scale, including a full ARM-to-Bicep migration for a regulated financial workload in 2023. This post is the security comparison I wrote as part of that migration's architecture review, updated for the features Microsoft has shipped through H1 2024.

The Core Technical Distinction

Bicep is a DSL that compiles to ARM JSON. The compiled output is what Azure actually deploys. So at the level of "what does Azure do with my template," Bicep and ARM are identical — they both produce an ARM deployment, Azure Resource Manager evaluates it, and the resources are created through the same set of resource provider APIs.

Where they differ is in authoring, modularity, and toolchain. ARM JSON is one format; Bicep is another format that compiles to it. The compilation is deterministic and reproducible, and bicep build on the same input produces the same output. You can always convert in either direction with bicep build and bicep decompile. This means "Bicep vs ARM" is a supply chain and workflow decision, not a runtime decision.

Module Provenance: Bicep Registries vs Nested ARM

The single biggest security difference between the two is how they handle reusable modules.

ARM supports linked and nested templates. A linked template is a URI to another ARM template file, typically hosted on a storage account or a Git raw URL. At deployment time, ARM fetches the URI, parses the template, and deploys it. The URI is evaluated by the deployment engine, not by you. If the storage account or Git branch is mutable, the linked template can be silently replaced.

Bicep modules are compiled in at build time. When you reference br/myregistry:modules/network:v1, the Bicep compiler pulls that module version from the specified container registry at compile time, embeds its compiled ARM into the output, and the deployment sees a single template. There is no runtime fetch. The supply chain is "compile time pulls from the registry; the compiled ARM is the deployment artifact."

Bicep private registries (GA since 2022) are backed by Azure Container Registry (br:myregistry.azurecr.io/...) or the public Bicep registry (br/public:...). ACR-backed registries inherit ACR's RBAC, signing, and scope map features — the Container Registry trust model applies directly. Public modules are published to a Microsoft-managed registry; those should be pinned to specific versions and reviewed on upgrade like any third-party dependency.

For a regulated workload, the Bicep model is strictly better. The ARM linked-template model can be made safe — use immutable blob URIs, use SAS tokens with tight expiry, mirror into a controlled storage account — but every one of those controls is a thing to maintain, and none of them are the default. Bicep's compile-time embedding is the default and is the safer default.

What-If Analysis and Deployment Stacks

Both Bicep and ARM support what-if — a preview operation that shows the resource changes a deployment would make without making them. This is the primary pre-deployment security review control for infrastructure, and it is equally available in both languages because it is a property of the ARM control plane, not the authoring language.

Where the two diverge in practice is quality of diff. what-if against a Bicep deployment surfaces clearer change sets, because Bicep preserves parameter names and resource identifiers that map to the source file. ARM's what-if output is still correct but harder to read when the template uses complex copy loops or computed resource names.

The newer primitive is Deployment Stacks (GA in May 2024). A deployment stack is a managed resource that tracks the set of resources a deployment owns, and it supports "deny deletion" and "apply" modes that prevent out-of-band modifications to stack-managed resources. Both Bicep and ARM can deploy into stacks — this is a control-plane feature — but Bicep's tooling around stacks is more mature (az stack group create --template-file main.bicep Just Works; the ARM path requires the compiled JSON).

Deployment stacks are the closest thing Azure has to Terraform-style state management, and for regulated workloads they close the gap between "what I deployed" and "what exists." The supply chain relevance is that an attacker who creates an out-of-band resource (a storage account with public access, an unused VM with a broad identity) cannot hide the resource from a deployment stack — it will be flagged as unmanaged on the next deployment.

Review Experience and the Lines-of-Code Effect

This is the argument I did not expect to make when I started the migration but ended up making repeatedly. Bicep templates are roughly 40% fewer lines than equivalent ARM, and the reduction is almost entirely in syntax noise — fewer nested objects, implicit dependency resolution, symbolic references instead of resourceId() calls. Fewer lines means reviewers read the template. More lines means reviewers skim.

I have reviewed hundreds of ARM PRs where the actual change was one property on one resource, but the diff ran to 200 lines because the copy block expanded into every child resource. The review was "looks fine to me," and the change landed, and the copy block had a subtle bug that was only caught in production. The Bicep equivalent of the same change is 5 lines of diff, and the reviewer actually reads them.

This is not a theoretical difference. Every DevSecOps post I have read on Azure IaC has some version of "your review culture is your primary control." A language that makes reviews easier produces better reviews.

Parameters, Secrets, and the @secure Decorator

Both languages support secure parameters — values that are not logged in deployment history and are not readable through the Azure portal after deployment. In ARM, you mark them with "type": "securestring". In Bicep, you use the @secure() decorator. Both integrate with Key Vault through parameter file references.

The Bicep path has two small advantages. First, the decorator is syntactically adjacent to the parameter declaration, which makes it harder to miss on review. Second, Bicep linters (available through the Bicep VS Code extension and bicep lint) warn when a secure parameter is used in an output or passed to a non-secure parameter — a subtle leak that happens regularly in hand-authored ARM and is much harder to spot in 300 lines of JSON.

For secret references specifically, both languages support getSecret() on a Key Vault resource, and both evaluate the reference at deployment time. The secret is not stored in the template. This is a Microsoft control plane feature, not a language feature, so it works identically in both.

CI/CD Integration and SBOM Considerations

Bicep ships a compiler as a portable binary that integrates cleanly with Azure DevOps and GitHub Actions. The standard pipeline compiles Bicep to JSON, runs bicep lint, runs az deployment group validate and what-if against the target environment, and applies on approval. Every one of those steps runs against the compiled JSON, so the runtime artifact is the same as an ARM deployment would be.

For SBOM generation, Bicep has a natural unit — the compiled main template plus the referenced modules. bicep build --stdout plus the module lockfile (bicep.lockfile, GA 2024) gives you a deterministic list of exactly which module versions were compiled in. Feeding that into an SBOM generator captures the infrastructure dependency tree the way a language package lock captures application dependencies. ARM has no equivalent — linked templates are resolved at deploy time, and there is no lockfile.

The Migration Cost

Converting a large ARM codebase to Bicep is not free. bicep decompile gets you a syntactically valid Bicep file from any ARM template, but the output is not idiomatic and usually needs a human pass to use module registries, symbolic references, and decorators. For a large codebase, budget one engineer-week per 10,000 lines of ARM, plus regression testing the deployments against a non-production environment.

The migration is worth it for any team that will maintain the templates for more than 12 months. For a one-off deployment that will be thrown away, the conversion is overhead.

How Safeguard Helps

Safeguard scans both Bicep source and compiled ARM for the patterns that produce incidents — overscoped role assignments, public-network resources, secure parameters leaked into outputs, and module references that float rather than pin. For Bicep, it reads bicep.lockfile and reconciles compiled module versions against known vulnerable or withdrawn modules in the public registry. For ARM, it resolves linked-template URIs and flags mutable references. The comparison I walked through in this post becomes a concrete migration backlog with blast radius scoring, so teams know which stacks benefit most from converting.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.