Domain 4 · 10–15%

Develop a security and compliance plan

Who and what your automation runs as, how secrets are stored and delivered without ever touching source control, and how vulnerabilities in code, dependencies and containers are caught automatically before they ship.

Three objectives:

4.1 Authentication & authorization methods

Service principal vs managed identity

The most heavily tested comparison in the domain. Both are Microsoft Entra identities that non-human workloads use to authenticate to Azure.

Service principalManaged identity
What it isAn app identity in Entra ID (the local instance of an app registration)An Azure-managed service principal you never see the credentials for
CredentialsClient secret or certificate — you store and rotate themNone to manage — Azure rotates automatically
Works fromAnywhere (other clouds, on-prem, external CI)Only from an Azure resource that supports it
Federation (OIDC)✅ workload identity federation — no secret✅ (system-assigned, inherently federated)
TypesOne kindSystem-assigned and user-assigned
System-assigned MIUser-assigned MI
LifecycleTied to one resource; deleted with itStandalone resource; independent lifecycle
Sharing1:1 — one identity, one resourceAssignable to many resources
Pre-provision before the resource existsNoYes
Best forA single, self-contained resourceMany resources needing the same permissions; stable identity across redeploys

Caveat — the decision tree

Runs inside Azure and calls Azure servicesmanaged identity (no secret to leak). Many resources need the same role, or you must pre-create the identityuser-assigned. One dedicated resourcesystem-assigned. Runs outside Azure (GitHub Actions, external CI, another cloud) → service principal, ideally with workload identity federation so there is still no stored secret.

GitHub authentication

MethodIdentityBest for
GitHub AppIts own identity, fine-grained repo/org permissions, high rate limitsRecommended for integrations and automation
GITHUB_TOKENAuto-generated per workflow run, scoped to the repo, expires at run endActions working within the same repository
Fine-grained PATA user, scoped to specific repos, mandatory expiryPersonal/cross-repo scripts when an App is overkill
Classic PATA user, coarse scopesLegacy — avoid
GitHub Actions least-privilege GITHUB_TOKEN
permissions:            # start from nothing, grant only what's needed
  contents: read
  pull-requests: write
  id-token: write       # enables OIDC to Azure / cloud providers

Caveat — GITHUB_TOKEN vs App vs PAT

Same repo, from a workflowGITHUB_TOKEN (zero setup, auto-expires). Cross-repo or a third-party integration acting as a serviceGitHub App (not tied to a person, higher limits). A PAT ties automation to an individual and is the answer only when neither of the above fits. Default-deny permissions and grant per workflow.

Azure DevOps authentication

Service connection typeConnects to
Azure Resource ManagerAzure subscriptions — prefer workload identity federation
Docker RegistryACR, Docker Hub
KubernetesAKS or any cluster
GitHubGitHub repositories
Generic / SSHREST APIs, custom services, SSH targets

Prefer workload identity federation for the ARM connection

An ARM service connection using workload identity federation stores no secret or certificate — Entra ID trusts a short-lived token issued to the pipeline. It is the recommended default: nothing to rotate, nothing to leak. Fall back to a service-principal secret only where federation is not supported, and use a managed identity connection when the agent runs on an Azure VM.

Permissions, roles and access levels

Azure DevOps groupScope
Project Collection AdministratorsOrganization-wide control
Project AdministratorsFull control within a project
ContributorsRead/write most project resources
ReadersRead-only
Build / Release AdministratorsManage pipelines / releases
Azure DevOps access levelGrants
Stakeholder freeWork items, dashboards, view pipelines — no repo/code access, no Test Plans
BasicFull access to Repos, Pipelines, Artifacts, Boards
Basic + Test PlansBasic plus the Test Plans feature
Visual Studio subscriberEntitlement via a VS subscription

Caveat — Stakeholder access and outside collaborators

Stakeholder is the free Azure DevOps access level for non-technical people who need Boards/dashboards but not code — the answer to "give the product owner backlog access at no cost". On GitHub, an outside collaborator is a non-org member granted access to specific repos; prefer teams for manageability, and remember Deny wins in Azure DevOps while the highest permission wins in GitHub (from Domain 2).

