Back to Blog
5 min read

Zero Trust Security in Azure: Trust Nothing, Verify Everything

Zero Trust security became the dominant security paradigm in 2021. The principle is simple: never trust, always verify. Let’s explore how to implement Zero Trust on Azure.

Zero Trust Principles

  1. Verify Explicitly: Always authenticate and authorize based on all available data points
  2. Least Privilege Access: Limit user access with just-in-time and just-enough-access
  3. Assume Breach: Minimize blast radius and segment access

Identity-Based Perimeter

Moving from network-based to identity-based security:

// Managed Identity for workloads
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2021-09-30-preview' = {
  name: 'app-identity'
  location: resourceGroup().location
}

// Key Vault with RBAC
resource keyVault 'Microsoft.KeyVault/vaults@2021-10-01' = {
  name: 'app-keyvault'
  location: resourceGroup().location
  properties: {
    tenantId: subscription().tenantId
    sku: {
      family: 'A'
      name: 'standard'
    }
    enableRbacAuthorization: true
    enablePurgeProtection: true
    enableSoftDelete: true
    networkAcls: {
      defaultAction: 'Deny'
      bypass: 'AzureServices'
      virtualNetworkRules: [
        {
          id: subnet.id
        }
      ]
    }
  }
}

// Grant identity access to secrets
resource secretsOfficer 'Microsoft.Authorization/roleAssignments@2021-04-01-preview' = {
  scope: keyVault
  name: guid(keyVault.id, managedIdentity.id, 'secrets-officer')
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets Officer
    principalId: managedIdentity.properties.principalId
    principalType: 'ServicePrincipal'
  }
}

Conditional Access Policies

# Create Conditional Access Policy via Microsoft Graph
$policy = @{
    displayName = "Require MFA for Azure Management"
    state = "enabled"
    conditions = @{
        users = @{
            includeUsers = @("All")
            excludeUsers = @()
            includeGroups = @()
            excludeGroups = @("$breakGlassGroupId")
        }
        applications = @{
            includeApplications = @("797f4846-ba00-4fd7-ba43-dac1f8f63013") # Azure Management
        }
        locations = @{
            includeLocations = @("All")
            excludeLocations = @("$trustedLocationId")
        }
        clientAppTypes = @("all")
        signInRiskLevels = @()
        userRiskLevels = @()
    }
    grantControls = @{
        operator = "AND"
        builtInControls = @("mfa", "compliantDevice")
    }
    sessionControls = @{
        signInFrequency = @{
            value = 4
            type = "hours"
            isEnabled = $true
        }
    }
}

Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" -Body $policy

Network Micro-Segmentation

// Private Endpoints for all PaaS services
resource storagePrivateEndpoint 'Microsoft.Network/privateEndpoints@2021-05-01' = {
  name: 'storage-pe'
  location: resourceGroup().location
  properties: {
    subnet: {
      id: privateEndpointSubnet.id
    }
    privateLinkServiceConnections: [
      {
        name: 'storage-pls'
        properties: {
          privateLinkServiceId: storageAccount.id
          groupIds: ['blob']
        }
      }
    ]
  }
}

resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
  name: 'privatelink.blob.core.windows.net'
  location: 'global'
}

resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
  parent: privateDnsZone
  name: 'vnet-link'
  location: 'global'
  properties: {
    registrationEnabled: false
    virtualNetwork: {
      id: vnet.id
    }
  }
}

// Network Security Group with deny-all default
resource nsg 'Microsoft.Network/networkSecurityGroups@2021-05-01' = {
  name: 'app-nsg'
  location: resourceGroup().location
  properties: {
    securityRules: [
      {
        name: 'DenyAllInbound'
        properties: {
          priority: 4096
          direction: 'Inbound'
          access: 'Deny'
          protocol: '*'
          sourceAddressPrefix: '*'
          destinationAddressPrefix: '*'
          sourcePortRange: '*'
          destinationPortRange: '*'
        }
      }
      {
        name: 'AllowAppGatewayInbound'
        properties: {
          priority: 100
          direction: 'Inbound'
          access: 'Allow'
          protocol: 'Tcp'
          sourceAddressPrefix: 'GatewayManager'
          destinationAddressPrefix: '*'
          sourcePortRange: '*'
          destinationPortRange: '443'
        }
      }
    ]
  }
}

Workload Identity Federation

// Using workload identity in Kubernetes
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

public class SecretService
{
    private readonly SecretClient _secretClient;

    public SecretService()
    {
        // Uses workload identity when running in AKS
        var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
        {
            // Exclude unnecessary credential types for faster auth
            ExcludeEnvironmentCredential = true,
            ExcludeSharedTokenCacheCredential = true,
            ExcludeVisualStudioCredential = true,
            ExcludeVisualStudioCodeCredential = true,
            ExcludeAzureCliCredential = true,
            ExcludeInteractiveBrowserCredential = true,
            // Keep these for local dev and AKS
            ManagedIdentityClientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID")
        });

