Module 1 – Design and Implement Authentication and Authorization
Identity Interactions in DevOps
Three types of interactions to consider:
- Human: developer, admin → authenticate + authorize in the project/repo.
- Automation: Azure DevOps pipelines, GitHub Actions → have their own identity.
- External services: pipelines interact with Azure, ACR, AKS, etc. via credentials.
Personal Access Tokens (PAT)
| Property | Description |
|---|---|
| Why use a PAT? | Some tools don’t support Entra ID. Need for a limited scope. |
| Limited scope | Subset of the user’s permissions |
| Expiration | Time-boxed (e.g. 14, 30 days) |
| Revocable | Can be revoked at any time |
| Separate | Azure DevOps PAT ≠ GitHub PAT |
| Recommended alternative | Service 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
| Group | Permissions |
|---|---|
| Project Administrators | Full admin on the project |
| Build Administrators | Manage build pipelines |
| Contributors | Contribute code, create work items |
| Readers | Read-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
| Type | Description | Recommendation |
|---|---|---|
| User account + password | ❌ Never use | Forbidden |
| Service Principal + secret | Application identity in Entra | OK |
| Managed Identity | SP without a secret to manage, auto by Azure | Preferred if agent hosted in Azure |
| Workload Identity Federation (OIDC) | Temporary token via OpenID Connect | Recommended 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
| Practice | Description |
|---|---|
| No secrets in code | Never hardcode credentials in YAML/script files |
| Use Managed Identity | Avoids having secrets to access Azure resources |
| RBAC Data Plane | Avoids SAS tokens and connection strings |
| Mandatory encryption | Secrets always encrypted at rest and in transit |
| Principle of least privilege | Minimize who has access to each secret |
| Avoid logs | Verify that secrets don’t appear in logs |
| Regular rotation | Secret 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)
| Feature | Description |
|---|---|
| Dependency Graph | Visualizes dependencies in use and their versions |
| Dependabot Alerts | Alerts when a dependency has a known vulnerability. Proposes updates. |
Paid features (or free for public repos)
| Feature | Description |
|---|---|
| Code Scanning (CodeQL) | Creates a queryable database of the code, detects hundreds of vulnerabilities (MITRE CWE) |
| Secret Scanning | Detects credentials, tokens, API keys accidentally committed |
| Security Advisories | Private 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
| Challenge | Description |
|---|---|
| Quality | Potentially low, bugs, reliability issues |
| Security | Known vulnerabilities in packages |
| Licenses | GPL, MIT, Apache — compatibility and legal obligations |
| Maintenance | Abandoned project → no security patches |
Module 4 – Exam Review Questions
Key Points for the Exam
| Question type | Answer |
|---|---|
| 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
| Term | Definition |
|---|---|
| DAST | Dynamic Application Security Testing — black-box security testing on a deployed app |
| Dependabot | GitHub service that monitors dependencies and proposes secure updates |
| Dependency Graph | Visualization of a project’s transitive dependencies |
| Federated Credential | Trust relationship between an external identity provider and Azure Entra ID |
| GHAS | GitHub Advanced Security — suite of GitHub security features |
| HSM | Hardware Security Module — physical device for securing cryptographic keys |
| IaC Security Scanning | Analysis of Bicep/Terraform/ARM templates to detect misconfigurations |
| Managed Identity | Azure service principal without credentials to manage, tied to an Azure resource |
| MITRE CWE | Common Weakness Enumeration — standardized catalog of software security weaknesses |
| OIDC | OpenID Connect — authentication protocol based on OAuth 2.0 with JWT tokens |
| OSS | Open Source Software — open source software with potential vulnerabilities/licenses |
| PAT | Personal Access Token — temporary, scope-limited authentication token |
| SAST | Static Application Security Testing — source code analysis without execution |
| Secure Files | Azure DevOps feature for storing encrypted sensitive files |
| Secret Scanning | Automatic detection of credentials/tokens accidentally committed to Git |
| Shift Left | Moving quality/security controls to the earliest phases of development |
| Workload Identity | Identity 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