App Service – Key Vault Vnet Service Endpoint access options explored + NodeJS runtime examples

I was recently drafting recommendations for using Azure Key Vault with App service. While available documentation is excellent and comprehensive it seemed, that I needed to document some overview in order to save time in future. Otherwise I am back at deciphering some of the key configuration options, such as Azure Key Vault Firewall settings again 🙂

Important info about App service Regional VNET integration

Capabilities are very good after all.

While this blog highlights some limitations of regional VNET integration in App Service, I’d recommend that the reader compares these limitations to subscribing full fledged App Service Environment. Features like limiting outbound traffic and reaching private resources inside VNET, can be achieved with other plans than the App service environment -plan only.

For further info check the excellent article at https://docs.microsoft.com/en-us/azure/azure-functions/functions-networking-options

App Service and Key Vault firewall using the ”Trusted Services” option

  • Using Key Vault References for App Service at the moment is not supported when you are calling Key Vault using VNet service endpoint

Currently, Key Vault references won’t work if your key vault is secured with service endpoints. To connect to a key vault by using virtual network integration, you need to call Key Vault in your application code.

https://docs.microsoft.com/en-us/azure/azure-functions/functions-networking-options#use-key-vault-references

1-to-1 Relation between app service and the Subnet

  • The integration subnet can be used by only one App Service plan. What this means is that while you can have multiple web apps /functions enabled for VNET integration on the same App Service Plan, they must all share the same integration subnet
  • This means that App or function running on the app service plan cant be assigned to any other subnets, than the one app service plan is already assigned to
  • Try anything else, and you get ”Adding this VNET would exceed the App Service Plan VNET limit of 1”
    • This is explained in detail in docs issue at @github
The integration subnet can be used by only one App Service plan.https://docs.microsoft.com/en-us/azure/app-service/web-sites-integrate-with-vnet#regional-vnet-integration

Consumption plans

Consumption plans do not support Virtual Network integration required for using VNET Service Endpoints used in this article


Getting to the point? Regional VNET integration

This blog focuses on Regional VNET integration for App Service, which is subject to following main assumptions

  • The Vnet which you select for the app service has to share the same subscription and the region as the App Service Plan (link)
    • The article in the link, also mentions ’Resources in VNets peered to the VNet your app is integrated with’ I haven’t tested if the same region requirement applies here, as VNET peering works across regions.
  • Your target resources in VNET’s must be in same region as your app service
    • Is this applicable to VNET service endpoints? based on my testing calling network restricted Key Vault behind service endpoint worked for app service regardless was key vault in the same region or not. This worked as long as the caller VNET is authorized. I believe this is exception, or that it only includes VNET based resources, not resources behind VNET service endpoints

  • Regional vnet interation enables you to place also NSG rules on outbound traffic from your App Service Function, or Web App
  • Virtual Network integration is only meant for outbound calls from your app into your VNet, or to another resource which is behind Vnet Service Endpoint
  • There is another feature called ’Gateway-required VNet Integration which relies on P2S connections to another regions from gateway enabled VNET’s which is subject to another set of assumptions.

Example scenarios

All testing was done on Azure Key Vault Standard, and Linux based app service plan.

  • App service plan S1 and P1V2
  • All code, apps and secrets are created for testing purposes (run none of this stuff against anything in production)
    • for both web apps and functions
      • Node 12 LTS runtime
      • System assigned managed identity
      • Key Vault is called on specific functions defined in the application code
  • All resources on West Europe
  • App Service and VNET in same subscription and region
  • Key Vault
    • Only allows traffics from authorized VNET’s using VNET service endpoints feature enabled on the source VNET (AppService Integration VNET)

Azure side configuration screencaps

Node JS example code for Linux App Service Plan

calling the Node.JS web app only demonstrates the connectivity to the key vault by fetching a list of secrets and outputting it to the screen (Nobody in their sane mind would list secrets in public website, so don’t use this code in this format against anything on production)

Expected Output from web app example
Expected Output from web app example

Web App

App.js

  • If you test the code, remember to update the Package.JSON to run app.js in main, not the default index.js
  • For both function and web app include request depedency on the Package JSON
  • For the kvOpt variable in code remember to update the fqdn of your key vault (this could also use env.variable, which update in the app settings)
    • Or you could add it as query param to the code if you want to test the samples with multiple key vaults
Query Param for the global KV name (The suffix is the same)
Calling with query Param
hardcoded URL as provided in the example code
var express = require('express')
var app = express()
var {secretsList,getMsitoken,getClientCredentialsToken} = require(`${__dirname}/src/msi`)
var port = process.env.PORT || 8080
console.log(port)
app.get('/home', (req,res) => {
    //console.log(process.env)
    var apiVer = "?api-version=2016-10-01"
    var kvOpt = {
        json:true,
        uri:"https://appservicekvs1.vault.azure.net/secrets/" + apiVer,
        headers:{
           
        }
    }
      
    if (process.env['MSI_ENDPOINT']) {
        console.log('using MSI version')
        getMsitoken()
        .catch((error) => {
            return (error)
        
        }).then((data) => {
            kvOpt.headers.authorization = "Bearer " + data['access_token']
            console.log(kvOpt)
            secretsList(kvOpt).catch((error) => {
                return res.send(error)
            } ).then((data) => {
                console.log(data)
                return res.send(data)
            })
        })
    } else {
        console.log('using local version')
        getClientCredentialsToken()
        .catch((error) => {
            return (error)
        
        }).then((data) => {
            kvOpt.headers.authorization = "Bearer " + data['access_token']
            console.log(kvOpt)
            secretsList(kvOpt).catch((error) => {
                return res.send(error)
            } ).then((data) => {
                console.log(data)
                return res.send(data)
            })
        })
    }
 
})
app.listen(port, () => {
    console.log('listening on', port)
})

