Advanced AZ-400

AZ-400: Security and Compliance

Shift-left security, GitHub Advanced Security, Key Vault, OIDC workload identity and container scanning.

Module 1 – Design and Implement Authentication and Authorization

Identity Interactions in DevOps

Three types of interactions to consider:

  1. Human: developer, admin → authenticate + authorize in the project/repo.
  2. Automation: Azure DevOps pipelines, GitHub Actions → have their own identity.
  3. External services: pipelines interact with Azure, ACR, AKS, etc. via credentials.

Personal Access Tokens (PAT)

PropertyDescription
Why use a PAT?Some tools don’t support Entra ID. Need for a limited scope.
Limited scopeSubset of the user’s permissions
ExpirationTime-boxed (e.g. 14, 30 days)
RevocableCan be revoked at any time
SeparateAzure DevOps PAT ≠ GitHub PAT
Recommended alternativeService Principal or Managed Identity for long-running automation

GitHub Permissions and Roles

Personal accounts vs organizations

  • Personal account: Owner (full control) and Collaborator (read/write) roles.
  • Organization: many built-in roles. Enterprise GitHub = custom roles possible.

GitHub Hierarchy

Enterprise (GitHub Enterprise Cloud)
  └── Organization (the company, kept minimal)
        ├── Teams (for notifications and access management)
        └── Repositories (one per project/product)
  • Teams: can have permissions across multiple repos.
  • @mention only works between members of the same organization.

Azure DevOps Permissions and Security Groups

Entra ID Integration

  • Unlike GitHub, Azure DevOps is built on Entra ID.
  • Natively supports Entra users and Entra groups.
  • Best practice: assign roles to groups (not individual users).
    • Reason: users accumulate permissions → hard to maintain.
    • Groups: users added/removed automatically when role changes.

Azure DevOps Hierarchy

Azure DevOps Organization
  └── Project (logical grouping)
        ├── Git Repositories
        ├── Boards / Work Items
        ├── Build Pipelines
        └── Release Pipelines

Built-in ADO Groups

GroupPermissions
Project AdministratorsFull admin on the project
Build AdministratorsManage build pipelines
ContributorsContribute code, create work items
ReadersRead-only

Service Connections (Pipeline → Azure Resources)

Architecture

  • Pipeline/GitHub Actions needs an identity to deploy Azure resources.
  • Azure RBAC = roles assigned in a subscription or resource group.
  • The Azure subscription trusts a specific Entra tenant.

Identity Types for Pipelines

TypeDescriptionRecommendation
User account + password❌ Never useForbidden
Service Principal + secretApplication identity in EntraOK
Managed IdentitySP without a secret to manage, auto by AzurePreferred if agent hosted in Azure
Workload Identity Federation (OIDC)Temporary token via OpenID ConnectRecommended for GitHub Actions

Key rule: If the agent is in Azure → use Managed Identity. Otherwise → OIDC/Workload Identity Federation.


Module 2 – Managing Sensitive Information in Automation

Azure Key Vault

  • Purpose: secure container for secrets, keys, certificates.
  • Tiers:
    • Standard: software-based key storage.
    • Premium: HSM (Hardware Security Module) for keys.
  • Supported objects:
    • Secrets: confidential data (connection strings, passwords) — readable value.
    • Keys: cryptographic keys (crypto operations in the vault, value NOT exportable).
    • Certificates: SSL/TLS certificate lifecycle management.
  • Access Control: granular RBAC per individual object (modern) or vault-access-policy (legacy).

Secret Variables (Azure DevOps & GitHub)

Azure DevOps – Secret Variables

  • Encrypted with RSA 2048-bit.
  • Value masked in logs (replaced with ***).
  • Syntax: $(SecretName).
  • Variable Groups: share secrets between multiple pipelines.
    • Can be mapped to Azure Key Vault (automatic value pull).

GitHub Secrets

  • Secure storage in repo or org Settings.
  • Accessible from YAML workflows.
  • ⚠️ GitHub does NOT have “Secure Files” (unlike Azure DevOps).

Azure DevOps – Secure Files

  • Encrypted file storage up to 10 MB.
  • Accessible only from pipeline tasks (no manual download).
  • Protected resource = checks + approvals possible.

Best Practices – Preventing Leaks

PracticeDescription
No secrets in codeNever hardcode credentials in YAML/script files
Use Managed IdentityAvoids having secrets to access Azure resources
RBAC Data PlaneAvoids SAS tokens and connection strings
Mandatory encryptionSecrets always encrypted at rest and in transit
Principle of least privilegeMinimize who has access to each secret
Avoid logsVerify that secrets don’t appear in logs
Regular rotationSecret rotation, ideally automated

Module 3 – Automating Security and Compliance Scanning

