Back to Software Engineering & Delivery Mastery Series Azure DevOps Bootcamp

Module 10: Security, Governance & Compliance

June 3, 2026 Wasil Zafar 42 min read

Master Azure DevOps security and governance — the permissions model, RBAC, service principals and managed identities, Azure Key Vault integration, secret management, secure pipeline patterns, audit logging, compliance frameworks, and DevSecOps practices for enterprise environments.

Table of Contents

  1. Identity & Access
  2. Secrets & Key Vault
  3. Compliance & DevSecOps
  4. Practice

Security in Azure DevOps

Security in DevOps isn't about choosing between speed and safety — it's about achieving both simultaneously. A well-secured Azure DevOps environment moves faster because developers trust the guardrails, automated checks catch issues before they reach production, and audit trails mean you can prove compliance without slowing down releases.

Analogy: Security in Azure DevOps is like building security — permissions are keycards, groups are access zones, audit logs are CCTV cameras, and Key Vault is the safe room. You need all layers working together. A keycard alone won't stop someone from tailgating, and a camera won't lock doors — layered defense is the only approach that works.

The Threat Model

Before configuring security, understand what you're defending against:

Threat Example Primary Defense
Insider risk Disgruntled developer deletes production branch Branch policies, RBAC, audit logs
Compromised credentials Leaked PAT token grants repo access Managed identities, short-lived tokens, conditional access
Supply chain attacks Malicious dependency injected via upstream package SCA scanning, upstream source pinning, signed packages
Misconfiguration Service connection with Owner-level permissions Least privilege, regular access reviews, policy enforcement
Pipeline abuse Fork submits PR that exfiltrates secrets via modified YAML Protected resources, extends templates, fork restrictions

For supply chain security concepts in depth, see Part 23: Supply Chain Security.

Permissions Model

Azure DevOps uses a hierarchical permissions model where settings at higher levels cascade down to lower levels unless explicitly overridden. Understanding this hierarchy is essential for maintaining least-privilege access.

Permission Inheritance Chain

Permission Inheritance Hierarchy
flowchart TD
    A[Organization Level] --> B[Project Level]
    B --> C[Repository]
    B --> D[Pipelines]
    B --> E[Boards]
    B --> F[Artifacts]
    C --> G[Branch Policies]
    D --> H[Environments]
    D --> I[Service Connections]
    D --> J[Agent Pools]

    style A fill:#132440,color:#fff
    style B fill:#16476A,color:#fff
    style C fill:#3B9797,color:#fff
    style D fill:#3B9797,color:#fff
    style E fill:#3B9797,color:#fff
    style F fill:#3B9797,color:#fff
                            

Permission States

Each permission can be in one of four states — and the interaction between them determines what a user can actually do:

State Meaning Precedence
Allow Explicitly grants the permission Overridden by explicit Deny
Deny Explicitly blocks the permission Always wins (highest precedence)
Not set Inherits from parent group/level Effectively Deny if nothing grants it
Allow (inherited) Granted by a parent group membership Can be overridden by explicit Deny
Critical rule: Deny always wins. If a user is in Group A (Allow: Delete repo) and Group B (Deny: Delete repo), the result is Deny. This catches people off guard — you can't "add" permissions to overcome a Deny. You must remove the Deny.

Checking Effective Permissions

To see what a user can actually do (after all inheritance and group memberships resolve):

  1. Navigate to Project Settings → Permissions
  2. Search for the user or group
  3. Click the Member of tab to see all group memberships
  4. Click the Permissions tab to see effective permissions with inheritance source

Principle of Least Privilege in Practice

  • Start with Reader — grant additional permissions only when needed
  • Use groups, not individual assignments — easier to audit and revoke
  • Scope service connections to specific resource groups — not entire subscriptions
  • Time-bound elevated access — use PIM (Privileged Identity Management) for admin roles
  • Separate production from development — different service connections, different approval gates

Groups & RBAC

