This post will provide an example of how to integrate Azure API Management, Key Vault and Managed Identities to securely retrieve and use a secret within an API.
GitHub repository: https://github.com/dbarkol/api-management-key-vault-secret
About Managed Identities
In Azure, an Active Directory identity can be assigned to a managed resource such as a Azure Function, App Service or even an API Management instance. Once an identity is assigned, it has the capabilities to work with other resources that leverage Azure AD for authentication, much like a service principal.
These managed identities come with several benefits:
- Simplified responsibilities. Passwords and other sensitive artifacts are no longer required within your code and configuration files.
- Central control plane. Permissions are assigned, revoked and managed from a single control plane using Azure Active Directory.
- Principle of least privilege. Access to resources is based on the need to know and least privilege security principles.
Registering API Management with Active Directory
The quickest way to do this from the Azure portal is by selecting Managed identities from your API Management instance and toggling the register option:
This will register the APIM instance as a resource within the Azure AD tenant.
Access Policies in Key Vault
The next step is to create an access policy within Key Vault so that a secret can be retrieved from API Management. Navigate to Access policies from your Key Vault instance:
Select only the Get operation from the list of Secret permissions:
This is a crucial step, as you want to make sure that you are only selecting the operations that the managed identity needs.
From within the same dialog, choose Select principal, search for your managed APIM instance by its name, and select it:
Don’t forget to click Save to commit the changes:
That’s all that is needed on the management side to connect the dots between API Management and Azure Key Vault with a managed identity. Now it’s time to put everything into practice.
Retrieving a Secret from Key Vault using a Managed Identity
For this scenario we are going to pretend that we have a backend API that requires basic authentication. The password that we’ll need for the header to the backend API is stored as a secret in Key Vault.
When we are done, the high-level flow will look like the following diagram:
Side note: I debated returning the secret from an API Management operation but that defeats the purpose of this whole exercise, which is to responsibly use all these security constructs correctly. If we simply create a wrapper around retrieving a secret and expose it as an API, then we’re doing it all wrong.
In API Management, the front end of the operation that we’ll create, Get Item; is just a GET request to a URL: /item/{id}.
All the magic happens in the inbound processing flow. However, before we jump in, we should review what a request for a secret looks like to Azure Key Vault:
GET {vaultBaseUrl}/secrets/{secret-name}/{secret-version}?api-version=7.0
Reference: https://docs.microsoft.com/en-us/rest/api/keyvault/getsecret/getsecret
This GET request, just like all the management REST APIs on Azure, expects an access token in the header. The best part of having API Management run as a managed identity is that all the work of receiving an access token, adding it to header and providing valid credentials is now done for you.
Retrieving a secret is now comprised of just one send-request policy:
<send-request mode="new" response-variable-name="secretResponse" timeout="20" ignore-error="false"> <set-url>{{vaultBaseUrl}}/secrets/{{secret-name}}/?api-version=7.0</set-url> <set-method>GET</set-method> <authentication-managed-identity resource="https://vault.azure.net" /> </send-request>
In this example, named values are used for the key vault base URL and the name of the secret that you want to retrieve. I use named values quite a bit when there are variables that might change, like an URL or parameter name. These values usually change between environments (QA, UAT, Production, etc.) and this is a great way to isolate those changes.
Your vaultBaseUrl should look like: https://{your-key-vault-name}.vault.azure.net.
The next step is saving the actual value of the secret into a variable for later use. This isn’t required but I typically do this extra step so that I can reference things easier within the inbound request.
<set-variable name="secret" value="@{ var secret = ((IResponse)context.Variables["secretResponse"]).Body.As<JObject>(); return secret["value"].ToString(); }" />
The final step is to make the call to the backend service and use the secret that we just retrieved as part of the authentication:
<!-- Set the password with the secret from Key Vault --> <authentication-basic username="zohan" password="@((string)context.Variables["secret"])" /> <!-- Call the back end --> <rewrite-uri template="?item={id}" /> <set-backend-service base-url="https://httpbin.org/get" />
For completeness, the entire inbound request is as follows (gist link):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<policies> | |
<inbound> | |
<base /> | |
<!– Retrieve the secret from Key Vault using a managed identity –> | |
<send-request mode="new" response-variable-name="secretResponse" timeout="20" ignore-error="false"> | |
<set-url>{{vaultBaseUrl}}/secrets/{{secret-name}}/?api-version=7.0</set-url> | |
<set-method>GET</set-method> | |
<authentication-managed-identity resource="https://vault.azure.net" /> | |
</send-request> | |
<!– Place the secret into a local variable –> | |
<set-variable name="secret" value="@{ | |
var secret = ((IResponse)context.Variables["secretResponse"]).Body.As<JObject>(); | |
return secret["value"].ToString(); | |
}" /> | |
<!– Remove the subscription key from the header –> | |
<set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" /> | |
<!– Use the password in the header –> | |
<authentication-basic username="zohan" password="@((string)context.Variables["secret"])" /> | |
<rewrite-uri template="?item={id}" /> | |
<set-backend-service base-url="https://httpbin.org/get" /> | |
</inbound> | |
<backend> | |
<base /> | |
</backend> | |
<outbound> | |
<base /> | |
</outbound> | |
<on-error> | |
<base /> | |
</on-error> | |
</policies> |
I use httpbin.org as much as I can for testing. It’s a great request and response service that let’s you review all the details about a request and can be a real time-saver.
Now, when I test the API Management endpoint with Postman, I get the following response:
{ "args": { "item": "500" }, "headers": { "Accept": "*/*", "Accept-Encoding": "gzip,deflate", "Authorization": "Basic em9oYW46VGVtcDEyMw==", "Cache-Control": "no-cache", "Host": "httpbin.org", "Postman-Token": "95c3627f-45c0-468a-9fa1-8ef37621d54f", "Request-Context": "appId=cid-v1:066da8fc-5746-4ff2-a2fa-7bc7b3ee3798", "Request-Id": "|c1f4a0a1-8fc4-4aca-9735-40ce9ad74b17.d3bdb1c2.", "User-Agent": "PostmanRuntime/7.15.0" }, "origin": "23.243.40.170, 52.183.61.120, 23.243.40.170", "url": "https://httpbin.org/get?item=500" }
Thank you for reading!