Skip to main content

Using a System-assigned managed identity in an Azure VM with an Azure Key Vault to secure an AppOnly Certificate in a Microsoft Graph or EWS PowerShell Script

One common and long standing security issue around automation is the physical storage of the credentials your script needs to get, whatever task your trying to automate done. The worse thing you can do from a security point of view is store them as plain text in the script (and there are still plenty of people out there doing this) a better option is to do some encryption (making sure you use the Windows Data Protection API) eg https://practical365.com/blog/saving-credentials-for-office-365-powershell-scripts-and-scheduled-tasks/ . Azure also offers some better options with ability to secure the credentials and certificates in RunBooks, so it is just a few clicks in the Gui and some simple code to secure your credentials when using a RunBooks.

In this post I’m going to look at the issues around storing and accessing SSL certificates associated with App only token authentication that you might be looking to use in Automation scripts. This is  more for when you can’t take advantage of Azure Runbooks and need the flexibility of a VM.

In Exchange (and Exchange Online) EWS Impersonation has been around for quite some time, it offers the ability to have a set of credentials that can impersonate any Mailbox owner. With the Microsoft Graph you have App Only tokens which offers a similar type of access with the additional functionality to limit the scope greatly to certain mailboxes and Item types . With App Only tokens you don’t have a username/password combination but a SSL certificate or application secret (the later should be avoided in production). So instead of the concern around the physical security of the username and password combination, your concern now is around the security of the underlying certificate.

One of the most important points is the time to start thinking about the security of the certificate is before you generate it. Eg just having a developer or Ops person generate it on their workstation leaving copies of the certificate file anywhere is the equivalent of the postit note on the monitor. This is where the Azure KeyVault (or AWS KMS) can be used to secure both the creation of the certificate and provide the ongoing storage and importantly Access Control and Auditing. So from the point of the creation of the AppOnly cert you should be able to have an audit trail of who created it and who accessed it. The other advantage of having the cert in a KeyVault is that it also makes it easy for you to have a short expiry on the certificate and automate the renewal process which in tern makes your auth process more secure.

Once the authentication Certificate is stored in the KeyVault then the weakest link can be the authentication method you then use to access the KeyVault. Eg a user account can be granted rights to the KeyVault (which then make that user account the weakest link) or you could use another Application secret or SSL Certificate to secure access to the data plain of the KeyVault. At some point all of these become self defeating from a security point of view as your still storing a credential (especially if you then store that as plain text) and for a persistent threat actor this still leaves you exposed.

One way of getting rid of the storage of credentials is the use of Managed identities where (“in my mind at least”) your trusting the infrastructure where the code is running. The simplest example would be say you create an Azure compute function and give that function a Managed identity, you can then grant that access to the KeyVault and your function can now access the certificate from the KeyVault and then authenticate to Azure and access every Mailbox in your tenant. So now you have placed the trust in the code in the function and the underlying security of the function (eg can the code be exploited, could somebody hack the deployment method being used and replace the code with their own Etc). With a System-assigned managed identity in an Azure VM your doing the same thing but this time the point of trust is the Azure Virtual Machine. So now its down to the physical security measures around the Azure VM which becomes the weakest link. Still not infallible but your security options around securing VM are many, so with good planning and practice you should be able get a balance between flexibility and security.
So lets look at a simple implementation of using a System-assigned managed identity in a Azure VM in a Powershell Script to get a SSL Certificate from a KeyVault and then access the Microsoft Graph using an AppOnly token generated using that certificate.

  1. This first thing you need is an Azure Key Vault where you have enabled auditing https://docs.microsoft.com/en-us/azure/key-vault/key-vault-logging
  2. You need to create an application registration in Azure AD for your app that will be using the SSL cert from the Keystore to generate a AppOnly token and then use Application permissions for the task you want it to perform. https://docs.microsoft.com/en-us/graph/auth-register-app-v2
  3. Make sure you then consent to the above application registration so it can be used in your tenant (The portal now make this very straight forward)
  4. You need an Azure VM where you have enabled System-assigned managed identity https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm
  5. One your Azure VM has a System-assigned managed identity you should be able to grant that secret-permissions so it can access the SSL certificate we are going to store in the KeyVault eg 
  6. Next step is create the Self Signed in the Azure Key Vault using the Azure Portal
  7. At this step you should now be able to access the Self Signed certificate from the Key Vault on your VM with some simple PowerShell code. All you will need is the URL to Key Secret Identifier eg