Role-Based Access Control (RBAC) in Azure DevOps is implemented through security groups. Rather than assigning permissions to individual users, you assign users to groups — and groups carry the permissions. This makes access management scalable.

Built-in Groups

Group Typical Members Key Permissions
Project Administrators Tech leads, DevOps engineers Full control over project settings, pipelines, repos
Contributors Developers Push code, create branches, run pipelines, manage work items
Readers Stakeholders, auditors View-only access to all artifacts
Build Administrators CI/CD engineers Manage build pipelines, agent pools, service connections
Release Administrators Release managers Manage release pipelines, approve deployments
Project Valid Users Everyone with any access Basic project visibility (auto-assigned)

Custom Groups

Create custom security groups when built-in groups don't match your access patterns:

  • Security Reviewers — Can approve PRs to /infrastructure/ paths but can't push code
  • Database Admins — Access to database migration pipelines only
  • External Contractors — Contributor on specific repos, no access to production pipelines
  • Compliance Officers — Read access to audit logs, pipeline history, and test results

Entra ID (Azure AD) Group Synchronization

For organizations using Microsoft Entra ID, you can synchronize directory groups directly into Azure DevOps:

# Add an Entra ID group to an Azure DevOps project group
# This syncs membership automatically — when people join/leave the Entra group,
# their Azure DevOps access updates accordingly

az devops security group membership add \
  --group-id "vssgp.Uy0xLTktMTY4MTM3NjAzOC..." \
  --member-id "aadgp.Uy0xLTktMTY4MTM3NjAzOC..." \
  --org https://dev.azure.com/yourorg

# List members of a security group
az devops security group membership list \
  --id "vssgp.Uy0xLTktMTY4MTM3NjAzOC..." \
  --org https://dev.azure.com/yourorg \
  --output table

# Create a custom security group
az devops security group create \
  --name "Security Reviewers" \
  --description "Can approve PRs touching infrastructure code" \
  --project "MyProject" \
  --org https://dev.azure.com/yourorg

Teams vs Security Groups

A common source of confusion — Azure DevOps has both teams and security groups:

  • Teams — Organize work (boards, backlogs, iterations, dashboards). Every team is also a security group, but their primary purpose is work management.
  • Security groups — Control access (permissions to repos, pipelines, resources). Use these for access control decisions.
Best practice: Don't assign pipeline permissions to teams. Create dedicated security groups for access control. A team called "Platform Team" should have a corresponding security group called "Platform-Pipeline-Admins" for pipeline management permissions.

Service Principals & Managed Identities

When pipelines need to access Azure resources (deploy to AKS, read Key Vault secrets, push container images), they authenticate using non-human identities. There are three approaches, from least to most secure:

Identity Type Secret Management Best For
Service Principal + Secret You manage rotation (expires 1-2 years) Legacy setups, cross-tenant access
Managed Identity Azure manages credentials (no secrets to rotate) Self-hosted agents running on Azure VMs
Workload Identity Federation (OIDC) No secrets at all — uses short-lived tokens Microsoft-hosted agents, modern setups ✅

Workload Identity Federation — The Modern Approach

Workload Identity Federation eliminates secrets entirely. Instead of storing a client secret, Azure DevOps presents a short-lived OIDC token that Azure trusts via a federated credential configuration:

Workload Identity Federation Flow
sequenceDiagram
    participant Pipeline as Azure Pipeline
    participant ADO as Azure DevOps OIDC Provider
    participant AAD as Microsoft Entra ID
    participant Azure as Azure Resources

    Pipeline->>ADO: Request OIDC token
    ADO-->>Pipeline: Short-lived JWT (valid ~10 min)
    Pipeline->>AAD: Present JWT + federated credential
    AAD->>AAD: Validate issuer, subject, audience
    AAD-->>Pipeline: Access token for Azure
    Pipeline->>Azure: Deploy with access token
                            
# Create a service connection using Workload Identity Federation (OIDC)
# This is the recommended approach — no secrets to rotate!

# Step 1: Create an app registration in Entra ID
az ad app create --display-name "azdo-prod-deployer"