        var vaultUri = new Uri(Environment.GetEnvironmentVariable("KEY_VAULT_URI"));
        _secretClient = new SecretClient(vaultUri, credential);
    }

    public async Task<string> GetSecretAsync(string secretName)
    {
        var secret = await _secretClient.GetSecretAsync(secretName);
        return secret.Value.Value;
    }
}
# AKS workload identity configuration
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-identity
  namespace: production
  annotations:
    azure.workload.identity/client-id: "<managed-identity-client-id>"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
  namespace: production
spec:
  template:
    metadata:
      labels:
        azure.workload.identity/use: "true"
    spec:
      serviceAccountName: app-identity
      containers:
        - name: api
          image: myacr.azurecr.io/api:latest
          env:
            - name: KEY_VAULT_URI
              value: "https://app-keyvault.vault.azure.net/"
            - name: AZURE_CLIENT_ID
              valueFrom:
                fieldRef:
                  fieldPath: metadata.annotations['azure.workload.identity/client-id']

Just-In-Time Access

# Request JIT access to VM
from azure.identity import DefaultAzureCredential
from azure.mgmt.security import SecurityCenter
from datetime import datetime, timedelta

def request_jit_access(
    subscription_id: str,
    resource_group: str,
    vm_name: str,
    requestor_email: str
):
    credential = DefaultAzureCredential()
    client = SecurityCenter(credential, subscription_id, "")

    # Define JIT request
    jit_request = {
        "virtualMachines": [
            {
                "id": f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.Compute/virtualMachines/{vm_name}",
                "ports": [
                    {
                        "number": 22,
                        "allowedSourceAddressPrefix": "Internet",
                        "endTimeUtc": (datetime.utcnow() + timedelta(hours=3)).isoformat()
                    }
                ]
            }
        ],
        "justification": "Troubleshooting production issue"
    }

    # Submit request
    result = client.jit_network_access_policies.initiate(
        resource_group_name=resource_group,
        jit_network_access_policy_name="default",
        jit_network_access_policy_initiate_type=jit_request
    )

    return result

# Azure Policy for JIT enforcement
jit_policy = {
    "mode": "All",
    "policyRule": {
        "if": {
            "allOf": [
                {
                    "field": "type",
                    "equals": "Microsoft.Compute/virtualMachines"
                },
                {
                    "field": "Microsoft.Compute/virtualMachines/storageProfile.osDisk.osType",
                    "equals": "Linux"
                }
            ]
        },
        "then": {
            "effect": "deployIfNotExists",
            "details": {
                "type": "Microsoft.Security/jitNetworkAccessPolicies",
                "existenceCondition": {
                    "field": "Microsoft.Security/jitNetworkAccessPolicies/virtualMachines[*].id",
                    "contains": "[field('id')]"
                }
            }
        }
    }
}

Continuous Verification

# Implement continuous access evaluation
from azure.identity import DefaultAzureCredential
from azure.monitor.query import LogsQueryClient
from datetime import datetime, timedelta

def monitor_anomalous_access():
    credential = DefaultAzureCredential()
    client = LogsQueryClient(credential)

    query = """
    SigninLogs
    | where TimeGenerated > ago(1h)
    | where ResultType != 0
    | summarize FailedAttempts = count() by UserPrincipalName, IPAddress, Location
    | where FailedAttempts > 5
    | project UserPrincipalName, IPAddress, Location, FailedAttempts, RiskLevel = iff(FailedAttempts > 10, 'High', 'Medium')
    """

    result = client.query_workspace(
        workspace_id="your-workspace-id",
        query=query,
        timespan=timedelta(hours=1)
    )

    for row in result.tables[0].rows:
        if row['RiskLevel'] == 'High':
            # Trigger automated response
            revoke_user_sessions(row['UserPrincipalName'])
            send_security_alert(row)

def revoke_user_sessions(user_principal_name: str):
    """Revoke all active sessions for a user"""
    # Use Microsoft Graph to revoke sessions
    pass

Zero Trust Checklist

ControlStatusImplementation
Strong AuthenticationRequiredAzure AD + MFA
Device ComplianceRequiredIntune + Conditional Access
Least PrivilegeRequiredPIM + RBAC
Micro-SegmentationRequiredPrivate Endpoints + NSGs
EncryptionRequiredTLS 1.2+ + At-rest encryption
MonitoringRequiredAzure Sentinel + Defender
Automated ResponseRecommendedLogic Apps + Playbooks

Zero Trust in 2021 transitioned from concept to implementation. Azure provides the tools; the challenge is configuring them cohesively.

Resources

Michael John Pena

Michael John Pena

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.