Why Your Azure Portal Clicks Will Fail the Next Audit
In the cloud era, infrastructure configuration has become code review material. Yet I’ve watched teams spend months preparing for ISO 27017 audits, frantically documenting Azure resources that were manually configured through the portal six months ago. When the auditor asks, “Show me the change control process for this storage account’s network rules,” the response is often an uncomfortable silence followed by scrambling through Azure Activity Logs.
ISO/IEC 27017:2015 provides cloud-specific security controls extending ISO 27002. Control CLD 6.3.1 (Shared roles and responsibilities for cloud services) demands clear documentation of who changed what, when, and why. Manual configuration through the Azure Portal fundamentally violates this requirement. There’s no code review, no approval process, no version history explaining the reasoning behind configuration decisions.
Infrastructure as Code (IaC) using Azure Bicep isn’t just a DevOps best practice—it’s the compliance foundation that makes ISO 27017 audits survivable. When your infrastructure is code, your Git history becomes your audit trail, your pull requests become your change control process, and your CI/CD pipeline becomes your enforcement mechanism.
The Fatal Pattern: Click-Ops Configuration Management
Let me show you what non-compliance looks like in practice. This isn’t a code example—it’s a process anti-pattern I’ve encountered in dozens of organizations:
Day 1: DevOps engineer creates Azure SQL Database through the portal. Clicks “Review + Create” without documenting firewall rules or network isolation decisions.
Day 30: Security team requests network lockdown. Same engineer adds VNet integration and private endpoints through the portal. No documentation of which IP ranges need access or why.
Day 60: Application breaks in production. Emergency change: someone adds a firewall rule allowing all Azure services. Incident ticket mentions “temporary fix” but the rule stays forever.
Day 180: Audit preparation begins. Team discovers:
- Seven different people modified the database configuration
- No documentation exists explaining current network rules
- Activity Logs show changes but not the reasoning
- Configuration drift exists between dev, staging, and production
- No evidence of security review or approval process
- Cannot recreate current state reliably
This is what ISO 27017 auditors flag as non-compliant:
Audit Finding: CLD 6.3.1 Violation
Severity: High
Evidence of manual configuration changes to production Azure SQL Database
without documented change control process. Network security rules modified
by multiple individuals without approval workflow. Unable to demonstrate
separation of duties or security review process. Configuration baseline
cannot be established for ongoing compliance monitoring.
Recommendation: Implement Infrastructure as Code with version control,
code review, and automated deployment pipeline.
The fundamental problem isn’t the tools—it’s the lack of process enforcement. The Azure Portal is a GUI that bypasses every control you’d apply to application code: no peer review, no automated testing, no approval gates, no rollback capability, no audit trail beyond basic activity logs.
Manual Configuration Violates Multiple ISO Controls
Beyond CLD 6.3.1, click-ops infrastructure management violates:
A.12.1.2 (Change Management): “Changes to the organization, business processes, information processing facilities and systems that affect information security shall be controlled.”
Manual portal changes are uncontrolled. There’s no systematic process ensuring security requirements are evaluated before infrastructure modifications.
A.14.2.2 (System Change Control Procedures): “Changes to systems within the development lifecycle shall be controlled by the use of formal change control procedures.”
Clicking buttons in the Azure Portal doesn’t constitute formal change control. There’s no structured approval workflow, no testing in lower environments with identical infrastructure definitions, no rollback procedure beyond manual reversal.
A.18.2.3 (Technical Compliance Review): “Information systems shall be regularly reviewed for compliance with the organization’s information security policies and standards.”
How do you review compliance when your infrastructure exists as portal clicks rather than declarative code? You can’t diff two portal sessions. You can’t run policy checks against mouse movements.
The moment you accept manual infrastructure changes, you’ve conceded that infrastructure security is less important than application security. No one would allow developers to deploy application code without peer review—why accept the same for infrastructure that controls network access, encryption, and identity management?
The Compliant Pattern: Infrastructure as Auditable Code
Enough about what’s broken. Let’s look at what works.
Infrastructure as Code with Azure Bicep transforms compliance from documentation theater into automated enforcement. Here’s what a compliant Azure SQL Database deployment looks like—notice how every security decision becomes self-documenting:
// main.bicep - Compliant SQL Database configuration
@allowed(['dev', 'staging', 'production'])
param environment string
param location string = resourceGroup().location
@secure()
param sqlAdminPassword string
var sqlServerName = 'sql-${environment}-${uniqueString(resourceGroup().id)}'
resource sqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = {
name: sqlServerName
location: location
properties: {
administratorLogin: 'sqladmin'
administratorLoginPassword: sqlAdminPassword
// Security baseline: No public access in production
publicNetworkAccess: environment == 'production' ? 'Disabled' : 'Enabled'
minimalTlsVersion: '1.2'
// Entra-only authentication enforced
administrators: {
administratorType: 'ActiveDirectory'
azureADOnlyAuthentication: true
// ... Entra ID configuration
}
}
resource auditingSettings 'auditingSettings' = {
name: 'default'
properties: {
state: 'Enabled'
retentionDays: 90
// Audit authentication attempts and queries
}
}
}
// Private endpoint for production - no portal clicks allowed
module privateEndpoint 'modules/private-endpoint.bicep' = if (environment == 'production') {
name: 'sqlPrivateEndpoint'
params: {
privateLinkServiceId: sqlServer.id
// ... network configuration
}
}
This template embeds compliance directly into code. The Git history for this file becomes your audit trail—every commit message explains why something changed, every pull request documents who approved it.
Modular Bicep Structure for Reusability
But one template isn’t enough. Real compliance requires consistent patterns across your entire Azure estate.
Bicep modules let you define security patterns once and reuse them everywhere:
// modules/private-endpoint.bicep - Reusable security pattern
param privateEndpointName string
param privateLinkServiceId string
param subnetId string
param location string
param groupIds array
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-06-01' = {
name: privateEndpointName
location: location
properties: {
subnet: { id: subnetId }
privateLinkServiceConnections: [{
name: privateEndpointName
properties: {
privateLinkServiceId: privateLinkServiceId
groupIds: groupIds
}
}]
}
}
// Private DNS integration - name resolution without public exposure
resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-06-01' = {
parent: privateEndpoint
name: 'default'
properties: {
privateDnsZoneConfigs: [{
name: 'sqlConfig'
properties: {
privateDnsZoneId: '/subscriptions/.../privatelink.database.windows.net'
}
}]
}
}
When your security team updates this pattern—say, adding a new DNS configuration requirement—every resource using this module inherits the improvement automatically. One change, organization-wide compliance.
Policy-as-Code: Automated Compliance Enforcement
Modules enforce patterns for teams that use them. But what about teams that don’t?
Azure Policy catches non-compliant configurations before they reach production:
// policy/sql-security-baseline.bicep
targetScope = 'subscription'
// Enforce TLS 1.2 minimum - no exceptions
resource policyTlsVersion 'Microsoft.Authorization/policyDefinitions@2021-06-01' = {
name: 'enforce-sql-tls-12'
properties: {
displayName: 'SQL Servers must use TLS 1.2 or higher'
policyType: 'Custom'
mode: 'All'
policyRule: {
if: {
allOf: [
{ field: 'type', equals: 'Microsoft.Sql/servers' }
{ field: 'Microsoft.Sql/servers/minimalTlsVersion', notEquals: '1.2' }
]
}
then: { effect: 'Deny' }
}
}
}
// Deny public network access in production
resource policyPublicAccess 'Microsoft.Authorization/policyDefinitions@2021-06-01' = {
name: 'deny-sql-public-network-production'
properties: {
displayName: 'Production SQL Servers must disable public network access'
policyType: 'Custom'
mode: 'All'
policyRule: {
if: {
allOf: [
{ field: 'type', equals: 'Microsoft.Sql/servers' }
{ field: 'tags[environment]', equals: 'production' }
{ field: 'Microsoft.Sql/servers/publicNetworkAccess', notEquals: 'Disabled' }
]
}
then: { effect: 'Deny' }
}
}
}
// Assign policies to subscription
resource policyAssignmentTls 'Microsoft.Authorization/policyAssignments@2022-06-01' = {
name: 'sql-tls-baseline'
properties: {
displayName: 'SQL TLS Version Baseline'
policyDefinitionId: policyTlsVersion.id
enforcementMode: 'Default'
}
}
These policies act as guardrails. Non-compliant Bicep templates fail validation before any Azure API calls are made. Your security baseline becomes enforceable code, not a PDF checklist gathering dust.
GitHub Actions Workflow: Deployment with Compliance Gates
With templates and policies in place, the deployment pipeline ties everything together:
# .github/workflows/deploy-infrastructure.yml
name: Deploy Infrastructure
on:
pull_request:
paths: ['infrastructure/**']
push:
branches: [main]
paths: ['infrastructure/**']
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
validate:
name: Validate Bicep Templates
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Bicep Lint & Build
run: az bicep build --file infrastructure/main.bicep
- name: What-If Analysis
run: |
az deployment group what-if \
--resource-group rg-production \
--template-file infrastructure/main.bicep \
--parameters @infrastructure/parameters.production.json
- name: Policy Compliance Check
run: |
az deployment group validate \
--resource-group rg-production \
--template-file infrastructure/main.bicep \
--parameters @infrastructure/parameters.production.json
deploy:
name: Deploy to Azure
needs: validate
if: github.ref == 'refs/heads/main'
environment: production
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy Bicep Template
run: |
az deployment group create \
--resource-group rg-production \
--template-file infrastructure/main.bicep \
--parameters @infrastructure/parameters.production.json
- name: Archive Deployment Evidence
uses: actions/upload-artifact@v4
with:
name: deployment-evidence
path: deployment-output.json
retention-days: 2555 # 7 years for compliance
drift-detection:
name: Configuration Drift Detection
needs: deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Detect Configuration Drift
run: |
az deployment group what-if \
--resource-group rg-production \
--template-file infrastructure/main.bicep \
--parameters @infrastructure/parameters.production.json \
--mode Complete > drift-check.txt
if grep -q "Resource changes:" drift-check.txt; then
echo "::error::Configuration drift detected!"
exit 1
fi
This workflow enforces change control automatically. Every infrastructure change requires code review before merge. What-if analysis shows exactly what will change. Policy compliance verifies security baselines. And deployment artifacts get retained for 7 years—because auditors love documentation they can actually find.
Drift Detection as Continuous Compliance Monitoring
Here’s the uncomfortable truth: even with IaC in place, someone will eventually click something in the portal. “Just this once” to fix a production issue.
Configuration drift detection catches these changes before they become audit findings:
// modules/drift-detection.bicep - Azure Function for periodic compliance checks
param functionAppName string
param location string = resourceGroup().location
param repositoryUrl string
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: '${functionAppName}-plan'
location: location
sku: { name: 'Y1', tier: 'Dynamic' }
}
resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
name: functionAppName
location: location
kind: 'functionapp'
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
appSettings: [
{ name: 'FUNCTIONS_WORKER_RUNTIME', value: 'powershell' }
{ name: 'REPOSITORY_URL', value: repositoryUrl }
{ name: 'RESOURCE_GROUP', value: resourceGroup().name }
]
}
}
}
The Azure Function runs hourly, comparing deployed resources against your Bicep definitions:
# DriftDetection/run.ps1
param($Timer)
$ErrorActionPreference = 'Stop'
$tempDir = Join-Path $env:TEMP "infra-$(Get-Date -Format 'yyyyMMddHHmmss')"
# Clone infrastructure repo and run what-if analysis
git clone --depth 1 $env:REPOSITORY_URL $tempDir
$whatIfResult = az deployment group what-if `
--resource-group $env:RESOURCE_GROUP `
--template-file "$tempDir/infrastructure/main.bicep" `
--parameters "@$tempDir/infrastructure/parameters.production.json" `
--mode Complete --output json | ConvertFrom-Json
# Check for unauthorized changes
$driftChanges = $whatIfResult.changes | Where-Object {
$_.changeType -in @('Modify', 'Delete', 'Create')
}
if ($driftChanges) {
Write-Warning "Configuration drift detected!"
# Alert security team via webhook
$incident = @{
title = "Drift Detected in $($env:RESOURCE_GROUP)"
severity = "High"
changes = $driftChanges | ConvertTo-Json -Depth 5
}
Invoke-RestMethod -Uri $env:INCIDENT_WEBHOOK_URL -Method Post `
-Body ($incident | ConvertTo-Json) -ContentType 'application/json'
throw "Manual changes found in Azure infrastructure."
}
Write-Host "No drift detected. Infrastructure matches Git."
Remove-Item $tempDir -Recurse -Force
With hourly drift detection, manual portal changes get caught within hours, not months. The security team receives immediate notification, and the audit trail documents when drift was detected and how it was remediated.
The Compliance Transformation
Infrastructure as Code with Azure Bicep transforms ISO 27017 compliance from documentation burden to automated enforcement:
Before (Click-Ops):
- Configuration exists as portal clicks, not code
- No version history or change rationale
- Manual documentation prone to errors and drift
- Audit preparation requires months of retroactive investigation
- Non-compliance discovered during audit
After (IaC with Bicep):
- Infrastructure defined as declarative code with inline compliance documentation
- Git history provides complete audit trail
- Pull requests enforce code review and approval workflow
- Azure Policy prevents non-compliant deployments
- Drift detection catches unauthorized changes automatically
- Compliance demonstrated continuously, not just during audits
The cost savings are measurable. One organization I worked with reduced ISO 27017 audit preparation from 3 months to 2 weeks by implementing Bicep-based IaC. Their auditor’s report literally stated: “Version-controlled infrastructure templates with inline compliance documentation exceed ISO 27017 requirements for change control.”
More importantly, compliance became continuous rather than periodic. Security teams shifted from reactive documentation to proactive policy enforcement. When a new ISO control requirement emerged, they updated Bicep modules and Azure Policies once—every resource using those modules inherited compliance automatically.
Practical Implementation Recommendations
If you’re starting IaC implementation for ISO 27017 compliance:
Start with new resources: Don’t try to Bicep-ify your entire Azure estate overnight. New deployments go through IaC from day one.
Incrementally import existing resources: Use
az bicep decompileto generate Bicep from existing ARM templates or portal-created resources. Treat the output as a starting point, not production-ready code.Establish modules early: Create reusable Bicep modules for common patterns (private endpoints, managed identities, diagnostic settings). Consistency across resources simplifies compliance demonstration.
Implement policy-as-code in parallel: Azure Policy enforcement prevents regression to manual configuration. Deploy policies to audit mode first, then switch to deny mode after teams adapt.
Automate drift detection from the beginning: Manual changes will happen during the transition period. Automated detection ensures they’re caught and documented.
Retain deployment artifacts: Store deployment outputs, what-if results, and approval records for the retention period required by your compliance framework (typically 7 years for ISO).
Train teams on compliance rationale: Developers need to understand why IaC matters for compliance, not just how to write Bicep. When they understand the audit implications, they become compliance advocates.
Infrastructure as Code isn’t optional for compliance in cloud environments. Manual configuration through the Azure Portal fundamentally cannot satisfy change control requirements. The moment you accept click-ops, you’ve accepted non-compliance.
Bicep makes compliance enforceable rather than aspirational. Your Git history becomes your audit trail. Your pull request reviews become your change approval process. Your CI/CD pipeline becomes your enforcement mechanism.
The question isn’t whether to implement IaC for compliance—it’s whether you implement it proactively or after an audit finding forces your hand.

Comments