# Step 2: Create a federated credential (trusts Azure DevOps)
az ad app federated-credential create \
  --id <app-object-id> \
  --parameters '{
    "name": "ado-main-branch",
    "issuer": "https://vstoken.dev.azure.com/<tenant-id>",
    "subject": "sc://yourorg/yourproject/prod-azure-connection",
    "audiences": ["api://AzureADTokenExchange"]
  }'

# Step 3: Assign RBAC role to the app (scoped to resource group)
az role assignment create \
  --assignee <app-client-id> \
  --role "Contributor" \
  --scope "/subscriptions/<sub-id>/resourceGroups/production-rg"

# Step 4: In Azure DevOps, create service connection
# Project Settings → Service connections → New → Azure Resource Manager
# Choose "Workload Identity federation (automatic)" or "manual"
Never store service principal secrets in pipeline variables — use Workload Identity Federation (no secrets) or Azure Key Vault (centralized rotation). Hard-coded secrets are the #1 cause of credential leaks in CI/CD systems.

Azure Key Vault Integration

Azure Key Vault is a centralized store for secrets, encryption keys, and certificates. Integrating Key Vault with Azure DevOps means your pipelines can access secrets at runtime without storing them in Azure DevOps at all — they're fetched fresh, and Key Vault handles access logging, rotation, and expiry notifications.

What Key Vault Provides

  • Secrets — API keys, connection strings, passwords (with automatic version history)
  • Keys — Cryptographic keys for signing/encryption (backed by HSMs)
  • Certificates — TLS/SSL certificates with auto-renewal from CAs
  • Access policies — Fine-grained control over who can read/write/manage each type
  • Audit logging — Every access is logged to Azure Monitor

Linking Key Vault to Variable Groups

The simplest integration — a variable group backed by Key Vault secrets:

  1. Navigate to Pipelines → Library → + Variable group
  2. Toggle "Link secrets from an Azure key vault as variables"
  3. Select your Azure service connection and Key Vault
  4. Choose which secrets to map (each becomes a pipeline variable)
  5. Reference in YAML: $(my-secret-name)

AzureKeyVault@2 Task — Runtime Fetching

# Fetch secrets from Key Vault at pipeline runtime
# Secrets become pipeline variables for subsequent steps

stages:
  - stage: Deploy
    jobs:
      - job: DeployApp
        steps:
          # Fetch secrets from Key Vault
          - task: AzureKeyVault@2
            displayName: 'Fetch secrets from Key Vault'
            inputs:
              azureSubscription: 'prod-azure-connection'  # Service connection name
              KeyVaultName: 'myapp-prod-kv'
              SecretsFilter: 'DatabaseConnectionString,ApiKey,StorageAccountKey'
              # Each secret becomes a variable: $(DatabaseConnectionString), etc.
              RunAsPreJob: true  # Available to all steps in this job

          # Use the secrets (they're automatically masked in logs)
          - script: |
              echo "Deploying with database connection..."
              # $(DatabaseConnectionString) is available but masked in output
              dotnet publish --configuration Release
            displayName: 'Build application'

          - task: AzureCLI@2
            displayName: 'Deploy to App Service'
            inputs:
              azureSubscription: 'prod-azure-connection'
              scriptType: 'bash'
              scriptLocation: 'inlineScript'
              inlineScript: |
                az webapp config appsettings set \
                  --resource-group production-rg \
                  --name myapp-prod \
                  --settings \
                    "ConnectionStrings__Default=$(DatabaseConnectionString)" \
                    "ApiSettings__Key=$(ApiKey)"

Access Policies vs RBAC for Key Vault

Model Granularity Recommendation
Vault access policy Per-vault, per-principal (get, list, set, delete) Legacy — simpler but less flexible
Azure RBAC Per-secret granularity, Entra ID integration Recommended — consistent with Azure-wide RBAC ✅