Shift Left Security

  • Concept: moving security controls toward the initial phases of the development cycle.
  • The earlier a problem is found → the easier and less costly to fix.
  • Cycle phases: Code → Build → QA → Security → Deploy → Production.
  • Goal: detect at the developer level, not in production.

GitHub Advanced Security (GHAS)

Features available for ALL repos (public + private)

FeatureDescription
Dependency GraphVisualizes dependencies in use and their versions
Dependabot AlertsAlerts when a dependency has a known vulnerability. Proposes updates.
FeatureDescription
Code Scanning (CodeQL)Creates a queryable database of the code, detects hundreds of vulnerabilities (MITRE CWE)
Secret ScanningDetects credentials, tokens, API keys accidentally committed
Security AdvisoriesPrivate discussion on vulnerabilities before disclosure

CodeQL in Detail

  • Transforms code into queryable data.
  • Dedicated language: CodeQL (domain-specific language).
  • Integrated into PRs and CI/CD workflows.
  • Predefined queries for thousands of vulnerabilities.
  • Custom queries possible.

Microsoft Defender for Cloud DevOps Security

  • Centralized console for Azure DevOps, GitHub, GitLab.
  • Analysis:
    • Code findings (vulnerabilities in source code).
    • Secret scanning.
    • OSS dependency vulnerabilities.
    • IaC misconfigurations (ARM, Bicep, Terraform).
  • Cloud Security Explorer: query engine with pre-built templates.
  • Cloud Security Posture Management (CSPM): compares configuration against best practices.

Container Scanning

Microsoft Defender for Containers supports:

  • Azure: AKS, Azure Container Registry (ACR).
  • AWS: EKS, ECR.
  • GCP: GKE, Google Container Registry.
  • Image scanning at push time and continuously.

Open Source Software (OSS) – Challenges

ChallengeDescription
QualityPotentially low, bugs, reliability issues
SecurityKnown vulnerabilities in packages
LicensesGPL, MIT, Apache — compatibility and legal obligations
MaintenanceAbandoned project → no security patches

Module 4 – Exam Review Questions

Key Points for the Exam

Question typeAnswer
Self-hosted agent in Azure → authentication?Managed Identity (not service principal, not user)
Scope of a Team in GitHub?At the organization level (access to multiple repos)
Key Vault object to store a readable value?Secret (not Key = non-exportable)
GitHub does NOT have what?Secure Files (ADO has Secure Files, GitHub does not)
Free GHAS features for private repos?Dependency Graph + Dependabot Alerts only
Defender for Containers does NOT support?Everything is supported (Azure AKS+ACR, AWS EKS+ECR, GCP GKE)
Avoid secrets in pipelines → modern solution?Workload Identity Federation (OIDC) / Managed Identity
Should a PAT contain all permissions?No → limited scope, limited duration, revocable

Module 5 — Shift-Left Security: Complete Architecture

DevSecOps Pipeline — Security at Every Stage

flowchart LR
    subgraph "Developer Workstation (Inner Loop)"
        IDE[IDE / VS Code] --> LINT[Linter + Local SAST\nRoslyn Analyzers]
        LINT --> COMMIT[git commit]
    end

    subgraph "Pull Request"
        COMMIT -->|push| PR[Pull Request]
        PR --> CODE_REVIEW[Code Review\nManual]
        PR --> STATIC[Static Analysis\nSonarQube / CodeQL]
        PR --> SECRET_SCAN[Secret Scanning\nGHAS Secret Scanner]
        PR --> DEP_REVIEW[Dependency Review\nDependabot / Snyk]
    end

    subgraph "CI Build Pipeline"
        PR -->|merge| BUILD[Build]
        BUILD --> UNIT_TEST[Unit Tests\n+ Security Unit Tests]
        BUILD --> OSS_SCAN[OSS Vulnerability Scan\nWhiteSource / Snyk]
        BUILD --> CONTAINER_SCAN[Container Image Scan\nTrivy / Qualys]
        BUILD --> IaC_SCAN[IaC Security Scan\nCheckov / Terrascan]
    end

    subgraph "CD Deployment Pipeline"
        BUILD -->|artifact| DEPLOY_TEST[Deploy to Test]
        DEPLOY_TEST --> DAST[DAST\nOWASP ZAP]
        DEPLOY_TEST --> PENTEST[Pen Test\nManual / Automated]
        DEPLOY_TEST --> AZURE_POLICY[Azure Policy Gate\nInfra Compliance]
    end

    subgraph "Production"
        AZURE_POLICY -->|gate passed| PROD[Deploy to Production]
        PROD --> MONITOR[Azure Security Center\nMicrosoft Defender]
        MONITOR -->|alert| INCIDENT[Incident Response]
    end

Cost of Fixing a Bug by Phase

