Category Archives: Security

Securing Azure Function App

WIP: Work In Progress

There are several assertion regarding how an Azure Function App should be configured according to security best practices. Think of each one as a requirement or policy Azure Function App should be compliant with. Azure Function App setup usually includes other Azure services – like Storage account, Key Vault, Networking etc. Some of the requirements contradict others. Blindly following remediation steps might break function app. Let us think of it holistically and propose an ideal Azure Function App configuration.

Requirements (policies) are:

  • function apps should only be accessible over https
  • function apps should have client certificates (incoming client certificates) enabled
  • storage account public access should be disallowed
  • versioning for Azure Storage should be enabled
  • storage account should use private link
  • storage accounts should restrict network access using virtual network rules
  • storage accounts should prevent shared key access
  • managed identity should be enabled on function apps
  • azure key vault should have purge protection enabled
  • azure key vaults should use private link
  • azure key vault should use RBAC permission model
  • firewall should be enabled on key vault

Step-by-step guide securing Azure Function App

Create an OotB Function App

Let us first build a working function app created “as is”, with all settings left by default, so we could apply best practices later. First, we’d need a new resource groups, then we’d create a function with “App Service” (aka Dedicated) hosting plan:

“App Service” (Dedicated) hosting plan is required as it is the only one that will allow us to satisfy all security best practices under Windows. Alternatives might be (tbc) Container or Flex Consumption – both Linux-based.

I will use PowerShell – so I have selected Code (not container) under Windows and PowerShell 7.4. runtime stack. All the other settings are by default – new auto-created storage, “Enable public access” – On, “Enable virtual network integration” – Off, new auto-created application insights etc.

You can use whatever runtime stack you are comfortable with – C#, Python etc. As this is what we need to ensure function is working before and after we applied all the security best practices. Actual steps to secure the function app are not depending on runtime stack chosen.

Create a simple Function and deploy it to Function App

