Using Key Vaults to Encrypt/ Decrypt data between automation stuff

I often invoke one automation “thingy” from another (e.g. an Automation Account Runbook from a Logic App, or a Logic App from an Azure Function). And in most of these cases I want two things: 1) I want the thing I am calling to be “invokable” only the thing I want calling it; and 2) if I am passing sensitive data around I’d like to keep it secure.

What I mean by the first is that say I have a Runbook that does something sensitive. If it’s running on a Hybrid Runbook Worker I can lock it down from any changes etc. by signing it and enforcing that as a requirement. But what about anyone with rights to the Automation Account manually running the Runbook. Typically there’s no harm but when it comes to sensitive things I’d like to add as many layers as possible such that even if someone’s account gets compromised and they have Runbook run rights they’d still be unable to run it manually. Maybe it’s a Runbook that create a new user account on-prem and syncs to O365 – last thing I want is anyone using this loophole to create new accounts.

Similarly say the Runbook is usually called from a Logic App and it creates the account and passes a password back to the Logic App. Usually I pass the password back in plain text but again would be nice if I could make it more secure.

How to achieve these is something I had been thinking about for a while and some time ago I came up with a neat idea. Never got around to blogging it so I’ll try and explain it here. It’s very simple, and that’s the beauty of it.

Key Vaults

The key is Key Vaults. 🔐

The Key Vault connector in Logic Apps (and Flows too but its a Premium connector) can do encryption and decryption of data with a key. (Interestingly you can’t set something in a Key Vault from a Logic App – which is what I had been searching for when I stumbled upon this).

Similarly one can decrypt and encrypt using Azure REST API. (Not via any cmdlet, only via this API – but thankfully doing this is way easier nowadays; more later).

Generate a new Key in the Vault. I am going with RSA 4096.

The Idea

If I want the Runbook to only run when invoked via the Logic App (and I have it locked down from any changes to the code via something like signing) what I can do is set some sort of “authentication code” as a mandatory input. Say I am passing a Username string to the Runbook. In addition to that I’d also pass this “authentication code” which is an encrypted version of the same Username. The encryption will be done by the Logic App using a Key in the Key Vault and the only thing the Runbook can do is decrypt using the same Key in the Key Vault. If the decrypted text matches what it got as input then it knows it was invoked via the Logic App as no one else has access to the Key Vault. (Of course this hinges on no one being able to make changes to the Runbook to modify the code to remove this check… but the idea is to have layers, one can never be fool proof).

Similarly if I am passing a sensitive info like Password from the Runbook to Logic App. I could encrypt it with a Key in the Key Vault and only the Logic App could decrypt it.

Key Vaults have zero cost except for operations so I can make multiple Key Vaults – one where the Logic App has encrypt and Runbook has decrypt rights; and another where its the other way around.

Access Policies

As an example for this blog post I have a Logic App and Runbook. The Logic App will encrypt, the Runbook will decrypt.

So I will give the Managed Identity of the Logic App and Runbook the appropriate rights. The Logic App (create-user, below) needs the Get, List, and Encrypt rights (not sure if Get, List are really needed but it makes it easy when setting things up). The Automation Account (UserCreate, below) needs Decrypt.

Logic App

Create a connection to the Key Vault using the Managed Identity (seriously, don’t even think of anything else. Use Managed Identities always everywhere 🙂).

Select the Key, the algorithm, and the variable to encrypt.

Now we can pass this to the Runbook as a parameter.

Runbook

The Runbook is not as straightforward as the Logic App. 🙄

For one, to decrypt we need to do a REST API POST to  {vaultBaseUrl}/keys/{key-name}/{key-version}/decrypt?api-version=7.2 with a specific set of body parameters.

According to the docs the key-version is required in the Uri but in my testing I found one can skip it and it will select the latest version of the key (which is what I want). In my case the Uri is thus https://randomkeyvault9099.vault.azure.net/keys/key1/decrypt?api-version=7.2.

Obviously one cannot simply send a POST to this Uri and expect to decrypt something… there are additional steps to using the Azure REST API. This used to be a whole lotta bunch of steps in the past (including registering an App Registration in Azure AD) but nowadays there are useful cmdlets like Invoke-AzRestMethod that can reduce this to a single cmdlet. (Check out this blog post for the old and new way). I can’t use Invoke-AzRestMethod as that is designed to send requests to the Azure Management endpoint only while I have to make a request to the Key Vault directly. Moreover I don’t want to make an App Registration etc. as my Runbook is what has access to the Key Vault and I want to connect using that Identity.

Ideally what I need is: 1) connect to Azure with this Managed Identity (from the Runbook of course), 2) somehow get a token for this and 3) make a REST API call to the Key Vault. The first and third steps are easy, how to do the second?

Turns out there’s a cmdlet for that too! 🙂 Get-AzAccessToken

By default this gets me tokens using which I can authenticate to the Azure Management endpoint (this cmdlet calls it the ARM endpoint – same thing I think) but I can specify other endpoints either via name (AadGraph, AnalysisServices, Arm, Attestation, Batch, DataLake, KeyVault, MSGraph, OperationalInsights, ResourceManager, Storage, Synapse) or Url (e.g. https://graph.microsoft.com/). Nice! Notice KeyVault is already in the list so this is how I can connect with a Managed Identity and get a token for it:

Simple!

Now I can make the REST API call to decrypt.

Ideally one would simply convert this Base64 to string.

But that gives an error:

The trick to this is to add some padding. The Base64 needs to be divisible by 4 so we add some padding as needed. (Thanks to this and this blog post where I learnt about this issue as I was Googling on the error).

And now I can add a test in the Runbook to see if the decrypted text matches what I expect and exit if it does not:

The Runbook looks like this in its entirety:

Logic App

Now I can modify the Runbook connector with the parameters it needs.

And that’s it! You can see the encrypted text being passed in as a Runbook input:

No one can manually start the Runbook because it needs the second parameter:

And if I give some random second parameter…

it fails:

The 400 error is because the encrypted text is not really encrypted and so the Key Vault complains. If I put in some genuine encrypted data there’s no complaints but it still fails:

A similar process can be used to encrypt data in the Runbook and decrypt at the Logic App.