flowchart LR
    DEV["💰 1x\nDevelopment Phase"]
    CODE_REVIEW["💰💰 6x\nCode Review"]
    QA["💰💰💰 10x\nQA/Test Phase"]
    STAGING["💰💰💰💰 25x\nStaging/UAT"]
    PROD["💰💰💰💰💰💰 100x\nProduction"]
    DEV --> CODE_REVIEW --> QA --> STAGING --> PROD

Module 6 — GitHub Advanced Security (GHAS): Complete Guide

CodeQL Activation and Configuration

# .github/workflows/codeql-analysis.yml
name: CodeQL Security Analysis

on:
  push:
    branches: [main, release/*]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 0 * * 1'   # Every Monday at midnight

jobs:
  analyze:
    name: Analyze Code
    runs-on: ubuntu-latest
    
    permissions:
      actions: read
      contents: read
      security-events: write   # REQUIRED to publish results

    strategy:
      fail-fast: false
      matrix:
        language: ['csharp', 'javascript']
        # Supported languages: csharp, cpp, go, java, javascript, python, ruby, swift

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v2
        with:
          languages: ${{ matrix.language }}
          # Use extended security queries
          queries: +security-and-quality   # Add security-extended for more detection
          config-file: ./.github/codeql-config.yml

      - name: Build application
        run: dotnet build --configuration Release

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v2
        with:
          category: "/language:${{ matrix.language }}"
          # upload: true   # Upload SARIF results to GitHub Security tab
# .github/codeql-config.yml - Advanced CodeQL Configuration
name: "Custom CodeQL Config"
queries:
  - uses: security-and-quality
  - uses: security-extended
  - name: Custom injection vulnerability queries
    uses: ./.github/codeql-queries/injection-checks.ql

# Exclude test paths from scan
paths-ignore:
  - '**/*Test*.cs'
  - '**/test/**'
  - '**/node_modules/**'

# Include only source code
paths:
  - 'src/**'
  - 'lib/**'

Secret Scanning — Configuration

# Enable Secret Scanning via the GitHub API
# (normally enabled from Settings → Security → Code security and analysis)

# Configure custom patterns to detect your own secret formats
# Settings → Code security → Secret scanning → Custom patterns

# Example custom pattern: internal API Key format MYAPP-XXXXXXXX-XXXX
# Pattern: MYAPP-[A-Z0-9]{8}-[A-Z0-9]{4}

Dependabot — Automatic Dependency Updates

# .github/dependabot.yml
version: 2
updates:
  # NuGet (.NET)
  - package-ecosystem: "nuget"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "06:00"
    target-branch: "main"
    open-pull-requests-limit: 5
    reviewers:
      - "@security-team"
    labels:
      - "dependencies"
      - "security"
    # Group updates to reduce the number of PRs
    groups:
      azure-sdk:
        patterns:
          - "Azure.*"
        update-types:
          - "minor"
          - "patch"

  # npm (frontend)
  - package-ecosystem: "npm"
    directory: "/frontend"
    schedule:
      interval: "daily"
    allow:
      - dependency-type: "production"
    ignore:
      - dependency-name: "lodash"
        update-types: ["version-update:semver-major"]

  # Docker images
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"

  # GitHub Actions
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Dependency Review for Pull Requests

# .github/workflows/dependency-review.yml
name: Dependency Review

on: pull_request

permissions:
  contents: read
  pull-requests: write

jobs:
  dependency-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Dependency Review
        uses: actions/dependency-review-action@v4
        with:
          # Block new dependencies with vulnerabilities >= moderate
          fail-on-severity: moderate
          
          # List forbidden licenses (problematic copyleft for proprietary code)
          deny-licenses: >-
            GPL-1.0-only, GPL-1.0-or-later, GPL-2.0-only, GPL-2.0-or-later,
            GPL-3.0-only, GPL-3.0-or-later, AGPL-1.0-only, AGPL-3.0-only
          
          # Explicitly allow permissive licenses
          allow-licenses: >-
            MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, 0BSD
          
          # Add a summary comment in the PR
          comment-summary-in-pr: always
          
          # Alert if new dependencies are added (no vulnerability)
          warn-only: false

Module 7 — Azure Key Vault: Secure Implementation

Key Vault Architecture in a .NET Application

flowchart TD
    subgraph "Azure App Service Application"
        APP[ASP.NET Core App]
        IDENTITY[System-assigned\nManaged Identity]
        APP --> IDENTITY
    end

    subgraph "Azure Key Vault"
        SECRETS[Secrets\nConnectionStrings\nAPIKeys]
        KEYS[Keys\nEncryption Keys\nSigning Keys]
        CERTS[Certificates\nSSL/TLS]
    end

    subgraph "Azure RBAC"
        ROLE["Key Vault Secrets User\n(role assignment: identity → vault)"]
    end

    IDENTITY -->|"Azure AD Token"| ROLE
    ROLE -->|"Authorized read access"| SECRETS
    APP -->|"Read secret value"| SECRETS

Configuring Azure Key Vault in ASP.NET Core

// Program.cs - Load Key Vault secrets at startup
var builder = WebApplication.CreateBuilder(args);

// Method 1: Managed Identity (recommended for App Service)
if (!builder.Environment.IsDevelopment())
{
    var keyVaultUri = new Uri(builder.Configuration["KeyVaultUri"]!);
    builder.Configuration.AddAzureKeyVault(
        keyVaultUri,
        new DefaultAzureCredential());
}

// Method 2: Service Principal (for local dev/test)
// DefaultAzureCredential tries in order:
// 1. Environment variables (AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID)
// 2. Workload Identity (AKS)
// 3. Managed Identity
// 4. Azure CLI credentials
// 5. Visual Studio credentials
// 6. Azure PowerShell credentials
builder.Configuration.AddAzureKeyVault(
    keyVaultUri,
    new DefaultAzureCredential(new DefaultAzureCredentialOptions
    {
        ExcludeEnvironmentCredential = false,
        ExcludeManagedIdentityCredential = false,
        ExcludeAzureCliCredential = false
    }));

// Access in services
public class OrderService
{
    private readonly string _connectionString;
    private readonly string _paymentApiKey;

    public OrderService(IConfiguration configuration)
    {
        // Key Vault secrets are accessible like any other config
        // Naming: "--" in Key Vault names → ":" in configuration
        _connectionString = configuration["Database--ConnectionString"]!;
        _paymentApiKey = configuration["Payments--ApiKey"]!;
    }
}

Creating and Managing Key Vault via Azure CLI

# Create a Key Vault
az keyvault create \
  --name "myapp-prod-kv" \
  --resource-group "myapp-rg" \
  --location "eastus" \
  --enable-rbac-authorization true \     # RBAC mode (recommended)
  --enable-soft-delete true \
  --soft-delete-retention-days 90 \
  --enable-purge-protection true         # Prevents permanent deletion

# Store a secret
az keyvault secret set \
  --vault-name "myapp-prod-kv" \
  --name "Database--ConnectionString" \
  --value "Server=prod-sql.database.windows.net;Database=myapp;..." \
  --expires "2025-12-31T23:59:59Z"     # Automatic expiration

# Assign the "Key Vault Secrets User" role to a Managed Identity
APP_IDENTITY=$(az webapp identity show \
  --name "myapp-prod" \
  --resource-group "myapp-rg" \
  --query principalId -o tsv)

az role assignment create \
  --role "Key Vault Secrets User" \
  --assignee $APP_IDENTITY \
  --scope "/subscriptions/{sub}/resourceGroups/myapp-rg/providers/Microsoft.KeyVault/vaults/myapp-prod-kv"

# List and audit secrets
az keyvault secret list --vault-name "myapp-prod-kv" \
  --query "[].{Name:name,Enabled:attributes.enabled,Expires:attributes.expires}" \
  --output table

# View access logs
az monitor activity-log list \
  --resource-id "/subscriptions/{sub}/resourceGroups/myapp-rg/providers/Microsoft.KeyVault/vaults/myapp-prod-kv" \
  --start-time "2024-01-01" \
  --query "[].{caller:caller,operation:operationName.localizedValue,time:eventTimestamp}"

Key Vault in Azure Pipelines

# Load Key Vault secrets in an Azure DevOps pipeline
steps:
  - task: AzureKeyVault@2
    displayName: 'Load Secrets from Key Vault'
    inputs:
      azureSubscription: 'AzureServiceConnection'
      KeyVaultName: 'myapp-prod-kv'
      SecretsFilter: 'Database--ConnectionString,Payments--ApiKey,SendGrid--ApiKey'
      RunAsPreJob: false    # true = available for all jobs

  # Secrets are now available as pipeline variables
  - task: DotNetCoreCLI@2
    displayName: 'Run Integration Tests'
    env:
      # Map the secret as an environment variable
      ConnectionStrings__DefaultConnection: $(Database--ConnectionString)
      PaymentApiKey: $(Payments--ApiKey)
    inputs:
      command: 'test'
      projects: '**/*.IntegrationTests.csproj'
# Variable Group linked to Azure Key Vault
# Create in Azure DevOps > Pipelines > Library > + Variable group
# Name: Production-Secrets
# Link secrets from Azure Key Vault: ON
# Azure subscription: AzureServiceConnection
# Key Vault Name: myapp-prod-kv

# Use in the pipeline:
variables:
  - group: Production-Secrets  # Contains Key Vault secrets

steps:
  - script: |
      echo "Deploying with secure credentials"
      # $(Database--ConnectionString) is loaded from Key Vault
    env:
      DB_CONN: $(Database--ConnectionString)   # Masked in logs

Automatic Secret Rotation

# Configure automatic rotation with Azure Automation
az automation runbook create \
  --resource-group "automation-rg" \
  --automation-account-name "myapp-automation" \
  --name "RotateKeyVaultSecrets" \
  --type "PowerShell"
# Automatic Key Vault secret rotation runbook
param(
    [string]$VaultName = "myapp-prod-kv",
    [string]$SecretName = "DatabasePassword"
)

# Authentication via Automation Account Managed Identity
Connect-AzAccount -Identity

# Generate a new secure password
Add-Type -AssemblyName System.Web
$newPassword = [System.Web.Security.Membership]::GeneratePassword(32, 8)

# Update the database
$connectionString = Get-AzKeyVaultSecret -VaultName $VaultName -Name "Database--ConnectionString" -AsPlainText
# ... code to change the password in Azure SQL ...
Invoke-Sqlcmd -Query "ALTER LOGIN appuser WITH PASSWORD = '$newPassword'" -ConnectionString $connectionString

# Update Key Vault with the new secret
$securePassword = ConvertTo-SecureString -String $newPassword -AsPlainText -Force
Set-AzKeyVaultSecret -VaultName $VaultName -Name $SecretName -SecretValue $securePassword

Write-Host "✅ Secret '$SecretName' in '$VaultName' has been successfully rotated"

# Update the full connection string
$newConnString = "Server=prod-sql.database.windows.net;Database=myapp;User=appuser;Password=$newPassword;"
$secureConnString = ConvertTo-SecureString -String $newConnString -AsPlainText -Force
Set-AzKeyVaultSecret -VaultName $VaultName -Name "Database--ConnectionString" -SecretValue $secureConnString

Module 8 — Workload Identity Federation (OIDC)

Why OIDC Instead of Service Principal with Secret?

flowchart TD
    subgraph "Traditional Approach"
        SP[Service Principal] --> SECRET[Client Secret\nstored in GitHub Secrets]
        SECRET --> LEAK[Risk of leakage\nif repo is compromised]
    end

    subgraph "Workload Identity Federation (OIDC)"
        GHA[GitHub Actions Runner] --> TOKEN[Temporary JWT Token\ngenerated by GitHub OIDC Provider]
        TOKEN --> AZURE_AD[Azure Entra ID\nValidates the token]
        AZURE_AD --> CRED[Temporary Access Token\nValid 1 hour, no secret stored]
    end

OIDC Advantages:

  • No secret to store, manage, or rotate.
  • Temporary tokens (automatic expiration).
  • Full traceability: Azure knows exactly which GitHub workflow uses which identity.

OIDC Configuration Azure → GitHub Actions

# 1. Create an App Registration (or use Managed Identity)
APP_ID=$(az ad app create --display-name "GitHub-OIDC-MyRepo" --query appId -o tsv)
az ad sp create --id $APP_ID

TENANT_ID=$(az account show --query tenantId -o tsv)
SUBSCRIPTION_ID=$(az account show --query id -o tsv)

# 2. Assign a role to the App Registration
az role assignment create \
  --role "Contributor" \
  --assignee $APP_ID \
  --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/myapp-rg"

# 3. Configure the Federated Identity Credential (trust toward GitHub)
# For the main branch
az ad app federated-credential create \
  --id $APP_ID \
  --parameters "{
    \"name\": \"github-main\",
    \"issuer\": \"https://token.actions.githubusercontent.com\",
    \"subject\": \"repo:myorg/myrepo:ref:refs/heads/main\",
    \"audiences\": [\"api://AzureADTokenExchange\"]
  }"