Secret Rotation Strategies

  • Key Vault auto-rotation — Configured per-secret with Event Grid notifications
  • Near-expiry alerts — Azure Monitor alerts 30 days before expiry
  • Dual-key pattern — Rotate by activating a second key, then deactivating the old one (zero-downtime)
  • Pipeline-triggered rotation — Scheduled pipeline rotates secrets and updates dependent resources

Secure Pipeline Patterns

Pipelines are both your greatest productivity tool and your largest attack surface. A misconfigured pipeline can expose secrets, deploy malicious code, or destroy infrastructure. Azure DevOps provides multiple layers to lock pipelines down.

Protected Resources

Certain resources can be marked as protected, requiring explicit pipeline authorization before use:

  • Environments — Production environment requires approval + checks
  • Service connections — Production Azure connection restricted to specific pipelines
  • Agent pools — Self-hosted production agents only accessible by release pipelines
  • Variable groups — Key Vault-linked groups restricted to authorized pipelines
  • Repositories — Shared template repos with controlled access
Mark sensitive service connections and environments as "protected resources" — this requires pipeline administrators to explicitly grant access, preventing any random pipeline from accessing production credentials. When a new pipeline first references a protected resource, it pauses and requests authorization.

Extends Templates for Enforced Security

The most powerful security pattern — use extends templates to enforce that every pipeline runs required security checks, regardless of what developers put in their YAML:

# templates/secure-pipeline.yml (in a locked, shared repository)
# ALL project pipelines must extend this template

parameters:
  - name: stages
    type: stageList

stages:
  # Enforced security scanning BEFORE any custom stages
  - stage: SecurityGate
    displayName: '🔒 Security Scanning (enforced)'
    jobs:
      - job: SecurityChecks
        steps:
          - task: CredScan@3
            displayName: 'Credential scanning'
          - task: SdtReport@2
            displayName: 'Security report'
          - script: |
              echo "Running SAST analysis..."
              # CodeQL or SonarQube analysis
            displayName: 'Static analysis'

  # Developer-defined stages run AFTER security passes
  - ${{ each stage in parameters.stages }}:
    - ${{ stage }}

  # Enforced compliance gate AFTER all stages
  - stage: ComplianceCheck
    displayName: '📋 Compliance Verification'
    dependsOn: ${{ each stage in parameters.stages }}
    jobs:
      - job: Verify
        steps:
          - script: echo "Verifying deployment compliance..."
            displayName: 'Compliance attestation'
# azure-pipelines.yml (developer's pipeline)
# MUST extend the secure template — can't bypass security checks

trigger:
  - main

resources:
  repositories:
    - repository: templates
      type: git
      name: SharedProject/pipeline-templates
      ref: refs/heads/main

extends:
  template: templates/secure-pipeline.yml@templates
  parameters:
    stages:
      - stage: Build
        jobs:
          - job: BuildApp
            steps:
              - script: dotnet build
                displayName: 'Build application'

      - stage: Deploy
        jobs:
          - deployment: DeployProd
            environment: 'production'
            strategy:
              runOnce:
                deploy:
                  steps:
                    - script: echo "Deploying..."

Fork and PR Validation Security

When accepting contributions from forks (open source projects), be cautious:

  • Don't make secrets available to fork builds — A malicious PR could exfiltrate them
  • Require a maintainer comment before running pipelines on fork PRs
  • Use pr triggers with restrictions — Only run limited checks (no deployment, no secret access)
  • Limit what fork pipelines can modify — No access to protected resources

Secret Management Best Practices

Secrets are everywhere in pipelines — database passwords, API keys, deployment tokens, SSH keys. Managing them properly is critical because a leaked secret can compromise your entire infrastructure.

Secret Storage Options Comparison

Storage Method Rotation Audit Scope Best For
Pipeline variable (secret) Manual Limited Single pipeline Quick prototyping only
Variable group (secret) Manual Change history Multiple pipelines Shared non-critical secrets
Key Vault-linked variable group Automated Full audit trail Multiple pipelines Production secrets ✅
Secure files Manual upload Download logged Multiple pipelines Certificates, SSH keys, .env files
AzureKeyVault@2 task Automated Full audit trail Per-job Dynamic secret fetching ✅