Them some code like

$KeyVaultURL = "https://xxx.vault.azure.net/secrets/App1AuthCert/xxx99c5d054f43698f39c51f24440xxx?api-version=7.0"
$SptokenResult = Invoke-WebRequest -Uri 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fvault.azure.net' -Headers @{Metadata="true"}
$Sptoken = ConvertFrom-Json $SptokenResult.Content
$headers = @{
'Content-Type'  = 'application\json'
'Authorization' = 'Bearer ' + $Sptoken.access_token    
}

$Response = (Invoke-WebRequest -Uri $KeyVaultURL  -Headers $headers) 
$certResponse = ConvertFrom-Json $Response.Content
With the above example you’ll see the following hard-coded URI

http://169.254.169.254/metadata/identity/oauth2/token

This isn’t something you need to change as 169.254.169.254 is

The endpoint is available at a well-known non-routable IP address (169.254.169.254) that can be accessed only from within the VM
https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service

So the above script uses this local Metadata Service to acquire the access token to access the Azure KeyVault (as the System-assigned managed identity). One you have the certificate raw data from the KeyVault you can then load it into a Typed Certificate Object eg

$base64Value = $certResponse.value
$Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$Certificate.Import([System.Convert]::FromBase64String($base64Value))
The last step is the certificate must be uploaded to the Application registration created in step 2 or added to the application manifest either manually or pro-grammatically. eg the following is an example to produces the required manifest format described here https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials

   
    $SptokenResult = Invoke-WebRequest -Uri 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fvault.azure.net' -Headers @{Metadata="true"}
    $Sptoken = ConvertFrom-Json $SptokenResult.Content
    $KeyVaultURL = "https://gspskeys.vault.azure.net/secrets/App1AuthCert/xxx99c5d054f43698f39c51f24440xxx?api-version=7.0"
    $headers = @{
        'Content-Type'  = 'application\json'
        'Authorization' = 'Bearer ' + $Sptoken.access_token    
    }
    $Response = (Invoke-WebRequest -Uri $KeyVaultURL  -Headers $headers) 
    $certResponse = ConvertFrom-Json $Response.Content
    $base64Value = $certResponse.value
    $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
    $Certificate.Import([System.Convert]::FromBase64String($base64Value))
    $bin = $Certificate.GetCertHash()
    $base64Thumbprint = [System.Convert]::ToBase64String($bin)
    $keyid = [System.Guid]::NewGuid().ToString()
    $jsonObj = @{ customKeyIdentifier = $base64Thumbprint; keyId = $keyid; type = "AsymmetricX509Cert"; usage = "Verify"; value = $base64Value }
    $keyCredentials = ConvertTo-Json @($jsonObj) | Out-File "c:\temp\tmp.key"

This puts the certificate data into a file temporarily which isn’t great you can actually use the Graph API to create an app registration and add the cert data directly which means the cert data never needs to be export/import into a file.

Authenticating with the SSL Certifcate you retrieved from the KeyVault

Once you have the Certificate loaded you can then use the ADAL library to perform the authentication and get the AppOnly access token you can either use in the Microsoft Graph or EWS eg

$ClientId = "12d09d34-c3a3-49fc-bdf7-e059801801ae"
$MailboxName = "gscales@datarumble.com"  
Import-Module .\Microsoft.IdentityModel.Clients.ActiveDirectory.dll -Force
$TenantId = (Invoke-WebRequest -Uri ('https://login.windows.net/' + $MailboxName.Split('@')[1] + '/.well-known/openid-configuration') | ConvertFrom-Json).authorization_endpoint.Split('/')[3]