# For environments (Production)
az ad app federated-credential create \
  --id $APP_ID \
  --parameters "{
    \"name\": \"github-env-production\",
    \"issuer\": \"https://token.actions.githubusercontent.com\",
    \"subject\": \"repo:myorg/myrepo:environment:production\",
    \"audiences\": [\"api://AzureADTokenExchange\"]
  }"

# 4. Add to GitHub Secrets (Settings → Secrets → Actions)
# AZURE_CLIENT_ID = $APP_ID
# AZURE_TENANT_ID = $TENANT_ID
# AZURE_SUBSCRIPTION_ID = $SUBSCRIPTION_ID
# .github/workflows/deploy.yml - OIDC Usage
permissions:
  id-token: write    # REQUIRED for OIDC
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Azure Login (OIDC - No secrets!)
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          # No client_secret! OIDC = zero stored secrets

      - name: Deploy to Azure
        run: |
          az webapp deploy \
            --resource-group myapp-rg \
            --name myapp-prod \
            --src-path ./dist/app.zip

Module 9 — Container Scanning

Microsoft Defender for Containers

flowchart TD
    subgraph "Cloud Providers"
        AZURE[Azure AKS + ACR]
        AWS[AWS EKS + ECR]
        GCP[GCP GKE + GCR]
    end

    subgraph "Microsoft Defender for Containers"
        direction LR
        RUNTIME[Runtime Protection\nK8s Anomaly Detection]
        IMAGE_SCAN[Image Scanning\nOSS Vulnerabilities + CVEs]
        POSTURE[Security Posture\nK8s Misconfigurations]
        NETWORK[Network Threat Detection]
    end

    AZURE & AWS & GCP --> RUNTIME & IMAGE_SCAN & POSTURE & NETWORK

    IMAGE_SCAN -->|"Alerts"| DEFENDER[Microsoft Defender for Cloud\nCentralized Security]
    RUNTIME -->|"Alerts"| DEFENDER

