Published on

Deploy an Azure Function App to host a MTA-STS policy file using Bicep template

Authors
  • avatar
    Name
    Jonathan Devere-Ellery
    Twitter

If you have used Azure before you are probably familiar creating ARM templates and using them to deploy resources in Azure, but Bicep is a modern approach on templates in Azure.

The public documentation has been updated to include some instructions on how to to setup either an Azure Static Web App or an Azure Function App for hosting the policy file. I prefer the Function App approach because this does not require us to also configure and setup an Azure DevOps organization.

If you prefer using other services, I included instructions for setting up a Cloudflare Worker for hosting the MTA-STS policy file on my previous post.

I put together a Bicep template that could be used to deploy all the required Function App resources automatically into a Resouce Group that would serve a MTA-STS policy file for Exchange Online mailboxes. This template creates the required App Service Plan, Storage Account, Function App as well as configuring a HTTP trigger to respond at the required URL (/.well-known/mta-sts.txt).

After deployment of the template the only thing that still needs to be configured is adding a Custom Domain and issuing a (free) TLS certificate to the App. The policy file is initially created in testing mode but this will need to be changed to enforced mode to ensure protection to incoming emails start to be applied.

Ensure you have the AZ module installed and are connected to your tenant:

Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force
Connect-AzAccount

Then we can deploy either into a new or an existing Resource Group:

New-AzResourceGroup -Name "MTASTSRG" -Location "westeurope"
New-AzResourceGroupDeployment -ResourceGroupName "MTASTSRG" -TemplateFile ./main.bicep

The template will create all the resources needed to host a Function App, such as a Storage Account, App Service Plan, and then configure the Function App to return a valid mta-sts.txt file on the correct URL. All the resources will be deployed by default using FuncAppMtaSts as a prefix in the name, but this can be customised by adding -resourceNamePrefix <yourPreferedPrefix> switch.

The default behavior is that it deploys a template with MX records for Exchange Online Protection (i.e. mx: *.mail.protection.outlook.com) within the mta-sts.txt file. If you would like to customise this, you can modify this by adding -mxRecord <mx.contoso.com> switch.

The only thing that remains to be configured is to add a custom domain (mta-sts.yourdomain.com), issuing a (free) TLS certificate, and configuring the SSL SNI binding to the app. The remainder of these steps are documented on the Microsoft documentation.

Contents of the Bicep template are shared below, or the latest version will be at my GitHub repo

main.bicep
param location string = resourceGroup().location

@description('Resource name prefix')
param resourceNamePrefix string = 'FuncAppMtaSts'
var envResourceNamePrefix = toLower(resourceNamePrefix)

@description('MX record to be used within MTA-STS policy')
param mxRecord string = '*.mail.protection.outlook.com'

resource StorageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = {
  name: '${envResourceNamePrefix}storage'
  location: location
  kind: 'StorageV2'
  sku: {
    name: 'Standard_LRS'
  }
  properties: {
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
  }
}
var StorageAccountPrimaryAccessKey = listKeys(StorageAccount.id, StorageAccount.apiVersion).keys[0].value

resource HostingPlan 'Microsoft.Web/serverfarms@2022-03-01' = {
  name: '${envResourceNamePrefix}-asp'
  location: location
  kind: 'Windows'
  sku: {
    name: 'Y1'
    tier: 'Dynamic'
  }
  properties: {
    reserved: false
  }
}

resource FunctionApp 'Microsoft.Web/sites@2022-03-01' = {
  name: '${envResourceNamePrefix}-app'
  location: location
  kind: 'functionapp'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    httpsOnly: true
    serverFarmId: HostingPlan.id
    reserved: false
    siteConfig: {
      alwaysOn: false
      numberOfWorkers: 1
      http20Enabled: true
      ftpsState: 'Disabled'
      minTlsVersion: '1.2'
      appSettings: [
        {
          name: 'AzureWebJobsStorage'
          value: 'DefaultEndpointsProtocol=https;AccountName=${StorageAccount.name};AccountKey=${StorageAccountPrimaryAccessKey};EndpointSuffix=core.windows.net'
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: 'dotnet'
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~3'
        }
        {
          name: 'AzureFunctionsJobHost__extensions__http__routePrefix'
          value: ''
        }
      ]
    }
  }
}

resource HttpTrigger 'Microsoft.Web/sites/functions@2022-03-01' = {
  name: '${FunctionApp.name}/HttpTrigger1'
  properties: {
    language: 'CSharp'
    isDisabled: false
    files: {
      'run.csx': '#r "Newtonsoft.Json"\nusing System.Net;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Extensions.Primitives;\nusing Newtonsoft.Json;\n\npublic static async Task<IActionResult> Run(HttpRequest req, ILogger log)\n{\n    log.LogInformation("C# HTTP trigger function processed a request.");\n\n    string responseMessage = "version: STSv1\\nmode: testing\\nmx: ${mxRecord}\\nmax_age: 604800";\n\n    return new OkObjectResult(responseMessage);\n}'
    }
    config: {
      bindings: [
        {
          name: 'req'
          route: '.well-known/mta-sts.txt'
          authLevel: 'anonymous'
          methods: [
            'get'
          ]
          direction: 'in'
          type: 'httpTrigger'
        }
        {
          name: '$return'
          type: 'http'
          direction: 'out'
        }
      ]
    }
  }
}