The ClientId is the ClientId from the application registration in step 2 the following does the Authentication using the configuration information from the above and then makes a simple Graph
request that uses the App Only Access Token that is returned.
$Context = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext("https://login.microsoftonline.com/" + $TenantId)
$clientCredential = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.ClientAssertionCertificate($ClientId,$Certificate)
$token = ($Context.AcquireTokenAsync("https://graph.microsoft.com", $clientCredential).Result)
$Header = @{
   'Content-Type'  = 'application\json'
   'Authorization' = $token.CreateAuthorizationHeader()
}
$UserResult = (Invoke-RestMethod -Headers $Header -Uri ("https://graph.microsoft.com/v1.0/users?`$filter=mail eq '" + $MailboxName + "'&`$Select=displayName,businessPhones,mobilePhone,mail,jobTitle,companyName") -Method Get -ContentType "Application/json").value
return $UserResult

A Gist of the full script can be found here

Popular posts from this blog

Testing and Sending email via SMTP using Opportunistic TLS and oAuth in Office365 with PowerShell

As well as EWS and Remote PowerShell (RPS) other mail protocols POP3, IMAP and SMTP have had OAuth authentication enabled in Exchange Online (Official announcement here ). A while ago I created  this script that used Opportunistic TLS to perform a Telnet style test against a SMTP server using SMTP AUTH. Now that oAuth authentication has been enabled in office365 I've updated this script to be able to use oAuth instead of SMTP Auth to test against Office365. I've also included a function to actually send a Message. Token Acquisition  To Send a Mail using oAuth you first need to get an Access token from Azure AD there are plenty of ways of doing this in PowerShell. You could use a library like MSAL or ADAL (just google your favoured method) or use a library less approach which I've included with this script . Whatever way you do this you need to make sure that your application registration  https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-

The MailboxConcurrency limit and using Batching in the Microsoft Graph API

If your getting an error such as Application is over its MailboxConcurrency limit while using the Microsoft Graph API this post may help you understand why. Background   The Mailbox  concurrency limit when your using the Graph API is 4 as per https://docs.microsoft.com/en-us/graph/throttling#outlook-service-limits . This is evaluated for each app ID and mailbox combination so this means you can have different apps running under the same credentials and the poor behavior of one won't cause the other to be throttled. If you compared that to EWS you could have up to 27 concurrent connections but they are shared across all apps on a first come first served basis. Batching Batching in the Graph API is a way of combining multiple requests into a single HTTP request. Batching in the Exchange Mail API's EWS and MAPI has been around for a long time and its common, for email Apps to process large numbers of smaller items for a variety of reasons.  Batching in the Graph is limited to a m

How to test SMTP using Opportunistic TLS with Powershell and grab the public certificate a SMTP server is using

Most email services these day employ Opportunistic TLS when trying to send Messages which means that wherever possible the Messages will be encrypted rather then the plain text legacy of SMTP.  This method was defined in RFC 3207 "SMTP Service Extension for Secure SMTP over Transport Layer Security" and  there's a quite a good explanation of Opportunistic TLS on Wikipedia  https://en.wikipedia.org/wiki/Opportunistic_TLS .  This is used for both Server to Server (eg MTA to MTA) and Client to server (Eg a Message client like Outlook which acts as a MSA) the later being generally Authenticated. Basically it allows you to have a normal plain text SMTP conversation that is then upgraded to TLS using the STARTTLS verb. Not all servers will support this verb so if its not supported then a message is just sent as Plain text. TLS relies on PKI certificates and the administrative issue s that come around certificate management like expired certificates which is why I wrote th
All sample scripts and source code is provided by for illustrative purposes only. All examples are untested in different environments and therefore, I cannot guarantee or imply reliability, serviceability, or function of these programs.

All code contained herein is provided to you "AS IS" without any warranties of any kind. The implied warranties of non-infringement, merchantability and fitness for a particular purpose are expressly disclaimed.