Trivy — Container Scanning in CI/CD

# Integrate Trivy into GitHub Actions
- name: Build Docker Image
  run: docker build -t myapp:${{ github.sha }} .

- name: Scan with Trivy
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'myapp:${{ github.sha }}'
    format: 'sarif'
    output: 'trivy-results.sarif'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'          # Fail the job if critical/high vulnerabilities
    vuln-type: 'os,library'
    ignore-unfixed: true    # Ignore CVEs without available patch

- name: Upload Results to GitHub Security
  uses: github/codeql-action/upload-sarif@v2
  if: always()   # Upload even if Trivy failed
  with:
    sarif_file: 'trivy-results.sarif'

- name: Push to ACR (if no vulnerabilities)
  if: success()
  run: |
    az acr login --name myregistry
    docker tag myapp:${{ github.sha }} myregistry.azurecr.io/myapp:${{ github.sha }}
    docker push myregistry.azurecr.io/myapp:${{ github.sha }}

Container Signing with Notation and Azure Container Registry

# Sign a Docker image with Notation (CNCF)
# Install Notation
brew install notation   # macOS
# or download from https://github.com/notaryproject/notation

# Sign the image in ACR with Azure Key Vault
notation sign \
  --plugin azure-kv \
  --id "https://myapp-prod-kv.vault.azure.net/certificates/signing-cert/version123" \
  "myregistry.azurecr.io/myapp:1.2.3"

