The other day I was talking to a colleague and evaluating options for modernizing a bunch of on-premises scripts. We discussed Azure Automation Accounts with PowerShell Runbooks as an option when I heard the feedback “it’s hard to email from a runbook” so I dove in, read a number of articles and got to work as I was curious how this can be done in a secure and modern fashion. In this post I’ll walk through the concept and how you can build it yourself using Azure Automation, PowerShell, Microsoft Graph and Keyvault.

Azure Automation Account - Runbooks Azure Automation Account - Runbooks

Goals and concept

The goal was pretty clear. We wanted to send an email from a PowerShell runbook without storing/maintaining SMTP credentials and not rely on an open SMTP relay (boooo).

  1. Has to use Azure Automation - PowerShell runbook.
  2. Can’t have credentials stored in the script.
  3. Shouldn’t rely on SMTP servers or relays.
  4. No on-premises or IaaS dependencies.
  5. Minimize or no usage of 3rd party services like SendGrid.

The concept for this is actually fairly simple. This will require a Microsoft 365 tenant with mail-enabled users and use of the Microsoft Graph API to interact with Exchange Online / email services. Using Azure AD, we’ll create an App Registration with Credentials (this allows us to interact with the Microsoft Graph in a secure scripted manner). To send an email, permissions must be set using the Graph API permissions feature of the Azure AD App Registration. We’ll use an Azure Key Vault to store necessary secrets or credentials securely.

Basic overview of the email from Runbook solution Basic overview of the email from Runbook solution

When we look at the chart above, we see the Azure AD App registration registering the needed permissions in the Graph API and store it’s credentials in the Key Vault. Using a bit of PowerShell code and a Automation Account RunAs account, we can retrieve the credentials from the Key Vault. Next, we call the Microsoft Graph API to retrieve a token and use that token to send an email.

Setting up the Azure AD App

Let’s get to work! First we go to Azure AD and look up App Registrations.

From here we’ll chose New Registration, give it a name and select Supported Account Types (by default, the first is more than enough).

After it’s created, open it up and check out the API permissions section. From here we’ll grant this app the permission to access the Microsoft Graph API and send emails on behalf of users.

Select “Add a permission” and look for the Microsoft Graph on top. Search for send and check “Send mail as any user”.

You’ll notice that the new permission has the status “Not granted”. You can fix this by clicking the “Grant admin consent” button above.

Next, we need a way to access this app (and the API permissions it now has). We do this by going over to the Certificates & secrets section. Think of this like a username and password. By knowing the application ID and the secret, we can call on this app from our own custom code. Select “New client secret” and add a client secret.

It’ll show you the new secret in the overview page. Be sure to copy the new secret as it will only be shown once.

Finally on the overview page, you’ll find the “Application (client) ID” and “Directory (tenant) ID”. Jot those as well as you’ll be referring to them later.

Automation Account

Next up we’ll setup an Automation Account. You may already have this but be sure to check if it has a proper RunAs account. Follow these steps if you want a new Automation Account. Go to the marketplace and select Automation. Fill in the details and select Yes on Create RunAs account. Finally remember the name of the RunAs account by opening the new Account and going to RunAs. We’ll refer to this later when we create the Key Vault.

Key Vault

To securely store the app-credentials, we’ll create a Key Vault. Like the name implies, it’s a security service that allow for finer control over sensitive data like logins, certificates etc. The default settings when creating a new Key Vault are fine. Under Access Policy we’ll add the name of the Azure Automation Account RunAs account.

When we’re adding the RunAs account (under select principal), we only need permissions to get a secret. Leave key and certificate permissions to 0 selected.

Leave the rest to default and create the vault. Open it and go to the secrets section. Here we’ll add the Azure AD App Registration Client Secret we noted down earlier. Click the Generate/Import option and select upload option Manual. Give it a name (we’ll refer to this in our PowerShell script) and fill the Azure AD App Registration Client Secret in Value.

Just to recap the steps we’ve taken thus far. We’ve created:

  1. An Azure AD App Registration and we gave it permission to send emails.
  2. An Azure Key Vault to safely store the access credentials to the Azure AD App.
  3. An Automation account to host our PowerShell script and given it permission to access the Key Vault.

PowerShell Runbook in Azure Automation