MSI.JS

  • Place msi.js in folder called src
  • Populate the options of first function only if you want to test it locally (You have to create your own app registration, and add it to access policy of the Key Vault)
var rq = require('request')
var path = require('path')
function getClientCredentialsToken () {
    return new Promise ((resolve,reject) => {
        var options = {
            json:true,
            headers:[{
            "content-type":"application/x-www-form-urlencoded" 
            }
            ],
            form: {
                grant_type:"client_credentials",
                client_id:"",
                client_secret:"",
                resource:"https://vault.azure.net"
                }
            }
        
            rq.post("https://login.microsoftonline.com/dewired.onmicrosoft.com/oauth2/token",options, (error,response) => {
            
                if (error) {
                    return reject (error)
                }
                Object.keys(response).map((key) => {
                    if (key == "body")  {
                        if (response.body.error) {return reject(response.body.error)} 
                        else if (response.body.access_token) {return resolve(response.body)} 
                        else {return resolve (response.body)}
                    }
                    
                })
               
             }
            )
    })
}
function getMsitoken () {
    return new Promise ((resolve,reject) => {
        var options = {
            json:true,
            uri: `${process.env['MSI_ENDPOINT']}?resource=https://vault.azure.net&api-version=2019-08-01`,
            headers:{
            "X-IDENTITY-HEADER":process.env['IDENTITY_HEADER']
            }
        }
        console.log(options)
        rq.get(options, (error,response) => {
            
            if (error) {
                return reject (error)
            }
            Object.keys(response).map((key) => {
                if (key == "body")  {
                    if (response.body.error) {return reject(response.body.error)} 
                    else if (response.body.access_token) {return resolve(response.body)} 
                    else {return resolve (response.body)}
                }
                
            })
            
        })
    })
}
function secretsList (kvOpt) {
    return new Promise ((resolve,reject) => {
        rq.get(kvOpt,(error,response) => {
              if (error) {
                  //console.log(error)
                    return reject(error)
                }
                Object.keys(response).map((key) => {
                    if (key == "body")  {
                        if (response.body.error) {return reject(response.body.error)} 
                        else if (response.body.access_token) {return resolve(response.body)}
                        else {return resolve (response.body)}
                    }
                    
                })
        })
     }
    
    )
   
}
module.exports={getMsitoken,getClientCredentialsToken,secretsList}

Azure Function

  • MSI.js in the SRC folder is the same as in web app
  • Update the variables (kvOpt) just like in the Web App example
var {secretsList,getMsitoken,getClientCredentialsToken} = require(`${__dirname}/src/msi`)
module.exports = async function (context, req) {
    if (process.env['MSI_ENDPOINT']) {
        console.log('running MSIVersion')
        console.log('using MSI version')
        result = await getMsitoken()
        .catch((error) => {
            return context.res = {
                body:error
            };
        
        })
    
    } else {
        console.log('using local version')
        result = await getClientCredentialsToken()
        .catch((error) => {
            return context.res = {
                body:error
            };
        
        })
    }
    if (result['access_token']) {
        var apiVer = "?api-version=2016-10-01"
        var kvOpt = {
            json:true,
            uri:"https://appservicekvs1.vault.azure.net/secrets/" + apiVer,
            headers:{
                "Authorization": "Bearer " + result['access_token']
            }
        }
        console.log(kvOpt)
        var finalresult = await secretsList(kvOpt)
        .catch((error) => {
            return context.res = {
                body:error
            };
        
        })
        return context.res = {
            body:finalresult
        };
    
        }
};

Related error messages

Having missed any of the regional VNET integration settings, or having misconfigured access policies one might easily see any of the following errors

  1. ”Client address is not authorized and caller was ignored because bypass is set to None”.
    • Caller is not authorized in the firewall list
  2. The user, group or application ’appid=/’ does not have secrets list permission on key vault ’AppServicekvs1;location=westeurope’.
    • Caller is not authorized in the access policies

Till next time!

Br, Joosua

Vastaa

Täytä tietosi alle tai klikkaa kuvaketta kirjautuaksesi sisään:

WordPress.com-logo

Olet kommentoimassa WordPress.com -tilin nimissä. Log Out /  Muuta )

Google photo

Olet kommentoimassa Google -tilin nimissä. Log Out /  Muuta )

Twitter-kuva

Olet kommentoimassa Twitter -tilin nimissä. Log Out /  Muuta )

Facebook-kuva

Olet kommentoimassa Facebook -tilin nimissä. Log Out /  Muuta )

Muodostetaan yhteyttä palveluun %s