# Verify the signature
notation verify "myregistry.azurecr.io/myapp:1.2.3"

# In AKS, enable mandatory signature validation
az aks update \
  --resource-group myapp-rg \
  --name myapp-aks \
  --enable-image-integrity \
  --enable-oidc-issuer

Module 10 — Compliance and Azure Policy

Security Policy in a CI/CD Pipeline

# Azure Policy gate before production deployment
- task: AzurePowerShell@5
  displayName: 'Verify Azure Policy compliance'
  inputs:
    azureSubscription: 'AzureServiceConnection'
    ScriptType: 'InlineScript'
    Inline: |
      # Trigger a compliance evaluation
      Start-AzPolicyComplianceScan -ResourceGroupName "myapp-prod-rg"
      
      # Wait for evaluation to finish (may take a few minutes)
      Start-Sleep -Seconds 60
      
      # Check for non-compliant resources
      $nonCompliant = Get-AzPolicyState `
        -ResourceGroupName "myapp-prod-rg" `
        -Filter "complianceState eq 'NonCompliant'" |
        Where-Object { $_.policyDefinitionEffect -in @("deny", "audit") }
      
      if ($nonCompliant.Count -gt 0) {
        Write-Error "❌ $($nonCompliant.Count) non-compliant resource(s)!"
        $nonCompliant | Select-Object PolicyDefinitionName, ResourceId, ComplianceState |
          Format-Table -AutoSize
        exit 1
      }
      
      Write-Host "✅ All resources are compliant with Azure policies"
    azurePowerShellVersion: 'LatestVersion'

Checkov — IaC Security Scanner

# Integrate Checkov to scan Bicep/Terraform/ARM templates
- name: Run Checkov IaC Security Scan
  uses: bridgecrewio/checkov-action@master
  with:
    directory: infra/
    framework: bicep,terraform
    output_format: sarif
    output_file_path: checkov-results.sarif
    soft_fail: false      # fail the pipeline if issues found
    check: "CKV_AZURE_*"  # Azure-specific checks
    skip_check: "CKV_AZURE_99"  # Skip a specific check if false positive

- name: Upload Checkov Results
  uses: github/codeql-action/upload-sarif@v2
  if: always()
  with:
    sarif_file: checkov-results.sarif

Review Questions — Security and Compliance

Q1 — Managed Identity vs Service Principal

