Skip to main content

Microsoft Entra

Overview

This integration uses both a bash script and PowerShell script to execute. The PowerShellpowershell script managesis designed to manage named locations in Microsoft Azure Conditional Access policies via the Microsoft Graph API. It allows users to add, delete, or flush named locations related to specific IP addresses. The primary use case is to automate the management of named locations in a secure and efficient manner.

This integration with knocknoc enables IP whitelisting within Microsoft 365. An example of a user's experience can be found below:

  1. User attempts to access Microsoft 365 services:

    image.png

  2. User must authenticate with knocknoc and will see their granted ACL on the right hand side:

    image.pngimage.png

     

  3. User can now access Microsoft 365 services:

    image.png

Prerequisites

Before running this script, ensure that you have the following prerequisites installed and configured:

  • Powershell - Install PowerShell on your system. For Linux, follow these steps:
    # Update the list of packages
    sudo apt-get update
    
    # Install pre-requisite packages
    sudo apt-get install -y wget apt-transport-https software-properties-common
    
    # Download the Microsoft repository GPG keys
    wget -q https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb
    
    # Register the Microsoft repository GPG keys
    sudo dpkg -i packages-microsoft-prod.deb
    
    # Update the list of packages after we added the Microsoft repository
    sudo apt-get update
    
    # Install PowerShell
    sudo apt-get install -y powershell
  • Azure Enterprise Application: An enterprise application must be created to allow knocknoc to authenticate and change conditional access policies and named locationslocations. inCreate an enterprise application, create a client secret, and copy the application's client id, azure tenant id and secret. You'll need these later.

  • Microsoft AzureGraph usingAPI Permissions: Ensure that the application has the necessary permissions to access the Microsoft Graph API.API, Itparticularly supportsfor adding, removing, and flushing IP-based named locations within conditional access policies. The script is designed to optimize API calls by caching policy and location data.

    Prerequisites

    • Azure PowerShell Module
    • Microsoft Graph PowerShell Module
    • Azure AD app registration with appropriate permissions to managemanaging conditional access policies and named locations.

  • Conditional Access Policy/Policies: Knocknoc needs to be able to distinguish between policies it can amend and policies it cannot. Therefore, knocknoc looks for a prepending "knocknoc_" prior to the name of the ACL.  For example, a conditional access policy might be named "knocknoc_financedepartment" with specific rules around applications and services that group can access. These must be created PRIOR to configuring anything in the knocknoc admin portal. 
  • Credentials File: Ensure that a credentials file is present at /opt/knocknoc-agent/etc/entra-credentials.sh with the following content:

     
    entra_clientid=""
    entra_tenantid=""
    entra_clientsecret=""

    You will need to input the necessary ClientID for your  Azure Enterprise Application, your Azure Tenant ID and your application's client secret. 

     

Script Parameters

    $action:
  • action: Specifies the action to perform. AcceptableValid values are `add`add, `remove`del, and `flush`flush.

  • acl: $acl:The Namename of the access control listlist.
  • (ACL)
  • ip: to modify.

    • $ip:The IP address to add or removedelete (required for add and del actions).

  • ClientId: The client ID of your Azure AD application (sourced from the namedcredentials location.

    file).
  • Functions

  • TenantId:

    Get-ClientSecret

    The
    functiontenant Get-ClientSecretID {of paramyour Azure AD (sourced [string]$secretNamefrom )the returncredentials "your-secure-secret"file).
  • ClientSecret: # Placeholder for testing }

    Retrieves aThe client secret securelyof your Azure AD application (sourced from the credentials file).

