A Complete Guide to Restricting Access to Sitecore Managed Cloud

Sitecore Managed Cloud can be a lot of things for an organization, saving a great deal of time and expense having offloaded numerous tasks and resources. One important step that's still up to you, is restricting access to non-delivery resources.

You can restrict access to your resources by IP in Azure, but the challenge is knowing what ranges to whitelist. Having spoken with Sitecore about the documentation around this, they've confirmed the instructions are backwards in how to discover what resources should be allowed in. Well, we'll make it clear now.

You're going to have to whitelist the following ranges when applying IP restriction in Azure:

  1. Sitecore Managed Cloud instances such as Cortex Reporting, SOLR or Azure Search, etc.
  2. Integrations such as SiteImprove.
  3. Application Insights.
  4. Your organization's IP addresses.

You can get the ranges for your integrations and offices, and Application Insights at the Microsoft documentation page here, but the one that requires a bit of work are the Sitecore dependencies.


Discovering IP Addresses Required by Sitecore

The first step in getting these IPs will be understanding what dependencies there are between the Sitecore instances. We'll use a PowerShell script to do this. If you haven't installed the PowerShell AzureRM module on your PC before, you're going to have to do that first with the following commands.

set-executionpolicy unrestricted

then

Install-Module AzureRM -AllowClobber

Once installed, create the file GetConnectionStrings.ps1 with the following script:

param( 
      [Parameter(Mandatory=$True)][string] $subscriptionId,
      [Parameter(Mandatory=$True)][string] $resourceGroupName
     )
      
Login-AzureRmAccount
$env:ScriptLocation = Get-Location
$rootPath = $env:ScriptLocation
Set-AzureRmContext -SubscriptionId $subscriptionId
$resourcesObject=New-Object -TypeName PSObject
$resourcesObject=@{}
$webApps=Get-AzureRmWebApp  -ResourceGroupName $resourceGroupName
    
    foreach($webApp in $webApps)
    {
       $webAppName=$webApp.Name
        $fileName="ConnectionStrings.config"    
        $kuduApiUrl = "https://"+$webAppName+".scm.azurewebsites.net/api/vfs/site/wwwroot/App_Config/$fileName"
    
        Write-Host "Getting Webapp $webAppName credentials"
        
 $resourceType = "Microsoft.Web/sites/config"
 $resourceName = "$webAppName/publishingcredentials"
     $publishingCredentials = Invoke-AzureRmResourceAction -ResourceGroupName $resourceGroupName  -ResourceType $resourceType `
                                 -ResourceName $resourceName -Action list -ApiVersion 2015-08-01 -Force
 
 
         $kuduApiAuthorisationToken = ("Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f 
                $publishingCredentials.Properties.PublishingUserName,   $publishingCredentials.Properties.PublishingPassword))))
    
    
        $filePath="$rootPath\$fileName"
        try{
        Invoke-RestMethod -Uri $kuduApiUrl `
                            -Headers @{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} `
                            -Method GET `
                            -ContentType "xml" `
                            -OutFile $filePath `
                            -ErrorAction SilentlyContinue
            
            }catch{}                
        
        [xml]$xmlResult=Get-Content $filePath 
    
        $webAppObject = New-Object -TypeName PSObject
        $webAppObject | Add-Member -Name 'ConnectionStrings' -MemberType NoteProperty  -Value (New-object System.Collections.Arraylist)
        $webAppObject | Add-Member -Name 'ReferencedWebAppsComponents' -MemberType NoteProperty -Value (New-Object System.Collections.Arraylist)
        $webAppObject | Add-Member -Name 'WebAppName' -MemberType Noteproperty -Value ''
        $webAppObject | Add-Member -Name 'OutboundIpAddresses' -MemberType Noteproperty -Value ''
   
        $webAppObject.WebAppName=$webAppName
        
        Write-Host "Getting web app $webAppName  connection strings for databases and referenced components" -ForegroundColor DarkYellow
        foreach( $connectionStringNode in $xmlResult.connectionStrings.add)
        {
          
          if($connectionStringNode.connectionString.StartsWith("http"))
          {
               $referencedWebApp=[PSCustomObject]@{
               'ConnectionStringName'=$connectionStringNode.name
               'WebAppUrl'=$connectionStringNode.connectionString
               'WebAppName'=$connectionStringNode.connectionString.Replace("https://","").Replace(".azurewebsites.net","")
               }
               if($referencedWebApp.WebAppName -ne $null)
               {
               $j=$webAppObject.ReferencedWebAppsComponents.add($referencedWebApp)
               }
          
          }
          elseif($connectionStringNode.name.StartsWith("reporting.apikey") -and ($webApp.Name.EndsWith("rep") -ne $True))
          {
                $reportingWebApp=$webApps | Where-Object { $_.Name.EndsWith("rep") -and ($_.Name.EndsWith("ma-rep") -eq $false)}
                $repotingWebAppName=$reportingWebApp.ResourceName
                $reportingWebAppURL="https://$repotingWebAppName.azurewebsites.net"
                
                $referencedWebApp=[PSCustomObject]@{
               'ConnectionStringName'=$connectionStringNode.name
               'WebAppUrl'=$reportingWebAppURL
               'WebAppName'=$repotingWebAppName
               }
                             
               if($referencedWebApp.WebAppName -ne $null)
               {
               $j=$webAppObject.ReferencedWebAppsComponents.add($referencedWebApp)
               }
          }
          else {
          
               $sb = New-Object System.Data.Common.DbConnectionStringBuilder
      
              try
               {
      
               $sb.set_ConnectionString($connectionStringNode.connectionString)
      
               $connectionString=[PSCustomObject]@{
               'name'=$connectionStringNode.name
               'initialcatalog'=$sb.'initial catalog'
               'datasource'=$sb.'data source'
               }
                if($connectionString.initialcatalog -ne $null){
                  $i=$webAppObject.ConnectionStrings.Add($connectionString)
                }
      
               }
               Catch 
               {
               }
          }
     }
     
     write-host "Getting web app $webAppName Outbound Ip Addresses" -ForegroundColor DarkYellow 
     $webAppObject.OutboundIpAddresses=(Get-AzureRmWebApp -Name $webAppName -ResourceGroupName $resourceGroupName).OutboundIpAddresses
     $resourcesObject.Add($webAppName,$webAppObject)
   }
   
   $outputFile= $rootPath+"\WebAppConnectionStrings.json"
   $resourcesObject | ConvertTo-Json -Depth 4 | Out-File $outputFile
   Write-Host "Script completed successfuly" -ForegroundColor Green
   Write-Host "Find the results in $outputFile" -ForegroundColor Green