Question: Your Azure DevOps pipeline uses a self-hosted agent deployed on an Azure VM. The pipeline must deploy resources in an Azure Resource Group. Which authentication method do you use?

  • A. Service Principal with client secret stored in an Azure DevOps secret Variable
  • B. Service Principal with certificate stored in Azure Key Vault
  • C. Managed Identity assigned to the agent VM
  • D. OIDC Workload Identity Federation

Explanation: When the agent is hosted on an Azure VM, the best practice is to use a Managed Identity assigned to the VM. Azure automatically manages the credential lifecycle — no secret to store, manage, or rotate. Managed Identity is a special form of Service Principal without a secret. OIDC is ideal for GitHub Actions but Azure Pipelines uses Managed Identities instead.


Q2 — Free GHAS Features

Question: Your organization uses GitHub with private repositories without a GitHub Advanced Security license. Which security features are available for free?

  • A. CodeQL code scanning + Dependabot Alerts
  • B. Secret Scanning + Dependency Graph
  • C. Dependency Graph + Dependabot Alerts
  • D. All GHAS features are free

Explanation: For private repositories without a GHAS license, only Dependency Graph and Dependabot Alerts are available for free. CodeQL, Secret Scanning, and Security Advisories require a GHAS license (included for public repos, paid for private).


Q3 — Azure Key Vault Object Types

Question: Your application needs to sign JWT tokens with an RSA key. This key must remain in Azure Key Vault and never leave it. Which Key Vault object type do you use?

  • A. Secret (with the private key value in Base64)
  • B. Certificate (self-signed)
  • C. Key (RSA 2048-bit, non-exportable)
  • D. Secret (with the PEM content of the key)

Explanation: A Key Vault Key is a cryptographic key that resides in the vault and is never exported. Cryptographic operations (signing, decryption) are performed directly in the vault via the API. A Secret is readable and exportable — inappropriate for a private key. The Certificate manages SSL/TLS certificates, not general RSA keys.


Q4 — Shift Left Security

Question: Your team discovers security vulnerabilities (OWASP Top 10) mainly during penetration testing before each release, causing significant delays. What approach do you recommend to reduce these delays?

  • A. Hire more pentesters to speed up testing
  • B. Move penetration testing to the end of the sprint
  • C. Implement Shift Left: CodeQL, SAST, and dependency scanning from Pull Requests
  • D. Fully automate penetration testing

Explanation: Shift Left Security consists of moving security controls as early as possible in the development cycle. Integrating CodeQL (SAST), dependency scanning, and secret scanning into Pull Requests detects the majority of security issues when they are cheapest to fix (x1), rather than at release end (x100). Penetration testing remains necessary but finds far fewer problems.


Q5 — Secure Files vs GitHub Secrets

Question: You need to store an Apple code signing certificate (.p12) of 8 MB for your iOS builds in your CI/CD pipeline. You are using Azure DevOps. Which feature do you use?

  • A. Azure Key Vault Certificate
  • B. Azure DevOps Variable Group with secret value
  • C. GitHub Secret (base64 encoded)
  • D. Azure DevOps Secure Files (Library)

Explanation: Azure DevOps Secure Files allow storing files up to 10 MB in an encrypted form. They are accessible only from pipeline tasks (no manual download). This is exactly what’s needed for certificates, Android keystores, iOS provisioning profiles. GitHub has no equivalent to Secure Files.


Q6 — Container Scanning

Question: You want to automatically scan Docker images in your Azure Container Registry for CVEs, WITHOUT having to modify your CI/CD pipelines. Which service do you use?

  • A. GitHub Advanced Security with CodeQL
  • B. Trivy in GitHub Actions
  • C. Microsoft Defender for Containers with scanning at ACR registration
  • D. Azure Policy with image scanning

Explanation: Microsoft Defender for Containers offers continuous and automatic scanning of images in Azure Container Registry upon push, without pipeline modifications. It uses Qualys to detect CVEs in OS packages and dependencies. It alerts via Microsoft Defender for Cloud without manual intervention.


Q7 — Workload Identity Federation

Question: Your security team requires that GitHub Actions workflows never have access to long-lived secrets to connect to Azure. How do you implement this?

  • A. Store the client_secret in GitHub Secrets and rotate it monthly
  • B. Use a Service Principal with certificate stored in Azure Key Vault
  • C. Use an Azure user account with MFA
  • D. Configure Workload Identity Federation (OIDC) between GitHub and Azure Entra ID

Explanation: Workload Identity Federation (OIDC) allows GitHub Actions to obtain a temporary Azure token without storing ANY long-lived secret. GitHub generates a signed JWT for each workflow run, Azure Entra ID validates it and issues a temporary access token (~1 hour). No secret is stored in GitHub Secrets — only non-secret identifiers (tenant ID, client ID, subscription ID).


Glossary — Security and Compliance AZ-400