Secret Hygiene Rules

  • Never echo secrets — Azure DevOps masks them, but env | sort in a script can still leak them
  • Use issecret=true for dynamic secretsecho "##vso[task.setvariable variable=myVar;issecret=true]$value"
  • Rotate regularly — Set calendar reminders or automate with Key Vault rotation policies
  • Minimize blast radius — Separate secrets per environment (dev-kv, staging-kv, prod-kv)
  • Detect leaks proactively — Enable GitHub Advanced Security secret scanning or use CredScan

Secure Files

For binary secrets that can't be stored as text (certificates, keystores, SSH keys):

# Using secure files in a pipeline
# Upload the file via Pipelines → Library → Secure files

steps:
  # Download the secure file to the agent
  - task: DownloadSecureFile@1
    name: sshKey
    displayName: 'Download SSH key'
    inputs:
      secureFile: 'deploy-key.pem'

  # Use the file (path available via the task output variable)
  - script: |
      # Install the SSH key with correct permissions
      mkdir -p ~/.ssh
      cp $(sshKey.secureFilePath) ~/.ssh/deploy-key.pem
      chmod 600 ~/.ssh/deploy-key.pem

      # Use it for deployment
      ssh -i ~/.ssh/deploy-key.pem user@server "cd /app && git pull"
    displayName: 'Deploy via SSH'

  # Clean up (the agent does this automatically, but be explicit)
  - script: rm -f ~/.ssh/deploy-key.pem
    displayName: 'Remove SSH key'
    condition: always()

Audit Logging & Monitoring

Audit logs record who did what, when, and from where across your Azure DevOps organization. They're essential for security investigations, compliance evidence, and detecting suspicious behavior.

What Gets Audited

  • Authentication events — Sign-ins, PAT creation/revocation, OAuth connections
  • Permission changes — Group membership modifications, role assignments
  • Pipeline events — Pipeline created/modified/deleted, runs, approvals
  • Repository actions — Branch policy changes, force pushes, deletions
  • Policy modifications — Branch policies, build validation rules, reviewer requirements
  • Service connection changes — Creation, modification, authorization grants

Audit Event Flow

Audit Log Pipeline
flowchart LR
    A[User Action] --> B[Azure DevOps
Audit Service] B --> C{Streaming
configured?} C -->|Yes| D[Azure Monitor
Log Analytics] C -->|Yes| E[Event Hubs] C -->|Yes| F[Splunk/SIEM] C -->|No| G[90-day retention
in Azure DevOps] D --> H[KQL Queries
& Alerts] E --> I[Real-time
Processing] style B fill:#16476A,color:#fff style D fill:#3B9797,color:#fff style H fill:#BF092F,color:#fff

Querying Audit Logs via REST API

# Query Azure DevOps audit logs for the last 24 hours
# Useful for security monitoring scripts and compliance dashboards

# Set variables
ORG="https://auditservice.dev.azure.com/yourorg"
TOKEN=$(echo -n ":$PAT" | base64)
START_TIME=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)
END_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# Fetch audit events
curl -s -H "Authorization: Basic $TOKEN" \
  "$ORG/_apis/audit/auditlog?startTime=$START_TIME&endTime=$END_TIME&api-version=7.1-preview.1" \
  | jq '.decoratedAuditLogEntries[] | {
    timestamp: .timestamp,
    actor: .actorDisplayName,
    action: .actionId,
    details: .details,
    ipAddress: .actorClientId
  }'

# Example output:
# {
#   "timestamp": "2026-06-03T14:22:00Z",
#   "actor": "jane.doe@company.com",
#   "action": "Security.ModifyPermission",
#   "details": "Added user to Project Administrators",
#   "ipAddress": "203.0.113.42"
# }

Streaming to Log Analytics