Next up is the script. In order to access the Key Vault from our PowerShell Runbook, we need PowerShell modules. Without these modules, PowerShell won’t know how to access and talk to the Azure resources. In the Auotmation Account, go to Modules. Click Add a module and add the Az.Accounts and later the Az.KeyVault (in that order, be patient, adding the first takes a while).

Having imported these modules, we can now finally add a new Runbook. Select the Runbooks section, Create a runbook and select the type PowerShell. From the new RunBook, click the Edit button on the top row.

Paste the following code below and click the Save button. You can directly test your new script using the Test Pane. That’s it, you’ve just sent an email from an Azure Automation PowerShell Runbook using an Azure AD App with Graph API permissions secured by an Azure Key Vault!

SecureEmailDemo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
## PowerShell Runbook for Azure Automation to send mail securely using Key Vault, Azure AD App and Microsoft Graph API ##
## MartijnBrant.net ##

###### Change these values ######

	# Name of your Key Vault
	$KeyVaultName = "SecureEmailDemo"

	# Name of the secret in your Key Vault
	$KeyVaultSecretName = "AADAppSecret"

	# The Application ID of your Azure AD App Registraion
	$client_id = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

	# The Tenant ID (can be found at Azure AD App Registration
	$tenant_id = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
	
	# The From Address needs to be a valid email address of an Exchange-Online/Office 365 mail-enabled user
	$fromAddress = '[email protected]'

	# Who should we email
	$toAddress = '[email protected]'
	
	# The mail subject and it's message
	$mailSubject = 'This is a test message from Azure via Microsoft Graph API'
	$mailMessage = 'This is a test message from Azure via Microsoft Graph API'


###### Don't change below ######

Write-Verbose -Message 'Importing Modules...'
Import-Module Az.Accounts
Import-Module Az.KeyVault

Write-Verbose -Message 'Connecting to Azure using Automation Account RunAs Account...'
$ConnectionName = 'AzureRunAsConnection'
try
{
    # Get the connection properties
    $ServicePrincipalConnection = Get-AutomationConnection -Name $ConnectionName      

    $null = Connect-AzAccount `
        -ServicePrincipal `
        -TenantId $ServicePrincipalConnection.TenantId `
        -ApplicationId $ServicePrincipalConnection.ApplicationId `
        -CertificateThumbprint $ServicePrincipalConnection.CertificateThumbprint 
}
catch 
{
    if (!$ServicePrincipalConnection)
    {
        # You forgot to turn on 'Create Azure Run As account' 
        $ErrorMessage = "Connection $ConnectionName not found."
        throw $ErrorMessage
    }
    else
    {
        # Something else went wrong
        Write-Error -Message $_.Exception.Message
        throw $_.Exception
    }
}


Write-Verbose -Message 'Retrieving value from Key Vault...'
$KeyVaultSecretValue = (Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name $KeyVaultSecretName).SecretValueText

Write-Verbose -Message 'Getting the secret...'
$secret = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name $KeyVaultSecretName
$ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret.SecretValue)
try {
   $secretValueText = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)
} finally {
   [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
}

Write-Verbose -Message 'Getting a token from Graph...'
$client_secret = $secretValueText
$request = @{
        Method = 'POST'
        URI    = "https://login.microsoftonline.com/$tenant_id/oauth2/v2.0/token"
        body   = @{
            grant_type    = "client_credentials"
            scope         = "https://graph.microsoft.com/.default"
            client_id     = $client_id
            client_secret = $client_secret
        }
    }
$token = (Invoke-RestMethod @request).access_token


# Build the Microsoft Graph API request
$params = @{
  "URI"         = "https://graph.microsoft.com/v1.0/users/$fromAddress/sendMail"
  "Headers"     = @{
    "Authorization" = ("Bearer {0}" -F $token)
  }
  "Method"      = "POST"
  "ContentType" = 'application/json'
  "Body" = (@{
    "message" = @{
      "subject" = $mailSubject
      "body"    = @{
        "contentType" = 'Text'
        "content"     = $mailMessage
      }
      "toRecipients" = @(
        @{
          "emailAddress" = @{
            "address" = $toAddress
          }
        }
      )
    }
  }) | ConvertTo-JSON -Depth 10
}

Write-Verbose -Message 'Sending mail via Graph...'
Invoke-RestMethod @params -Verbose

Write-Verbose -Message 'All Done!'