Execute this command (.\GetConnectionStrings.ps1), and you'll be asked for your Azure  subscription ID, resource group name, and username/password. This will generate the file, WebAppConnectionStrings.json which we'll need.

In today's example I'm going to restrict access to the Content Authoring instance, so when I open WebAppConnectionStrings.json, I'm going to look for the entry named mc-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxx-cm. 

When looking at this entry, you'll see ReferencedWebAppsComponents, which has all the resources that must be whitelisted. Search for each item by its WebAppName in this file, get the OutboundIpAddresses, and add them to the list of IPs you've made already.


Applying Your IP Restrictions

Now that you have all of the ranges needed, open your Application Insights and filter to the last 30 minutes. Create the file AutoApplyIpRanges.ps1 from the following script:

Param
(
  [Parameter(Mandatory = $true,Position = 0,
  HelpMessage = 'Please provide subscription Name')]
  [ValidateNotNullOrEmpty()]
  [string]$SubscriptionId,
  [Parameter(Mandatory = $true,Position = 1,
  HelpMessage = 'Please provide resource group Name')]
  [ValidateNotNullOrEmpty()]
  [string]$ResourceGroup,
  # Name of your Web or API App.
  [Parameter(Mandatory = $true,Position = 2,
  HelpMessage='Provide Webapp Name')]
  [ValidateNotNullOrEmpty()]
  [string]$WebAppName,
  [Parameter(Mandatory = $true,Position = 3,
  HelpMessage = 'Please provide Azure User Id')]
  [ValidateNotNullOrEmpty()]
  [string]$AzureUserId,
  [Parameter(Mandatory = $true,Position = 4,
  HelpMessage = 'Please provide Azure Password')]
  [ValidateNotNullOrEmpty()]
  [string]$AzurePassword,
  # Priority value.
  [Parameter(Mandatory=$true,HelpMessage='Provide priority number')]
  [ValidateNotNullOrEmpty()]
  [int]$Priority,
  # WhitelistIp values.
  [Parameter(Mandatory=$true,HelpMessage='Provide list of IPs to whitelist')]
  [ValidateNotNullOrEmpty()]
  [string]$IPList,
  [ValidateNotNullOrEmpty()]
  [Parameter(HelpMessage='Provide Firewall Rule Name')]
  [string]$RuleName='WhiteList'
)
$ErrorActionPreference='stop'
try
{
  function Add-AzureIpRestrictionRule
  {
    Write-Host "Whitelisting $($rule.IPAddress)"
    $ApiVersions = Get-AzureRmResourceProvider -ProviderNamespace Microsoft.Web | 
    Select-Object -ExpandProperty ResourceTypes |
    Where-Object ResourceTypeName -eq 'sites' |
    Select-Object -ExpandProperty ApiVersions
    $LatestApiVersion = $ApiVersions[0]
    $WebAppConfig = Get-AzureRmResource -ResourceType 'Microsoft.Web/sites/config' -ResourceName $WebAppName -ResourceGroup $ResourceGroup -ApiVersion $LatestApiVersion
    $WebAppConfig.Properties.ipSecurityRestrictions = $WebAppConfig.Properties.ipSecurityRestrictions + @($rule) 
    $null=Set-AzureRmResource -ResourceId $WebAppConfig.ResourceId -Properties $WebAppConfig.Properties -ApiVersion $LatestApiVersion -Force 
    Write-Host "Whitelisting $($rule.IPAddress) Success"
  }
  #Login to Azure
  Write-Host "`nLogging into Azure"
  $SecurePwd = ConvertTo-SecureString -String $AzurePassword -AsPlainText -Force
  $Creds = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ($AzureUserId, $SecurePwd)
  Connect-AzureRmAccount -Credential $Creds 
  Write-Host "Logging into Azure Success`n"
  #Set Required subscription
  Write-Host "`nSelecting subscription $SubscriptionId"
  Set-AzureRmContext -SubscriptionId $SubscriptionId
  Write-Host "Selecting subscription $SubscriptionId Success `n"  
   
  $IncrementCounter = 1
  $IPs=$IPList -split ';'
  foreach ($IPAddress in $IPs)
  {
    if($IPAddress -match '\\' -or $IPAddress -eq '' -OR $IPAddress -eq ' ')
    {
      Write-Host "Invalid IP address format $IPAddress, So ignored it"
      Continue
    }
    
    try
    {
      if($IPAddress -match '/')
      {
         $ip=$IPAddress.Substring(0,$IPAddress.IndexOf('/'))
      }
      else
      {
        $ip=$IPAddress
      }
      #Ip address validation
      $null=[IPAddress]$ip
    }
    catch
    {
      Write-Host "Invalid IP address format $IPAddress, So ignored it"
      Continue
    }    
    
    if($IPAddress -match '/')
    {
      $IPAddress=$IPAddress.Trim()
      $rule = [PSCustomObject]@{
        IPAddress = "$($IPAddress)"
        Action = 'Allow'
        Priority = "$Priority"
        Name = "$RuleName-$IncrementCounter"
      }
      $IncrementCounter++
      Add-AzureIpRestrictionRule -ResourceGroup "$ResourceGroup" -AppServiceName "$WebAppName" -rule $rule
    }
    else
    {
      $IPAddress=$IPAddress.Trim()
      $rule = [PSCustomObject]@{
        IPAddress = "$($IPAddress)/32"
        Action = 'Allow'
        Priority = "$Priority"
        Name = "$RuleName-$IncrementCounter"
      }
      $IncrementCounter++
      Add-AzureIpRestrictionRule -ResourceGroup "$ResourceGroup" -AppServiceName "$WebAppName" -rule $rule
    } 
    
  }
}
catch
{
  # get error record
  [Management.Automation.ErrorRecord]$e = $_
  # retrieve information about runtime error
  $info = [PSCustomObject]@{
    Exception = $e.Exception.Message
    Reason    = $e.CategoryInfo.Reason
    Target    = $e.CategoryInfo.TargetName
    Script    = $e.InvocationInfo.ScriptName
    Line      = $e.InvocationInfo.ScriptLineNumber
    Column    = $e.InvocationInfo.OffsetInLine
  }
  
  # output information. Post-process collected info, and log info (optional)
  $info
  Throw $_.exception.message
}

Execute the following command to apply your whitelisted IPs. 

.\AutoApplyIpRanges.ps1 `
-SubscriptionId '[your-subscription-id]' `
-ResourceGroup '[you-resource-group]' `
-WebAppName '[your-app's-name]' `
-AzureUserId '[your-email]' `
-AzurePassword '[your-password]'  `
-Priority 100 -IPList '123.123.123.123/28;456.456.456.456/28' `
-RuleName '[name-for-your-rule]'

A couple of things about this. When you run this script it will automatically create Deny All entry. If you've whitelisted an IP, you obviously need to deny all others, right? Also, Application Insights will hiccup if you're entering a large number of IPs, so just monitor that everything comes back up to 100% availability when it's done. If not, just remove all entries and check what IPs are missing. 

Now, this is how you address one instance, where you likely have many. The good news is the IP ranges are usually the same across all of them. Good luck!