An Azure Resource Manager template can deploy a fully working storage account, SQL server, or key vault without a single security property set — and Azure will happily provision it. Microsoft.Storage/storageAccounts does not enforce minimumTlsVersion: "TLS1_2" unless the template author sets it explicitly; omit the property and older API versions can still negotiate TLS 1.0 or 1.1. allowBlobPublicAccess follows the same pattern: leave it unset, and any container-level "public access" setting an engineer flips later takes effect immediately, because nothing at the account level blocked it. These aren't edge cases — they're the literal default state of an ARM template the moment az deployment group create runs. Static scanners like Checkov ship dedicated CKV_AZURE_* checks for exactly this class of problem — public blob containers, SQL firewalls open to 0.0.0.0/0, missing diagnostic logging — because ARM's declarative JSON (or its Bicep front-end) makes it easy to define infrastructure that is functionally correct and security-absent at the same time. This piece walks through the insecure defaults that show up most often in real ARM templates, what the hardened property actually looks like, and how to catch the gap before deployment rather than after an audit.
Why doesn't ARM enforce TLS 1.2 by default?
ARM doesn't enforce TLS 1.2 by default because minimumTlsVersion is an optional property on Microsoft.Storage/storageAccounts, and Azure's platform-level default has historically permitted older protocol versions unless a template — or a later policy assignment — overrides it. Microsoft's own documentation on configuring the minimum TLS version for storage accounts is explicit that this is a per-resource setting you opt into, not a platform floor baked into every new account. A template that defines a storage account with only sku, kind, and properties.accessTier set is syntactically valid and deploys cleanly, but it inherits whatever the platform default happens to be rather than a deliberate "TLS1_2" value. The fix is one line inside the resource's properties block: "minimumTlsVersion": "TLS1_2". The problem is scale, not difficulty — a single ARM template with a dozen storage resources needs the property set a dozen times, and a copy-pasted template from an internal wiki or an old Azure Quickstart repo often predates the property being considered a baseline requirement at all.
What does "public by default" actually mean for a storage account?
"Public by default" means that without allowBlobPublicAccess: false set at the storage account level, nothing prevents a container's own access-level setting from exposing its blobs to anonymous internet requests. The account-level flag is a gate: when it's absent or true, container-level public access settings (Blob or Container) are permitted to take effect; when it's explicitly false, those settings are overridden and blocked account-wide regardless of what any individual container specifies later. PSRule for Azure — Microsoft's open-source rule set for validating Azure resources — documents a hardened storage baseline that goes beyond this single flag: allowBlobPublicAccess: false, supportsHttpsTrafficOnly: true, allowSharedKeyAccess: false (forcing Azure AD-based access instead of account keys), and networkAcls.defaultAction: "Deny" to require explicit network rules. None of these four properties are populated by ARM if a template omits them — each is a decision the template author has to make affirmatively, and a template that never makes it deploys with all four gates open.
Why do secrets end up in ARM deployment history?
Secrets end up in ARM deployment history because a plain string parameter type gets logged and persisted the same way any other parameter value does, and deployment history is retained and queryable by anyone with reader access to the resource group. If a template declares "adminPassword": { "type": "string" } and a pipeline passes a real credential as that parameter's value at deploy time, that value is written into the deployment record in the Azure portal and CLI output — in plaintext, indefinitely, until the deployment history entry is deleted. ARM and Bicep both support a dedicated secureString (and secureObject) parameter type specifically to prevent this: values passed as secureString are not persisted or displayed in deployment history or activity logs. The more robust pattern, which Microsoft's own guidance and independent write-ups (including Xebia's analysis of ARM secret handling) both recommend, is to avoid inlining credentials as parameters at all and instead reference a Key Vault secret directly in the template using a Microsoft.KeyVault/vaults/secrets reference expression, so the secret value never transits the deployment pipeline as a literal string.
What does Azure's own pre-deployment safety check catch — and why is it underused?
Azure's what-if operation is a GA feature — available via az deployment group create --confirm-with-what-if in the CLI or -WhatIf in Azure PowerShell — that renders a preview of every change a template would make (Create, Update, Delete, Ignore, NoChange, Modify, or Deploy) before the deployment actually executes. It's Azure's built-in equivalent of a dry run, and it will show you, in the same output, that a template is about to flip a firewall rule open or replace a resource entirely rather than update it in place. It's underused for an ordinary reason: it's opt-in per invocation, catches drift and intent problems rather than insecure-default problems, and says nothing about whether the properties in the template were secure to begin with — a template that never sets minimumTlsVersion will what-if cleanly every time, because leaving a property at its default isn't a "change" what-if is built to flag. What-if answers "is this template about to do something I didn't expect"; it doesn't answer "did this template's author omit a security property," which is the gap static configuration scanning is built to close.
How does static scanning close the gap that what-if leaves open?
Static scanning closes the gap by evaluating the template's declared properties against a policy baseline before any deployment call is made, catching the class of finding that "what changed" tooling structurally can't see. Checkov's ARM template module ships maintained CKV_AZURE_* checks that map directly to the patterns above — public blob container access, storage accounts without minimumTlsVersion enforcement, SQL server firewall rules that allow 0.0.0.0/0, and resources deployed without diagnostic logging enabled — and runs against the raw JSON (or compiled Bicep output) with no Azure credentials and no deployment required. That's the practical advantage over what-if: a scanner can run in a pull request against a template nobody has attempted to deploy yet, block the merge on a CKV_AZURE finding, and give the author the exact property and value to add, days before the template ever reaches an az deployment call. Treating ARM/Bicep templates as source code subject to the same pre-merge scanning as application code — rather than infrastructure that only gets reviewed after it's already running — is what turns "insecure default" from a production incident into a code review comment.
How Safeguard helps
Insecure ARM defaults are fundamentally a leaked-secret and misconfiguration problem playing out in infrastructure-as-code instead of application code, and Safeguard's existing secrets scanning already covers the credential half of that equation: it scans source code, Git history, and CI logs for hardcoded values — including credentials that end up inlined in ARM parameters instead of referenced through Key Vault — and verifies live findings against the issuing service before raising an alert, then runs a revoke-and-rotate playbook automatically for anything confirmed live. Because that scanning runs continuously across a connected repository rather than as a one-time check, a secureString-shaped secret that gets committed as a plain string parameter today is caught on the same commit, not after it's deployed and sitting in Azure's deployment history. Pairing that credential coverage with template-level policy checks in your pull request pipeline — Checkov or an equivalent ARM/Bicep-aware scanner enforcing TLS, public-access, and firewall baselines — gives you both halves of the insecure-defaults problem covered before az deployment group create ever runs.