Projects and teams in Azure DevOps

An organization contains projects; each project contains teams (each with its own backlog, boards and area/iteration paths). Use one project with multiple teams for related work that shares process and reporting; use separate projects for hard security or process boundaries. Security groups and permissions are configured at org, project and object level.

4.2 Managing sensitive information in automation

Azure Key Vault

ObjectStores
SecretsPasswords, connection strings, API keys
KeysRSA/EC keys for encryption & signing (wrap/unwrap, sign/verify)
CertificatesTLS/SSL certs with lifecycle & auto-renewal (integrates with issuers)
StandardPremium
Secrets, certs, software-protected keys
HSM-backed keys (FIPS 140-2 Level 2)

Caveat — RBAC over access policies; HSM only in Premium

Use the Azure RBAC permission model (fine-grained, auditable, roles like Key Vault Secrets User) rather than the legacy vault access policies (all-or-nothing per operation). HSM-backed keys require the Premium SKU; for a customer-managed key that Microsoft can never extract, that is the answer. Enable soft-delete and purge protection (soft-delete is on by default) so a deleted secret can be recovered.

azure-pipelines.yml AzureKeyVault task
- task: AzureKeyVault@2
  inputs:
    azureSubscription: 'MyServiceConnection'
    KeyVaultName: 'my-kv'
    SecretsFilter: 'DbPassword,ApiKey'
    RunAsPreJob: true      # fetch before steps run

- script: ./deploy.sh
  env:
    DB_PASSWORD: $(DbPassword)   # masked in logs
GitHub Actions fetch via Azure CLI (OIDC)
- uses: azure/login@v2
  with:
    client-id:       ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id:       ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- run: |
    echo "DB_PASSWORD=$(az keyvault secret show \
      --vault-name my-kv --name DbPassword \
      --query value -o tsv)" >> "$GITHUB_ENV"

Secretless authentication — OIDC / workload identity federation

The modern answer to "deploy to Azure from CI without storing a credential." The CI platform issues a short-lived OIDC token; Entra ID is configured to trust tokens from that specific repo/branch/environment and exchanges it for an Azure access token. No secret exists to steal or rotate.

sequenceDiagram participant W as CI job (GitHub/ADO) participant I as OIDC issuer participant E as Microsoft Entra ID participant A as Azure resource W->>I: request OIDC token (subject = repo:org/app:ref) I-->>W: signed short-lived JWT W->>E: present JWT to federated credential E-->>W: Azure access token (if subject matches trust) W->>A: call Azure with the access token
Federated identity: trust is scoped to a subject (repo + branch/environment), so a token from elsewhere is rejected.
GitHub Actions passwordless Azure login
permissions:
  id-token: write        # REQUIRED to request the OIDC token
  contents: read

steps:
  - uses: azure/login@v2
    with:
      client-id:       ${{ secrets.AZURE_CLIENT_ID }}
      tenant-id:       ${{ secrets.AZURE_TENANT_ID }}
      subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      # no client-secret — Entra federated credential trusts this repo/branch

Caveat — OIDC needs id-token: write and a scoped subject

Two things the exam checks: the workflow must grant permissions: id-token: write, and the Entra federated credential's subject must match the exact repo and ref/environment (e.g. repo:org/app:ref:refs/heads/main or repo:org/app:environment:production). A mismatch is why "the OIDC login is rejected" — the fix is aligning the subject, never adding a secret back.

Secrets in pipelines and Actions

ScopeAzure PipelinesGitHub Actions
Single pipeline/repoSecret pipeline variable (lock icon)Repository secret
Shared across pipelines/reposVariable group (optionally Key Vault-linked)Organization secret
Per deployment targetVariables on the environmentEnvironment secret (behind required reviewers)
azure-pipelines.yml passing a secret safely
steps:
  - script: echo "$(publicVar)"       # fine
  - script: echo "$(mySecret)"         # prints *** — masked, but don't do this

  # Correct: map the secret into the process environment explicitly
  - script: ./deploy.sh
    env:
      API_KEY: $(mySecret)             # secrets are NOT auto-injected as env vars