For retention beyond 90 days and advanced querying, stream audit logs to Azure Monitor:

  1. Navigate to Organization Settings → Auditing
  2. Click Configure stream → New stream → Azure Monitor
  3. Select your Log Analytics workspace
  4. Events appear in the AzureDevOpsAuditing table within minutes
# KQL query in Log Analytics: Detect permission escalation
# Alert on any user being added to administrative groups

AzureDevOpsAuditing
| where TimeGenerated > ago(1h)
| where OperationName == "Security.ModifyPermission"
| where Details contains "Project Administrators"
    or Details contains "Build Administrators"
    or Details contains "Collection Administrators"
| project TimeGenerated, ActorDisplayName, Details, IpAddress, ProjectName
| order by TimeGenerated desc
Create alerts for high-risk events: Permission escalation to admin groups, service connection creation, branch policy removal, and bulk repository deletions should all trigger immediate notifications to your security team.

Compliance Frameworks

Azure DevOps supports compliance with major frameworks by providing the tooling and evidence trails that auditors look for. The platform itself holds SOC 1/2/3, ISO 27001, and other certifications — but you're responsible for how you configure and use it.

Change Management Traceability

The most powerful compliance feature in Azure DevOps is automatic traceability — every deployment links back through a complete chain of evidence:

Compliance Traceability Chain
flowchart LR
    A[Work Item
Requirement] --> B[Branch &
Commits] B --> C[Pull Request
+ Reviewers] C --> D[Build &
Tests] D --> E[Approval
Gates] E --> F[Deployment
+ Logs] style A fill:#132440,color:#fff style C fill:#16476A,color:#fff style E fill:#BF092F,color:#fff style F fill:#3B9797,color:#fff
Azure DevOps provides automatic traceability from requirement to deployment — this is invaluable for compliance audits. Every deployment links back to the work item, PR, reviewer, and test results. Auditors can trace any production change back to who requested it, who reviewed it, what tests passed, and who approved the release.

Evidence Collection per Framework

Framework Key Requirement Azure DevOps Evidence
SOC 2 Change management controls Branch policies, PR reviews, approval gates, audit logs
ISO 27001 Access control (A.9) RBAC groups, permission inheritance, periodic access reviews
HIPAA Audit controls (§164.312) Audit log streaming, encryption in transit/at rest
FedRAMP Continuous monitoring (CA-7) Security scanning in pipelines, audit log retention, alerts
PCI DSS Secure development (Req 6) Code reviews, SAST/DAST, test evidence, deployment approval

Compliance Dashboards

Build dashboards that auditors can self-serve:

  • Deployment frequency — How often releases go to production (with approvals)
  • Change failure rate — Percentage of deployments rolled back
  • PR review coverage — Percentage of changes with required reviewers
  • Security scan pass rate — Percentage of builds passing SAST/SCA
  • Mean time to remediate — How quickly security vulnerabilities are fixed

DevSecOps in Pipelines

DevSecOps integrates security checks directly into the CI/CD pipeline — shifting security left so vulnerabilities are caught during development rather than after deployment. The goal: every commit gets scanned automatically, and high-severity findings block the release.

Security Scanning Categories

Category What It Detects Popular Tools
SAST (Static Analysis) Code-level vulnerabilities (SQL injection, XSS, buffer overflows) CodeQL, SonarQube, Checkmarx, Semgrep
SCA (Software Composition) Vulnerable dependencies, license violations Mend (WhiteSource), Snyk, Dependabot, OWASP Dependency-Check
DAST (Dynamic Analysis) Runtime vulnerabilities in running applications OWASP ZAP, Burp Suite, Nikto
Container Scanning Vulnerabilities in base images and layers Trivy, Aqua, Prisma Cloud, Grype
Secret Scanning Committed credentials, API keys, tokens CredScan, GitLeaks, TruffleHog, GitHub Advanced Security
IaC Scanning Misconfigured infrastructure templates Checkov, tfsec, KICS, Bridgecrew

Complete Security Scanning Pipeline