TermDefinition
DASTDynamic Application Security Testing — black-box security testing on a deployed app
DependabotGitHub service that monitors dependencies and proposes secure updates
Dependency GraphVisualization of a project’s transitive dependencies
Federated CredentialTrust relationship between an external identity provider and Azure Entra ID
GHASGitHub Advanced Security — suite of GitHub security features
HSMHardware Security Module — physical device for securing cryptographic keys
IaC Security ScanningAnalysis of Bicep/Terraform/ARM templates to detect misconfigurations
Managed IdentityAzure service principal without credentials to manage, tied to an Azure resource
MITRE CWECommon Weakness Enumeration — standardized catalog of software security weaknesses
OIDCOpenID Connect — authentication protocol based on OAuth 2.0 with JWT tokens
OSSOpen Source Software — open source software with potential vulnerabilities/licenses
PATPersonal Access Token — temporary, scope-limited authentication token
SASTStatic Application Security Testing — source code analysis without execution
Secure FilesAzure DevOps feature for storing encrypted sensitive files
Secret ScanningAutomatic detection of credentials/tokens accidentally committed to Git
Shift LeftMoving quality/security controls to the earliest phases of development
Workload IdentityIdentity assigned to a workload (pipeline, container) for secretless authentication

Appendix — Security Templates and Scripts

Secure Bicep Template (Best Practices)

// secure-webapp.bicep - Template with all security best practices

@description('Environment')
@allowed(['dev', 'test', 'prod'])
param environment string

@description('Application name')
param appName string

param location string = resourceGroup().location

var isProd = environment == 'prod'

// App Service Plan
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
  name: '${appName}-${environment}-asp'
  location: location
  sku: {
    name: isProd ? 'P1v3' : 'B1'
  }
  kind: 'linux'
  properties: {
    reserved: true
  }
}

// App Service with hardened security
resource webApp 'Microsoft.Web/sites@2023-01-01' = {
  name: '${appName}-${environment}'
  location: location
  identity: {
    type: 'SystemAssigned'   // Managed Identity for Key Vault
  }
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true           // Mandatory HTTPS
    siteConfig: {
      minTlsVersion: '1.2'    // Minimum TLS 1.2
      ftpsState: 'Disabled'   // Disable FTP
      http20Enabled: true     // HTTP/2
      alwaysOn: isProd
      
      // Security headers
      ipSecurityRestrictions: isProd ? [
        {
          action: 'Deny'
          priority: 1
          name: 'Deny all by default'
          description: 'Default deny rule'
        }
      ] : []
      
      appSettings: [
        {
          name: 'ASPNETCORE_ENVIRONMENT'
          value: isProd ? 'Production' : 'Development'
        }
        {
          name: 'KeyVaultUri'
          value: 'https://${appName}-${environment}-kv.vault.azure.net/'
        }
      ]
    }
  }
}

// Key Vault with RBAC
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
  name: '${appName}-${environment}-kv'
  location: location
  properties: {
    sku: {
      family: 'A'
      name: isProd ? 'premium' : 'standard'  // HSM for prod
    }
    tenantId: subscription().tenantId
    enableRbacAuthorization: true   // Modern RBAC (vs legacy access policies)
    enableSoftDelete: true
    softDeleteRetentionInDays: isProd ? 90 : 7
    enablePurgeProtection: isProd   // Only in prod
    networkAcls: isProd ? {
      defaultAction: 'Deny'
      bypass: 'AzureServices'
      virtualNetworkRules: []
      ipRules: []
    } : {
      defaultAction: 'Allow'
    }
  }
}

// Grant App Service access to Key Vault
resource kvRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(keyVault.id, webApp.id, 'KeyVaultSecretsUser')
  scope: keyVault
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')
    principalId: webApp.identity.principalId
    principalType: 'ServicePrincipal'
  }
}

// Checkov ignore: CKV_AZURE_13 - Storage for function app not needed here
// outputs
output appUrl string = 'https://${webApp.properties.defaultHostName}'
output keyVaultUri string = keyVault.properties.vaultUri
output appIdentityPrincipalId string = webApp.identity.principalId

Checkov - IaC Security Policies

# Install Checkov
pip install checkov

# Scan a Bicep template
checkov -d ./infra \
  --framework bicep \
  --output sarif \
  --output-file-path checkov-results.sarif

# Scan with custom policies
checkov -d ./infra \
  --external-checks-dir ./security-policies \
  --framework bicep \
  --check CKV_AZURE_13,CKV_AZURE_16,CKV_AZURE_17,CKV_AZURE_35

Search Terms

az-400 · security · compliance · azure · devops · iac · microsoft · github · vault · scanning · container · identity · secret · secure · architecture · configuration · features · ghas · oidc · pipeline · service · automatic · checkov · ci/cd

Interested in this course?

Contact us to book it or get a custom training plan for your team.