Caveat — secret variables aren't auto-exposed

Unlike normal variables, secret variables are not automatically available as environment variables to scripts — you must map them in explicitly under env:. Both platforms mask registered secrets in logs (***), but masking is best-effort: transformed or base64-encoded values can slip through, so never deliberately print them. Secrets are not passed to fork PR builds.

Azure Pipelines secure files

azure-pipelines.yml DownloadSecureFile
- task: DownloadSecureFile@1
  name: cert                        # reference by this name
  inputs:
    secureFile: 'signing-cert.p12'

- script: |
    echo "Cert at $(cert.secureFilePath)"
    # import / sign using the temp path; the file is deleted after the job

Secure files store signing certificates, provisioning profiles, key files and the like outside source control, with per-pipeline authorization. They live in the Library and are removed from the agent when the job ends.

Preventing secret leakage

VectorControl
Secret committed to GitPre-commit hooks (gitleaks, detect-secrets) + push protection
Secret in a PR diffSecret scanning blocks the push; Advanced Security alerts
Secret printed in logsAuto-masking of registered secrets; never echo them
Secret baked into an artifact/imageScan artifacts and images; use build-time secrets, not ENV layers
Secret in env-var dumpsLimit diagnostic logging; use secure files/Key Vault
Long-lived credentialsReplace with OIDC / managed identity; rotate what remains

Designing pipelines to not leak

Fetch secrets at the last moment (Key Vault at runtime), scope them to environments, never write them to artifacts, and prefer secretless auth so there is nothing to leak. If a secret is exposed, the response is the incident flow from Domain 2: rotate first, then rewrite history.

4.3 Automate security & compliance scanning

The shift-left model