# Full DevSecOps pipeline with layered security scanning
# Blocks release on critical/high findings

trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

stages:
  # ─── STAGE 1: SECURITY SCANNING ─────────────────────────
  - stage: SecurityScan
    displayName: '🔒 Security Scanning'
    jobs:
      - job: SAST
        displayName: 'Static Analysis (SAST)'
        steps:
          - task: SonarQubePrepare@5
            inputs:
              SonarQube: 'sonarqube-connection'
              scannerMode: 'CLI'
              configMode: 'manual'
              cliProjectKey: 'myapp'
              cliSources: 'src/'

          - script: dotnet build src/MyApp.sln
            displayName: 'Build for analysis'

          - task: SonarQubeAnalyze@5
            displayName: 'Run SonarQube analysis'

          - task: SonarQubePublish@5
            displayName: 'Publish quality gate result'

      - job: SCA
        displayName: 'Dependency Scanning (SCA)'
        steps:
          - script: |
              # Install and run Snyk for dependency scanning
              npm install -g snyk
              snyk auth $SNYK_TOKEN
              snyk test --severity-threshold=high --json > snyk-results.json || true
              snyk monitor  # Upload results for dashboard tracking
            displayName: 'Snyk dependency scan'
            env:
              SNYK_TOKEN: $(SnykApiToken)

          - publish: snyk-results.json
            artifact: 'sca-results'
            condition: always()

      - job: SecretScan
        displayName: 'Secret Detection'
        steps:
          - script: |
              # Install and run gitleaks for secret detection
              wget -q https://github.com/gitleaks/gitleaks/releases/download/v8.18.0/gitleaks_8.18.0_linux_x64.tar.gz
              tar -xzf gitleaks_8.18.0_linux_x64.tar.gz
              ./gitleaks detect --source . --report-format json --report-path gitleaks-report.json
            displayName: 'Gitleaks secret scan'

      - job: ContainerScan
        displayName: 'Container Scanning'
        steps:
          - script: docker build -t myapp:$(Build.BuildId) .
            displayName: 'Build container image'

          - script: |
              # Trivy container vulnerability scan
              # Fail on CRITICAL or HIGH vulnerabilities
              wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
              echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list
              sudo apt-get update && sudo apt-get install -y trivy

              trivy image --exit-code 1 --severity CRITICAL,HIGH \
                --format json --output trivy-results.json \
                myapp:$(Build.BuildId)
            displayName: 'Trivy vulnerability scan'

  # ─── STAGE 2: BUILD (only runs if security passes) ──────
  - stage: Build
    displayName: '🔨 Build & Test'
    dependsOn: SecurityScan
    jobs:
      - job: BuildAndTest
        steps:
          - script: dotnet build --configuration Release
            displayName: 'Build'
          - script: dotnet test --no-build --logger trx
            displayName: 'Run tests'

  # ─── STAGE 3: DAST (against deployed staging) ───────────
  - stage: DAST
    displayName: '🌐 Dynamic Analysis'
    dependsOn: Build
    jobs:
      - job: ZAPScan
        steps:
          - script: |
              # Run OWASP ZAP against staging environment
              docker run --rm -v $(pwd):/zap/wrk:rw \
                ghcr.io/zaproxy/zaproxy:stable \
                zap-baseline.py -t https://myapp-staging.azurewebsites.net \
                -r zap-report.html -J zap-results.json
            displayName: 'OWASP ZAP baseline scan'

          - publish: zap-report.html
            artifact: 'dast-report'

Quality Gates for Security

Configure your pipeline to block deployments when security thresholds are breached:

  • SAST — Block on any Critical severity finding; warn on High
  • SCA — Block on Critical CVEs with known exploits; require remediation plan for High
  • Container — Block if base image has Critical vulnerabilities; require update within 7 days for High
  • Secrets — Block immediately on any committed credential (zero tolerance)
  • DAST — Block on OWASP Top 10 High/Critical findings in staging

Exercises

Exercise 1 Difficulty: Intermediate