Script Details

  • The script begins by initializing a storagelocation systemcache liketo Azurestore Keyexisting Vault.

    named

    Get-NamedLocationId

    locations.
  • function
  • Depending Get-NamedLocationId {     param ([string]$displayName)     # Implementation... }
  • Retrieveson the IDspecified ofaction (add, del, or flush), the script performs the corresponding operation.

  • For the add action, the script creates a named location by its display name, caching the result to minimize repeated API calls.

    Update-NamedLocationTrustStatus

    function Update-NamedLocationTrustStatus {
        param (
            [string]$NamedLocationId,
            [bool]$IsTrusted,
            [string]$LocationType = "#microsoft.graph.ipNamedLocation"
        )
        # Implementation...
    }


    Updates the trust status of a named location.

    Get-ConditionalAccessPolicy

    function Get-ConditionalAccessPolicy {
        param ([string]$displayName)
        # Implementation...
    }


    Retrieves a conditional access policy by its display name, caching the result to minimize repeated API calls.

    Update-ConditionalAccessPolicy

    function Update-ConditionalAccessPolicy {
        param (
            [string]$PolicyId,
            [array]$LocationIds
        )
        # Implementation...
    }

    Updates a conditional access policy with a list of named location IDs.

    Add-NamedLocationAndModifyPolicy

    ```powershell
    function Add-NamedLocationAndModifyPolicy {
        # Implementation...
    }
    ```

    Adds a named location if it does not exist and updates the specified conditional access policy to includeexclude this location.

  • For the newdel namedaction, location.

    the

    Remove-LocationFromPolicies

    script

    ```powershell
    functionremoves Remove-LocationFromPoliciesthe {
        param ([string]$NamedLocationId)
        # Implementation...
    }
    ```

    Removes aspecified named location from all conditional access policies and then deletes the named location.

    location

    Flush-NamedLocationsFromPolicy

    itself.
  • ```powershell
    function Flush-NamedLocationsFromPolicy {
        # Implementation...
    }
    ```

    Removes all named locations matching

  • For the patternflush `knocknoc_*` from all conditional access policies and then deletes these named locations.

    Usage

    Add a Named Location and Modify Policy

    ```powershell
    .\Script.ps1 -action add -acl "aclName" -ip "192.168.1.1"
    ```

    Addsaction, the specifiedscript IP address as a named location and modifies the specified access control list to include this named location.

    Remove a Named Location from Policies

    ```powershell
    .\Script.ps1 -action remove -acl "aclName" -ip "192.168.1.1"
    ```

    Removes the specified IP address from the named locations in all policies.

    Flush Named Locations from Policies

    ```powershell
    .\Script.ps1 -action flush -acl "aclName"
    ```

    Removesremoves all named locations that match the pattern `knocknoc_*` from all policies.

  • Authentication

    The script requires authentication with Microsoft Graph API. This is achieved by creating a PSCredential object using the client ID, tenant ID, and client secret.

    Example

    ```powershell
    param (
        [ValidateSet('add', 'remove', 'flush')][string]$action,
        [string]$acl,
        [ValidatePattern('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')][string]$ip
    )

    # Authentication
    $ClientId = "client"
    $TenantId = "tenant"
    $ClientSecret = "secret"

    try {
        $ClientSecretPass = ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force
        $ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $ClientId, $ClientSecretPass

        Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $ClientSecretCredential -NoWelcome
    } catch {
        Write-Host "Failed to authenticate with Microsoft Graph. Error: $_"
        exit 1
    }

    # Action map
    $actionMap = @{
        'add'    = { Add-NamedLocationAndModifyPolicy }
        'remove' = {
            $NamedLocationId = Get-NamedLocationId "knocknoc_$ip"
            if ($null -eq $NamedLocationId) {
                Write-Host "Named location 'knocknoc_$ip' not found."
            } else {
                Remove-LocationFromPolicies -NamedLocationId $NamedLocationId
            }
        }
        'flush'  = { Flush-NamedLocationsFromPolicy }
    }

    # Execute action
    if ($actionMap.ContainsKey($action)) {
        & $actionMap[$action]
    } else {
        Write-Host "Invalid action specified"
    }
    ```

Script

Admin
Portal
param (
 [ValidateSet('add', 'remove', 'flush')][string]$action,
 [string]$acl,
 [ValidatePattern('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')][string]$ip
)


# Caching policy and named location data to reduce repeated API calls
$policyCache = @{}
$locationCache = @{}

# Securely retrieve client secret
function Get-ClientSecret {
 param (
 [string]$secretName
 )
 # Implementation for retrieving secret from a secure storage like Azure Key Vault
 return "your-secure-secret" # Placeholder for testing
}

# Helper functions
function Get-NamedLocationId {
 param ([string]$displayName)
 if ($locationCache.ContainsKey($displayName)) {
 return $locationCache[$displayName]
 }

 try {
 $namedLocations = Get-MgIdentityConditionalAccessNamedLocation -Filter "DisplayName eq '$displayName'"
 } catch {
 Write-Host "Error occurred while querying named locations: $_"
 return $null
 }

 if ($null -eq $namedLocations -or $namedLocations.Count -eq 0) {
 Write-Host "Named location '$displayName' not found."
 return $null
 }

 $namedLocation = $namedLocations | Select-Object -First 1
 $namedLocationId = $namedLocation.Id

 if ([string]::IsNullOrEmpty($namedLocationId)) {
 Write-Host "Named location ID is null or empty."
 return $null
 }

 $locationCache[$displayName] = $namedLocationId
 return $namedLocationId
}

function Update-NamedLocationTrustStatus {
 param (
 [string]$NamedLocationId,
 [bool]$IsTrusted,
 [string]$LocationType = "#microsoft.graph.ipNamedLocation"
 )

 $body = @{
 "@odata.type" = $LocationType
 IsTrusted = $IsTrusted
 }

 try {
 Update-MgIdentityConditionalAccessNamedLocation -NamedLocationId $NamedLocationId -BodyParameter $body
 return $true
 } catch {
 Write-Host "Failed to update trust status for $NamedLocationId. Error: $_"
 return $false
 }
}

function Get-ConditionalAccessPolicy {
 param ([string]$displayName)
 if ($policyCache.ContainsKey($displayName)) {
 return $policyCache[$displayName]
 }

 $policy = Get-MgIdentityConditionalAccessPolicy -Filter "DisplayName eq '$displayName'"
 if ($null -eq $policy) {
 Write-Host "Conditional Access Policy '$displayName' not found."
 return $null
 }
 $policyCache[$displayName] = Get-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $policy.Id
 return $policyCache[$displayName]
}

function Update-ConditionalAccessPolicy {
 param (
 [string]$PolicyId,
 [array]$LocationIds
 )

 $validIds = $LocationIds | Where-Object { $_ -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' }
 if ($validIds.Count -eq 0) {
 Write-Host "No valid location IDs to update for policy $PolicyId."
 return
 }

 try {
 $params = @{
 Conditions = @{
 Locations = @{
 IncludeLocations = $validIds
 }
 }
 }
 Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $PolicyId -BodyParameter $params
 Write-Host "Policy $PolicyId updated successfully."
 } catch {
 Write-Host "Failed to update policy $PolicyId. Error: $_"
 }
}

function Add-NamedLocationAndModifyPolicy {
 $namedLocationId = Get-NamedLocationId "knocknoc_$ip"
 $policy = Get-ConditionalAccessPolicy "knocknoc_$acl"

 if ($null -eq $namedLocationId) {
 $params = @{
 "@odata.type" = "#microsoft.graph.ipNamedLocation"
 DisplayName = "knocknoc_$ip"
 IsTrusted = $true
 ipRanges = @(
 @{
 "@odata.type" = "#microsoft.graph.iPv4CidrRange"
 cidrAddress = "$ip/32"
 }
 )
 }
 $newLocation = New-MgIdentityConditionalAccessNamedLocation -BodyParameter $params
 $namedLocationId = $newLocation.Id
 $locationCache["knocknoc_$ip"] = $namedLocationId
 Write-Host "Created new named location 'knocknoc_$ip' with ID $namedLocationId."
 } else {
 Write-Host "Named location 'knocknoc_$ip' already exists. No action required."
 return
 }

 if ($null -eq $policy) {
 Write-Host "Policy not found"
 return
 }

 $currentLocations = @($policy.Conditions.Locations.IncludeLocations)
 if ($namedLocationId -notin $currentLocations) {
 $updatedLocations = $currentLocations + $namedLocationId
 Update-ConditionalAccessPolicy -PolicyId $policy.Id -LocationIds $updatedLocations
 }
}

function Remove-LocationFromPolicies {
 param ([string]$NamedLocationId)

 if ([string]::IsNullOrEmpty($NamedLocationId)) {
 Write-Host "Invalid named location ID."
 return
 }

 $allPolicies = Get-MgIdentityConditionalAccessPolicy
 $policiesToUpdate = @()

 foreach ($policy in $allPolicies) {
 if ($NamedLocationId -in $policy.Conditions.Locations.IncludeLocations) {
 $updatedLocations = $policy.Conditions.Locations.IncludeLocations | Where-Object { $_ -ne $NamedLocationId }
 $policiesToUpdate += [PSCustomObject]@{ PolicyId = $policy.Id; LocationIds = $updatedLocations }
 }
 }

 foreach ($policyUpdate in $policiesToUpdate) {
 Update-ConditionalAccessPolicy -PolicyId $policyUpdate.PolicyId -LocationIds $policyUpdate.LocationIds
 }

 # Validate updates
 $allPolicies = Get-MgIdentityConditionalAccessPolicy
 foreach ($policy in $allPolicies) {
 if ($NamedLocationId -in $policy.Conditions.Locations.IncludeLocations) {
 Write-Host "Location $NamedLocationId still exists in policy $($policy.Id) after update attempt."
 return
 }
 }

 # Update trust status before deletion
 if (-not (Update-NamedLocationTrustStatus -NamedLocationId $NamedLocationId -IsTrusted $false)) {
 Write-Host "Failed to update trust status for $NamedLocationId. Cannot proceed with deletion."
 return
 }

 try {
 Remove-MgIdentityConditionalAccessNamedLocation -NamedLocationId $NamedLocationId
 Write-Host "Named location $NamedLocationId removed successfully."
 } catch {
 Write-Host "Failed to remove named location $NamedLocationId. Error: $_"
 }
}

function Flush-NamedLocationsFromPolicy {
 $namedLocations = Get-MgIdentityConditionalAccessNamedLocation | Where-Object { $_.DisplayName -like "*knocknoc_*" }

 $jobs = @()
 $throttleLimit = 5 # Adjust the throttle limit as needed

 foreach ($location in $namedLocations) {
 $jobs += Start-Job -ScriptBlock {
 param (
 $location,
 $ClientId,
 $TenantId,
 $ClientSecret
 )

 function Update-NamedLocationTrustStatus {
 param (
 [string]$NamedLocationId,
 [bool]$IsTrusted,
 [string]$LocationType = "#microsoft.graph.ipNamedLocation"
 )

 $body = @{
 "@odata.type" = $LocationType
 IsTrusted = $IsTrusted
 }

 try {
 Update-MgIdentityConditionalAccessNamedLocation -NamedLocationId $NamedLocationId -BodyParameter $body
 return $true
 } catch {
 Write-Host "Failed to update trust status for $NamedLocationId. Error: $_"
 return $false
 }
 }

 function Remove-LocationFromPolicies {
 param ([string]$NamedLocationId)

 $allPolicies = Get-MgIdentityConditionalAccessPolicy
 $policiesToUpdate = @()

 foreach ($policy in $allPolicies) {
 if ($NamedLocationId -in $policy.Conditions.Locations.IncludeLocations) {
 $updatedLocations = $policy.Conditions.Locations.IncludeLocations | Where-Object { $_ -ne $NamedLocationId }
 $policiesToUpdate += [PSCustomObject]@{ PolicyId = $policy.Id; LocationIds = $updatedLocations }
 }
 }

 foreach ($policyUpdate in $policiesToUpdate) {
 Update-ConditionalAccessPolicy -PolicyId $policyUpdate.PolicyId -LocationIds $policyUpdate.LocationIds
 }

 # Validate updates
 $allPolicies = Get-MgIdentityConditionalAccessPolicy
 foreach ($policy in $allPolicies) {
 if ($NamedLocationId -in $policy.Conditions.Locations.IncludeLocations) {
 Write-Host "Location $NamedLocationId still exists in policy $($policy.Id) after update attempt."
 return $false
 }
 }
 return $true
 }

 function Update-ConditionalAccessPolicy {
 param (
 [string]$PolicyId,
 [array]$LocationIds
 )

 $validIds = $LocationIds | Where-Object { $_ -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' }
 if ($validIds.Count -eq 0) {
 Write-Host "No valid location IDs to update for policy $PolicyId."
 return
 }

 try {
 $params = @{
 Conditions = @{
 Locations = @{
 IncludeLocations = $validIds
 }
 }
 }
 Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $PolicyId -BodyParameter $params
 } catch {
 Write-Host "Failed to update policy $PolicyId. Error: $_"
 }
 }

 # Re-establish connection inside job
 $ClientSecretPass = ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force
 $ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $ClientId, $ClientSecretPass

 Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $ClientSecretCredential -NoWelcome

 $trustUpdateSuccess = Update-NamedLocationTrustStatus -NamedLocationId $location.Id -IsTrusted $false

 if ($trustUpdateSuccess) {
 $removeSuccess = $false
 $retryCount = 0
 $maxRetries = 5

 while (-not $removeSuccess -and $retryCount -lt $maxRetries) {
 $removeSuccess = Remove-LocationFromPolicies -NamedLocationId $location.Id
 if (-not $removeSuccess) {
 Start-Sleep -Seconds 5 # Adjust sleep time as needed
 $retryCount++
 }
 }

 if ($removeSuccess) {
 try {
 Remove-MgIdentityConditionalAccessNamedLocation -NamedLocationId $location.Id
 } catch {
 Write-Host "Failed to remove named location $location.Id after updates. Error: $_"
 }
 } else {
 Write-Host "Could not confirm removal from policies for location $location.Id."
 }
 } else {
 Write-Host "Skipping deletion due to failed trust status update for location $location.Id."
 }
 } -ArgumentList $location, $ClientId, $TenantId, $ClientSecret

 while ($jobs.Count -ge $throttleLimit) {
 $jobs | ForEach-Object { Wait-Job -Job $_ }
 $jobs | ForEach-Object { Receive-Job -Job $_ }
 $jobs | ForEach-Object { Remove-Job -Job $_ }
 $jobs = $jobs | Where-Object { $_.State -ne 'Completed' }
 Start-Sleep -Seconds 1
 }
 }

 # Wait for all remaining jobs to complete
 $jobs | ForEach-Object { Wait-Job -Job $_ }
 $jobs | ForEach-Object { Receive-Job -Job $_ }
 $jobs | ForEach-Object { Remove-Job -Job $_ }
}

# Authentication and connection setup
$ClientId = "client"
$TenantId = "tenant"
$ClientSecret = "secret"

try {
 $ClientSecretPass = ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force
 $ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $ClientId, $ClientSecretPass

 Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $ClientSecretCredential -NoWelcome
} catch {
 Write-Host "Failed to authenticate with Microsoft Graph. Error: $_"
 exit 1
}

# Define a hashtable with actions as keys and their corresponding functions as values
$actionMap = @{
 'add' = { Add-NamedLocationAndModifyPolicy }
 'remove' = {
 $NamedLocationId = Get-NamedLocationId "knocknoc_$ip"
 if ($null -eq $NamedLocationId) {
 Write-Host "Named location 'knocknoc_$ip' not found."
 } else {
 Remove-LocationFromPolicies -NamedLocationId $NamedLocationId
 }
 }
 'flush' = { Flush-NamedLocationsFromPolicy }
}

# Check if the action exists in the hashtable and invoke the corresponding function
if ($actionMap.ContainsKey($action)) {
 & $actionMap[$action]
} else {
 Write-Host "Invalid action specified"
}

NotesConfiguration

 Ensure

that the Azure and Microsoft Graph PowerShell modules are installed and imported in your environment.
• Adjust the throttle limit for concurrency as needed.
• Securely store and retrieve secrets using Azure Key Vault or another secure method.