flowchart LR IDE["IDE
linting · secrets"] --> PR["PR / CI
SAST · SCA · secret scan"] PR --> BUILD["Build
SBOM · license check"] BUILD --> DEP["Deploy
image scan · IaC scan"] DEP --> RUN["Runtime
DAST · CSPM · Defender"]
The earlier a finding is caught, the cheaper it is to fix — push every scan as far left as it will go.
ScanFindsWhen
SAST (static)Vulnerabilities in your source codePR / build (CodeQL)
DAST (dynamic)Vulnerabilities in the running appAgainst staging
SCA (composition)Vulnerable / mis-licensed dependenciesPR / build (Dependabot, dependency review)
Secret scanningCommitted credentialsPush / commit
Container scanningCVEs in base images and OS packagesBuild / registry (Defender, Trivy)
IaC scanningInsecure infrastructure configPR (PSRule, checkov)

The four scan categories in the skills measured

The objective names them explicitly: dependency, code, secret, and licensing scanning. Map each to a tool — dependency → Dependabot/SCA, code → CodeQL, secret → secret scanning, licensing → dependency review / SCA license policy.

GitHub Advanced Security (GHAS)

FeatureDoes
Code scanning (CodeQL)SAST — semantic analysis for security bugs
Secret scanning + push protectionDetects committed secrets; push protection blocks the push
Dependency reviewShows vulnerable/newly-added dependencies in the PR diff
Dependabot alertsNotifies on dependencies with known CVEs
Dependabot security / version updatesOpens PRs to patch or refresh dependencies
GitHub Actions CodeQL analysis
name: CodeQL
on:
  push: { branches: [main] }
  pull_request: { branches: [main] }
  schedule: [ { cron: '0 2 * * 1' } ]

jobs:
  analyze:
    runs-on: ubuntu-latest
    permissions:
      security-events: write        # upload results to the Security tab
      contents: read
    strategy:
      matrix:
        language: [javascript, python, csharp]
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
          queries: security-extended
      - uses: github/codeql-action/autobuild@v3
      - uses: github/codeql-action/analyze@v3

Caveat — GHAS for Azure DevOps

GitHub Advanced Security is available for Azure DevOps too — it brings CodeQL, secret scanning (with push protection on Azure Repos) and dependency scanning to Azure Repos and Pipelines, surfaced in the Advanced Security tab. This is exactly the kind of GitHub-into-Azure-DevOps convergence the July 2026 refresh leans on. Don't assume GHAS is GitHub-only.

azure-pipelines.yml Advanced Security for Azure DevOps
- task: AdvancedSecurity-Codeql-Init@1
  inputs: { languages: 'csharp' }
- task: AdvancedSecurity-Dependency-Scanning@1
- task: AdvancedSecurity-Codeql-Analyze@1
# Secret scanning + push protection are enabled in Project/Repo settings

Microsoft Defender for Cloud — DevOps security

Provides a single security posture view across Azure DevOps, GitHub and GitLab.

Caveat — Defender for Cloud vs GHAS

GHAS is the scanning engine (CodeQL, secret scanning, dependency review) living in the repo. Defender for Cloud DevOps security is the aggregation and posture-management layer across many repos/orgs and clouds. "Unified security posture across GitHub and Azure DevOps in one pane" → Defender for Cloud. "Block a secret at push time" → GHAS secret scanning push protection.

Container and dependency scanning

GitHub Actions Trivy image scan → Security tab
- run: docker build -t myapp:${{ github.sha }} .

- uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'myapp:${{ github.sha }}'
    format: sarif
    output: trivy.sarif
    severity: 'CRITICAL,HIGH'
    exit-code: '1'          # fail the build on findings

- uses: github/codeql-action/upload-sarif@v3
  with: { sarif_file: trivy.sarif }
.github/dependabot.yml SCA automation
version: 2
updates:
  - package-ecosystem: npm
    directory: "/"
    schedule: { interval: weekly }
    open-pull-requests-limit: 10
  - package-ecosystem: nuget
    directory: "/src"
    schedule: { interval: daily }
  - package-ecosystem: docker
    directory: "/"
    schedule: { interval: weekly }
  - package-ecosystem: github-actions   # keep pinned actions current
    directory: "/"
    schedule: { interval: weekly }

SARIF is the universal format

Any scanner that emits SARIF can upload results to the GitHub Security tab (or Azure DevOps Advanced Security) via upload-sarif, so third-party tools (Trivy, Semgrep, tfsec) appear alongside CodeQL. "Surface a non-GitHub scanner's results in GitHub" → upload the SARIF file.

CVSS scoreSeverity
9.0–10.0Critical
7.0–8.9High
4.0–6.9Medium
0.1–3.9Low

CodeQL in a container build

For compiled languages CodeQL must observe the build to analyse it. When the build runs in a container, use github/codeql-action/autobuild (or a manual build inside the same container step) so CodeQL traces the compilation — otherwise it has nothing to analyse.

Domain 4 rapid recap

The requirement says…Answer
Azure resource calls another Azure service, no secretsManaged identity
Many resources need the same permissionsUser-assigned managed identity
Deploy to Azure from GitHub Actions with no secretOIDC / workload identity federation + id-token: write
OIDC login rejectedFix the Entra federated credential subject to match repo/ref
Automation runs outside AzureService principal (federated if possible)
Store connection strings for a pipeline securelyKey Vault → variable group / AzureKeyVault@2
Customer-managed key Microsoft can't extractHSM-backed key — Key Vault Premium
Block a secret at push timeGHAS secret scanning push protection
SAST in PR reviews on GitHubCodeQL (code scanning)
Auto-patch vulnerable npm packagesDependabot security updates
Scan container images for CVEs in CITrivy / Defender for Containers
Unified security view across GitHub + Azure DevOpsDefender for Cloud — DevOps security
Show a third-party scanner's results in GitHubUpload the SARIF file
Store a code-signing cert for the pipelineSecure files
Free Boards access for a product ownerStakeholder access level
Protect prod credentials from PR workflowsEnvironment secrets behind required reviewers