Configure RBAC with Custom Security Group

Goal: Create a custom security group "Security Reviewers" that can approve PRs to infrastructure code but can't directly push to protected branches.

  1. Create a custom security group named Security Reviewers in your project.
  2. Add 2-3 users (or yourself with a different account) to the group.
  3. Configure repository permissions: grant Contribute to pull requests but deny Force push and Bypass policies when pushing.
  4. Create a branch policy on main requiring at least 1 reviewer from the Security Reviewers group for changes to /infrastructure/** paths.
  5. Submit a PR that modifies a file in /infrastructure/ and verify the group is automatically required as a reviewer.
  6. Verify the group members cannot directly push to main (only approve PRs).

Success criteria: Infrastructure changes require Security Reviewer approval. Group members can approve but cannot bypass branch policies or force push.

RBAC Custom Groups Branch Policies
Exercise 2 Difficulty: Intermediate

Azure Key Vault Integration with Variable Group

Goal: Set up Azure Key Vault, link it to a pipeline variable group, and consume secrets at runtime.

  1. Create an Azure Key Vault (e.g., bootcamp-secrets-kv) in your subscription.
  2. Add three secrets: DatabasePassword, ApiKey, and StorageConnectionString.
  3. Create a service connection in Azure DevOps (preferably with Workload Identity Federation).
  4. Create a variable group linked to Key Vault — map all three secrets.
  5. Write a pipeline that references the variable group and uses the AzureKeyVault@2 task.
  6. Verify that secrets are masked in logs (try echo $(DatabasePassword) — should show ***).
  7. Check Key Vault access logs to confirm the pipeline accessed the secrets.

Success criteria: Pipeline successfully fetches secrets from Key Vault at runtime. Secrets are masked in pipeline logs. Key Vault audit log shows access events from the pipeline identity.

Key Vault Variable Groups Service Connections
Exercise 3 Difficulty: Advanced

Secure Pipeline with Protected Resources & Extends Template

Goal: Create a mandatory security scanning template that all pipelines must extend, with protected resource enforcement.

  1. Create a shared repository pipeline-templates with a security template (secure-pipeline.yml) that runs credential scanning.
  2. Mark your production service connection and production environment as protected resources.
  3. Configure the project to require extends templates from the shared repository (Organization Settings → Pipelines → Settings).
  4. Create a pipeline that extends the secure template and deploys to the production environment.
  5. Verify: Create a second pipeline that does NOT extend the template — confirm it cannot access protected resources.
  6. Verify: Attempt to modify the extends reference in the pipeline YAML — confirm it requires authorization.

Success criteria: Only pipelines extending the approved template can access production resources. Unauthorized pipelines are blocked. Template enforcement prevents bypass of security scanning.

Extends Templates Protected Resources Pipeline Security
Exercise 4 Difficulty: Intermediate

Audit Log Streaming & Security Alerts

Goal: Enable audit log streaming to Azure Monitor Log Analytics and create an alert for permission escalation events.

  1. Create an Azure Log Analytics workspace (if you don't have one).
  2. Configure audit log streaming: Organization Settings → Auditing → New stream → Azure Monitor.
  3. Perform several auditable actions (create a group, add a member, modify a branch policy).
  4. Wait 5-10 minutes, then query the AzureDevOpsAuditing table in Log Analytics — confirm events appear.
  5. Create a KQL alert rule that triggers when a user is added to "Project Administrators" or "Collection Administrators".
  6. Add yourself to a Project Administrators group and verify the alert fires (check Action Group notification).

Success criteria: Audit events stream to Log Analytics within minutes. Alert fires automatically when permission escalation is detected. You receive a notification (email/Teams/webhook).

Audit Logs Log Analytics Alerting

Next in the Bootcamp

In Module 11: Enterprise Patterns & AI Integration, we'll explore multi-team governance at scale, large organization patterns, Azure DevOps + GitHub together, AI-assisted development with GitHub Copilot in pipelines, and emerging practices for enterprise DevOps.