We`d need a fully working function that access SharePoint. Ideally, I’d create a key vault and save credentials in the key vault. I’d provide access to the key vault for my function app so my code would pull credentials when needed and access Microsoft 365 (you can refer to “Connect to SharePoint via Graph API from Azure Function App” for the detailed step-by-step process), but since we are going to get rid of key vaults and use Managed Identities – I’ll save app secret in the environment variable for now.

Import modules

So, I created a simple OotB PowerShell timer-triggered function and deployed it to My function app. Then I updated requirements.psd1:

# requirements.psd1
@{
    'PnP.PowerShell' = '3.*'
}

and I updated function code itself (run.ps1)

# run.ps1
param($Timer)
$currentUTCtime = (Get-Date).ToUniversalTime()
if ($Timer.IsPastDue) {
    Write-Host "PowerShell timer is running late!"
}
Write-Host "PowerShell timer trigger function ran! TIME: $currentUTCtime"
Write-Host "##############################################"
Write-Host "Check modules installed:"
Import-Module PnP.PowerShell 
Get-Module PnP.PowerShell
Write-Host "Check command available:"
Get-Command -Name Connect-PnPOnline -Module PnP.PowerShell | select Version, Name, ModuleName
Write-Host "##############################################"

Check function invocations, ensure function is running correctly – PnP.PowerShell module is imported and ready to use.

Credentials to access SharePoint

As I mentioned, we will temporary be using environment variables to keep secrets – so we ensure the function does have access to SharePoint. For this we need

  1. register a new application under Entra Id and configure it to access SharePoint
    here is the guide: “Authentication to Microsoft Graph: Azure Registered Apps Certificates and Secrets
  2. put secret in an environment variable
    you can use vscode local.settings.json or update environment variables manually under Azure Function App

VSCode local.settings.json:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME_VERSION": "7.4",
    "FUNCTIONS_WORKER_RUNTIME": "powershell",
    "ORGNAME": "contoso",
    "TENANT_ID": "your-tenant-id",
    "CLIENT_ID": "your-client-id",
    "TMP_SECRET": "This is a secret",
    "ADMIN_URL": "https://admin.microsoft.com",
    "SITE_URL": "https://contoso.sharepoint.com"
  }
}

Finally you should have something like this under environment variables of you Function App:

environment variables under Azure Function App

Access SharePoint from function

Now let us update the function code with actual connections to SharePoint tenant and to Specific site.
I will try to connect to SharePoint via PowerShell module PnP.PowerShell and via plain web-requests calls to Microsoft Graph API.

Add the following (PnP part) to your function code run.ps1:

$orgName = $env:ORGNAME
$tenantId = $env:TENANT_ID
$clientID = $env:CLIENT_ID
$clientSc = $env:TMP_SECRET
$adminUrl = $env:ADMIN_URL
$siteUrl = $env:SITE_URL

#############################################################################################
Write-Host "Let us connect to SharePoint via PnP:"
$connectionToTenant = Connect-PnPOnline -Url $adminUrl -ClientId $clientID -ClientSecret $clientSc -ReturnConnection
Write-Host "Connected to admin site:" $connectionToTenant.Url
$tenantSite = Get-PnPTenantSite -Url $siteUrl -Connection $connectionToTenant
Write-Host "Tenant site title:" $tenantSite.Title
$connectionToSite = Connect-PnPOnline -Url $siteUrl -ClientId $clientID -ClientSecret $clientSc -ReturnConnection
Write-Host "Connected to regular site:" $connectionToSite.Url
$site = Get-PnPSite -Connection $connectionToSite
Write-Host "Site title:" $site.Title

The the direct calls to MS Graph API:


#############################################################################################
Write-Host "##############################################"
Write-Host "Let us get token to connect to Microsoft Graph:"
# Construct URI and body needed for authentication
$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$body = @{
    client_id     = $clientid
    scope         = "https://graph.microsoft.com/.default"
    client_secret = $clientSc
    grant_type    = "client_credentials" 
}
$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
Write-Host "Token request status:" $tokenRequest.StatusDescription
$token = ($tokenRequest.Content | ConvertFrom-Json).access_token
$headers = @{Authorization = "Bearer $token" }

Write-Host "Let us connect to SharePoint via Microsoft Graph:"
$apiUrl = "https://graph.microsoft.com/v1.0/sites/$orgname.sharepoint.com"
$rootSite = Invoke-RestMethod -Headers $Headers -Uri $apiUrl -Method Get
Write-Host "Root site Url:" $rootSite.WebUrl
Write-Host "Root site display name:" $rootSite.displayName
#############################################################################################

Check function invocations, ensure function is working good getting SharePoint site via Microsoft Graph, but failing trying to get SharePoint site via PnP. The reason is PnP use SharePoint API under the hood, and calling SharePoint API require authentication with a certificate (but we have a secret here), though it’s OK to call Microsoft Graph API being authenticated with secret.

We will solve the SharePoint API secret/certificate problem below. Now it’s time to secure function according to the best practices.

Securing Azure Function App

TBC

SelectedOperations.Selected permissions in SharePoint

Microsoft says “Initially, Sites.Selected existed to restrict an application’s access to a single site collection. Now, lists, list items, folders, and files are also supported, and all Selected scopes now support delegated and application modes.”. This article deep-dives into providing and using SelectedOperations.Selected granular permissions to SharePoint:

Lists.SelectedOperations.SelectedProvides application access to a specific list
ListItems.SelectedOperations.SelectedProvides application access to one or more list items, files, or folders
Files.SelectedOperations.SelectedProvides application access to to one or more files or library folders

Set of SelectedOperations permissions is exactly what Microsoft promised a few years ago. And this is great, as we really need granular access to SharePoint sites. I’ve been supporting enterprise SharePoint for more than 10 years now, and I know that it was always a concern when application require access to a list/library or a folder or even document, but admins have to provide access to entire site.

Especially, I believe, this feature becomes more in demand because of Copilot for Microsoft 365. As for now – it’s mostly developers and data analytics who needs unattended application access to SharePoint, but what if regular users powered with m365 Copilot license start creating autonomous agents?

So below is my lab setup, PowerShell scripts and guides with screenshots on how to provide granular (not to entire site but to library/list or folder, or even just one document or list item) permissions to SharePoint and how to use provided permissions.

Admin App

First, we need an Admin App – an app we will use to provide permissions.

The only requirement – this app should have Microsoft.Graph Sites.FullControl.All API permissions consented:

Target Site and Dev Setup

For this lab/demo setup, I have created a team under Microsoft Teams (so it’s a group-based Teams-connected SharePoint site), then test list and test library:

There must be an App Registration for client application – application that will have access to Test-List-01 and Test-Lib-01 only. This app registration should have Microsoft Graph “Lists.SelectedOperations.Selected” API permissions consented:

and I will use Python to access SharePoint programmatically.

At this moment (we have a client app and secret, and “” API permissions, but did not provide for this app access to specific sites or libraries) – we should be able to authenticate to Microsoft 365, but not able to get any kind of data (we can get token, but other call to Graph API would return 403 error – Error: b'{“error”:{“code”:”accessDenied”,”message”:”Request Doesn\’t have the required Permission scopes to access a site.”,):

PowerShell script to provide selectedoperations.selected access for an app to a specific list would be as below. Here we use plain calls to MS Graph API. Full script for your refence is available at GitHub, but here is the essential part:

$apiUrl = "https://graph.microsoft.com/beta/sites/$targetSiteId/lists/$targetSiteListId/permissions"
$apiUrl 
$params = @{
	roles = @(
	    "read"
    )
    grantedTo = @{
        application = @{
            id = $clientAppClientId
        }
    }
}
$body = $params | ConvertTo-Json
$response = Invoke-RestMethod -Headers $Headers -Uri $apiUrl -Method Post -Body $body -ContentType "application/json"

Client Application

I use Python console app as a client application. Link to the code at the GitHub is shared below under References, but the core part of the Python code is (I do not use any Microsoft or other libraries here, just plain requests to Microsoft Graph for authentication and for data):

import requests
import json
from secrets import clientSc, clientId, tenantId, siteId, listId 

# specify client id, client secret and tenant id
# clientId = ""
# clientSc = "" 
# tenantId = "" 

apiUri = "https://login.microsoftonline.com/" + tenantId + "/oauth2/v2.0/token"

body = {
    "client_id"     : clientId,
    "client_secret" : clientSc,
    "scope"         : "https://graph.microsoft.com/.default",
    "grant_type"    : "client_credentials" 
}

try: 
    response = requests.post(apiUri, data=body)
    token = json.loads(response.content)["access_token"]
except:
    print("Error: ", json.loads(response.content)["error_description"])
    exit()

print("Got token: ", token[0:10], "...")
headers={'Authorization': 'Bearer {0}'.format(token)}

# Get specific site list
print("Geting specific site list")
# graph_url = 'https://graph.microsoft.com/v1.0/sites/' + siteId + '/lists/' + listId
graph_url = 'https://graph.microsoft.com/beta/sites/' + siteId + '/lists/' + listId
graphResponse = requests.get(   graph_url, headers=headers )
print(" Response status code: ", graphResponse.status_code)
if graphResponse.status_code == 200:
    list = json.loads(graphResponse.content)
    print(" List display name: ", list["displayName"])

Note.
I’m not sure if it’s a bug or my incorrect setup, but I noticed that if I provide access for the app to the list – app can read site.

TBC…

References

Securing Storage Account in Azure Function

A storage account that is created as part of an Azure Function App out of the box has some configuration tradeoffs that can be considered vulnerabilities. Let’s look at what can be changed to improve the security of the storage account without affecting the functionality of the Azure Function App.

Public (anonymous) Access

Storage account blob anonymous (public ) access should be disallowed. Though having this Enabled does not allow anonymous access to blobs automatically, it is recommended to disable public access unless the scenario requires it. Storage account that support Azure Function App is not supposed to be shared anonymously.

Navigate to your Storage Account at Azure portal, Settings/Configuration, then
for “Allow Blob anonymous access” select “Disabled”:

MS: “When allow blob anonymous access is enabled, one is permitted to configure container ACLs to allow anonymous access to blobs within the storage account. When disabled, no anonymous access to blobs within the storage account is permitted, regardless of underlying ACL configurations. Learn more about allowing blob anonymous access“.

Storage account key access

This is also considered as vulnerability and it is recommended to disable Storage account key access.

NB: (Jan 2025) It is not possible to Set “Allow storage account key access” to Disabled if you are using Consumption or Elastic Premium plans on both Windows and Linux without breaking the function app. For other plans – it’s possible but requires some work.

Set “Allow storage account key access” to Disabled:

MS: “When Allow storage account key access is disabled, any requests to the account that are authorized with Shared Key, including shared access signatures (SAS), will be denied. Client applications that currently access the storage account using Shared Key will no longer work. Learn more about Allow storage account key access

The problem is it seems like the function app ootb is configured to use key access, so just disabling storage account key access breaks the function app.

For the function app to adopt this new setting, you’d need:

  • provide access for the function app to the storage account
  • reconfigure the function app for authorization via Microsoft Entra credentials

Here is the separate article “Function app to access Storage via Microsoft Entra credentials” with the detailed research and step-by-step guide, so here I just summirise it:

Function App Hosting PlanAllow Storage Account Key Access
Flex ConsumptionPossible to disable Storage Account Key Access
Consumption, PremiumNot Possible to disable Storage Account Key Access
DedicatedPossible to disable Storage Account Key Access
Containertbc

References

Azure Key Vault Purge Protection

Azure key vault is something where you can store keys, secrets, certificates that you’d need to access services (e.g. call Graph API).

What if somebody malicious get access to key vault (that is not what we want but we should consider this as possible risk)? Surely leaked secrets is a serious issue (separate topic), but imagine if that somebody also deletes key vault content – if so, we will simply lose this data, which will break the functionality of the solution and our existing systems will stop working.

So enabling purge protection on key vaults is a critical security measure to prevent permanent data loss. This feature enforces a mandatory retention period for soft-deleted key vault contents (e.g. secrets), making them immune to purging during the retention period.

In a nutshell, you’d just select “Enable purge protection” under the key vault Settings/Properties (notice, that once enabled, this option cannot be disabled):

For details, please refer to the following articles:

References

Azure Function app to access Storage via Microsoft Entra credentials

There is a known problem with Azure Function App security configuration. Ootb function app access to storage is configured using shared keys. This is considered as potential vulnerability. Disabling storage account key access breaks the app. This article tells how to reconfigure the app to use Microsoft Entra credentials vs shared keys.

When the function app is created – a Storage Account is created to support function app. By default, storage account has shared keys enabled and function app is configured for shared keys. So we’d need:

  1. Enable function app managed identity
  2. Provide access for the function app managed identity to the storage account
  3. Configure function app to use managed identity

Enabling function app managed identity

It’s done via Azure portal -> function app -> Settings -> Identity:

Providing access for the function app’s managed identity to the storage account

First, you’d navigate to the Azure portal -> Storage Account -> Access Control (IAM)
and Add new role assignment.

then you’d select “Storage Blob Data Owner” role, “Managed identity” your app identity

Configure function app to use managed identity

Responsible app settings are (you can find them under your app Settings -> Environment Variables -> “App settings”:

You can remove “AzureWebJobsStorage” and replace it with “AzureWebJobsStorage__accountName” as per here.

For the WEBSITE_CONTENTAZUREFILECONNECTIONSTRING, unfortunately, it says that this env variable is required for Consumption and Elastic Premium plan apps running on both Windows and Linux… Changing or removing this setting can cause your function app to not start… Azure Files doesn’t support using managed identity when accessing the file share…

Possible error messages

If you have a Function app with Consumption or Elastic Premium plans and completed steps above to disable function storage account key access – your function app will not work. There will be no new invocations. Personally, I was able to observe the following error messages:

“We were not able to load some functions in the list due to errors. Refresh the page to try again. See details” :

We were not able to load some functions in the list due to errors. Refresh the page to try again. See details


Function App with Dedicated Plan

If you have a function app created based on dedicated plan (App Service):

then under function “Environment variables” you’ll see:

e.g. there is a AzureWebJobsStorage (that can be updated) and no WEBSITE_CONTENTAZUREFILECONNECTIONSTRING (than is required).

So, for the function app created with dedicated plan (App Service) – if you followed steps above (provide a role to managed identity, created AzureWebJobsStorage__accountName, removed AzureWebJobsStorage and disabled key access – the function should work.

Function App with Flex Consumption plan

Flex Consumption plan is a new linux-based plan (GA announced Nov 2024), and it looks promising – it’s still consumption, but supports virtual networks and allows fast start (and some more nice features).

What I do not like is it does not support installing dependencies via requirements.ps1 – you have to go with custom modules here (it says: Failure Exception: Failed to install function app dependencies. Error: ‘Managed Dependencies is not supported in Linux Consumption on Legion. Please remove all module references from requirements.psd1 and include the function app dependencies with the function app content).

For our specific needs – disabling storage key access and using function identity to access it’s own storage – I found the following promising: “By default, deployments use the same storage account (AzureWebJobsStorage) and connection string value used by the Functions runtime to maintain your app. The connection string is stored in the DEPLOYMENT_STORAGE_CONNECTION_STRING application setting. However, you can instead designate a blob container in a separate storage account as the deployment source for your code. You can also change the authentication method used to access the container.”

Let us create a function with a hosting option “Flex Consumption plan” (all the other settings are by default) via Azure Portal:

and right away we can see that app is using storage keys by default via environment variables: AzureWebJobsStorage (we know how to deal with) and DEPLOYMENT_STORAGE_CONNECTION_STRING (no description found).

Let us try to create a function app different way (customized as per this). First, we’d create a storage account. When creating a function app – we’d select an existing storage. I did not find any options to select function managed identity and configure the function to use managed identity to access storage account during function app creation wizard.

Let us try to reconfigure the existing app

After this – a system identity was created and the role “Storage Blob Data Contributor” was assigned to this identity to the storage account. Environment variables did not go. Let us disable access keys under storage account – and… and function app stopped working.

Since environment variables are still here – let us blame “AzureWebJobsStorage” and let us do the trick with it – create a new “AzureWebJobsStorage__accountName”, put our storage account name as a value, remove “AzureWebJobsStorage” and restart the app… Drumroll, please! And hooray! The function has started working again!

“Container app environment” – to be tested

tbc…

References

Authorization to Microsoft Graph: Azure Registered Apps API permissions

Being authenticated to Microsoft 365 tenant means Microsoft 365 knows who is trying to get access. To actually be able read/write or manage resource, your app must be Authorized to this resource.

For details – pls refer to MS authorization and Microsoft Graph API permissions. But again, in short in our case that means we need to have an API permission configured for our azure registered app. There are two kinds of API permissions – delegated and application.

Delegated permissions are intended to allow currently authenticated user to have access to the resource. Effective user permissions in this app would be an intersection of user own permissions and app permissions. So if an app have “Sites.FullControl.All” SharePoint delegated API permissions – that does not mean that user will have full control over all sites.

Here is an example of delegated permissions:

Permissions above allow you to search through SharePoint content being authenticated with your personal credentials. In search results you will see only content you already have access to.

Application permissions are what it says – once permissions are configured – application will have access to the resources according to API permissions.

Generally, application permissions allow an app to have access to all resources of the same kind in tenant, e.g. to get one specific groups owners an app must have “GroupMember.Read.All” permission that allows an app to read all tenant groups and their members. There are some exceptions – e.g. for Teams Microsoft developed RSC that allows scoped app access. For SharePoint there is a similar option – “Sites.Selected” API permissions.

API permissions must have an Admin consent. Here is an example of application permissions:

Permissions above allow your app to search all SharePoint content.

References

Microsoft 365 admin center: Manage ownerless Microsoft 365 groups and teams

There is a new feature published at Microsoft roadmap site:

Microsoft 365 admin center: Manage ownerless Microsoft 365 groups and teams

Teams, Outlook groups, Team Sites etc. powered by Microsoft 365 Groups supports two roles: members and owners. Members can collaborate with others in the group through files, emails, messages etc. Owners manage the group membership and monitor content and conversations. When employees leave an organization or switch projects internally, it results in their existing user accounts getting deleted. If such employees were group owners, keeping track of their groups becomes critical to ensure accountability within the organization. We have introduced a new ownership governance policy to help automate the management of ownerless groups by requesting active members to become owners of the group. Admins can define who is eligible for these notifications and configure what notifications and how often these notifications are sent to active group members. Users, who are members of the ownerless groups can simply accept or decline request via the actionable email message.

  • Feature ID: 180749
  • Added to roadmap: 10/10/2023
  • Last modified: 10/10/2023
  • Product(s): Microsoft 365 Admin Center
  • Cloud instance(s): GCC
  • Platform(s): Web
  • Release phase(s): General Availability


But based on the feature description – all looks exactly as what we already have for years as “Microsoft 365 ownerless groups policy” which you can configure under Microsoft 365 Admin Center -> Settings -> Org settings -> Microsoft 365 groups

More on Microsoft 365 ownerless groups

Massive Microsoft 365 groups update with PowerShell

What if you need to bulk update Microsoft 365 groups membership e.g. to add a group owner or member for tens of thousands m365 groups? Iterating through groups one-by-one is unproductive and could take days. Can we do it faster? Here is what I found.

In my case, it was Microsoft 365 ownerless groups policy implementation for large tenant… Skipping details – I needed to update ownership for 10,000 Microsoft 365 groups and I was looking for a best/fastest possible option maybe some kind of bulk update or with multiple threads. And I figured out that the fastest way is to use PnP.PowerShell that calls Microsoft Graph API but run it against list of groups with PowerShell parallel trick. Here is the sample PowerShell code:

$groups | ForEach-Object -Parallel {
    $owner = "newGroupOwnerUPN@contoso.com"
    Add-PnPMicrosoft365GroupOwner -Identity $_.Id -Users $owner
} -ThrottleLimit 50

That worked for me perfectly and it took ~8 seconds per 1,000 groups.