Azure Conditional Access Policies as Code with Graph API, Part 1: Write

Conditional access is an additional layer of security whereby it looks for signals and makes a decision on which actions the user must complete in order to access a resource.

Typically conditional access policies are manually managed in Azure AD through the portal, however we can leverage Microsoft Graph API to automate these policies through code.

In this series, I'll be demonstrating how you can manage Conditional Access Policies as Code through a CI/CD pipeline. This post is the first part in which I'll discuss how to import and validate your policies. We'll also begin building our pipeline to automate this workflow.

You can check out the other parts from the links below:

Full code examples can be found on my GitHub

Workflow

I've gone with a workflow akin to Terraform in which we'll Write (import and modify our policies), Plan (preview changes before applying) and Apply (deploy our configuration changes).

We'll be using PowerShell and Azure DevOps to automate this process. Also have a service principal ready to use.

Access token

First, to run any CRUD operations against AAD resources we need to acquire an access token. The PowerShell module to do so can be found [here]

The module:

  • Takes your service principal's client ID, client secret and AAD tenant ID as parameters

  • Then it sends those credentials to the Microsoft identity provider with a POST request

  • An access token is acquired and stored in $token

Import existing policies

Now that we have an access token, we need to import our existing policies.

The example PowerShell module is [here]

  • The module calls the Get-GraphToken function to fetch an access token

  • Invokes a GET request on the conditional access policies resource URL to retrieve all policies in the given tenant

  • It then outputs each policy into its own JSON file with the file name as the policy ID

We should have policy configuration files that look like the below:

Validate policies

Our policies have been imported and we have definitions for each of them. Let's add a step to validate them such that if someone were to create/modify a policy, it would check to see all the required properties are there.

The script to validate our policies is [here]. This will help us to enable a pre-check on our policies in the pipeline.

This was mostly taken from Wesley Trust's implementation, so credit goes to him for the awesome script

Pipeline

Let's begin creating the Azure DevOps pipeline in YAML. We'll take the validate script off our terminal and into this pipeline. The definition of the pipeline to start with is below:

name: Conditional_access - ${{ parameters.Action }}

trigger: none

pr: none

pool:
  vmImage: 'windows-2019'

parameters:
  - name: Action
    displayName: Action
    type: string
    default: 'Plan'
    values:
    - Plan
    - Apply

variables:
  isMain: $[in(variables['Build.SourceBranch'], 'refs/heads/main')]

stages:
  - stage: Validate
    jobs:
    - job: validate
      continueOnError: false
      steps:
      - task: Npm@1
        displayName: 'Install jsonlint'
        inputs:
          command: 'custom'
          customCommand: 'install jsonlint -g'
      - task: PowerShell@2
        displayName: 'Perform JSON syntax validation'
        inputs:
          targetType: 'inline'
          script: |
            Get-ChildItem -Path "$(Build.SourcesDirectory)" -Recurse -Include "*.json" |
              ForEach-Object {
                $jsonFile = $_.FullName
                Write-Host "[INFO] Validating [$jsonFile]"
                jsonlint $_
            }
          failOnStderr: true
      - task: PowerShell@2
        displayName: 'validate policies'
        inputs:
          filePath: '$(Build.SourcesDirectory)/scripts/validate-policies.ps1'

The pipeline has no trigger, it is manually triggered. However, feel free to change this to suit your requirements

The first stage, validate:

  • Installs jsonlint with npm

  • Runs jsonlint command against our JSON policy files to check for their validity and inform of any errors

  • Executes the validate-policies.ps1 script from the /scripts/ directory

Running the pipeline now has the below output:

ca-blog.png

Thanks for reading, be sure to check out part 2 where we'll discuss the Plan phase of Conditional Access Policies as Code with Graph API