Securely store and access secrets in Azure KeyVault from Docker-based App Service

Kevin Le
10 min readAug 27, 2021

--

Storing secrets such as database passwords, API keys, tokens, et cetera in .env files or appsettings.json or similar config files are not very secure. This is probably universally-known and requires no further elaboration. If you work with Azure as your cloud platform and want to learn how to do programmatically access secrets that are securely stored, you’ve come to the right place. In this post, we will learn how to:

  1. Properly store such secrets in Azure KeyVault.
  2. Then we will write a simple App Service in NodeJS/Express and again in ASP.NET WebAPIthat programmatically access these secrets.
  3. Whether you do your development work on Windows and Linux, I will cover both. We will learn how to programmatically access these secrets when we run the App Service locally on both Windows and Linux.
  4. Then we will create a Docker container for each App Service (one for NodeJS/Express and one for ASP.NET WebAPI). We must ensure that the Docker container still work and can access the secrets.
  5. Finally we do more configuration to make sure everything must still work as the App Service is deployed to Azure.

Let’s continue, but I’d like to point out that the sections below do not necessarily correspond to the bullet points above. The sections are just merely there for organizational purposes.

1. Azure KeyVault

Login to Azure Portal, search for Key Vault and click on the + Create button to create a new key vault.

On the next page, on the right menu, click on Settings -> Secrets and then + Generate/Import.

Proceed with the next screen to create a secret which is self-explanatory. Once done as shown in the above picture, DatabasePassword and DatabaseUserId are 2 secrets created in the key vault.

So at this point, I have created a key vault with the name MyStageAzureSecretVault (The picture above didn’t show this name, but let’s assume that’s the name I gave it) and a couple of secrets namely DatabasePassword and DatabaseUserId. The code we’ll write later will make reference to these names.

2. Write NodeJS/Express App Service

Let’s write a simple Hello World NodeJS app that uses Express API. To access the secrets stored in Azure KeyVault, we will need a couple of npm packages: @azure/identity and @azure/keyvault-secrets

We can proceed with the installation

$ npm i express @azure/identity @azure/keyvault-secrets

Now the code:

Of course if we run this now, the program will error out because we have not done any configuration except writing this code. Otherwise, if anyone knows our vault URL https://MyStageAzureSecretVault.vault.azure.net and the secrets name DatabaseUserId and DatabasePassword, they would be able to access our secrets.

Note: make sure if you use Azure CLI, you have to logout from it first (az logout) to make sure that the program errors out for now.

3. Write ASP.NET App service

We will stick with the barebone ASP.NET Core WebAPI app. Run

$ dotnet new webapi

Similarly to the NodeJS/Express app above, we will need to import some nuget packages.

$ dotnet add package Azure.Identity --version 1.4.1
$ dotnet add package Azure.Security.KeyVault.Secrets --version 4.2.0

By the way, if you work with both languages, you can see the parallel between the npm and nuget packages:

@azure/identity vs Azure.Identity
@azure/keyvault-secrets vs Azure.Security.KeyVault.Secrets

No code change is required anywhere except for the following code we that will add in the file Startup.cs.

Again, run and this program will fail.

4. Local Dev Account

To programmatically successfully access the secrets such DatabasePassword and DatabaseUserId in the above example when we run the App Service locally, we must first create a Local Dev account. This Local Dev Account plays no role once the App Service is later deployed on Azure, but it is crucial for development work such as running the App Service on development computers.

To create Local Dev Account, we will register it as App Registration.

In the next page, just simply enter the name Local Dev Account and choose Accounts in this organizational directory only (Obex only — Single tenant)

Once the Local Dev Account has been created, there are 3 IDs that we have to carefully note. The first 2 are in the Overview page: Client Id and Tenant Id. These are shown in the next picture.

The 3rd ID is actually the Client Secret that we need to make note of. If we go to the Certificates & secrets page, click on the + New client secret, Azure will generate a secret. We must save this immediately as the secret is only shown once.

To summarize this step, we have created a Local Dev Account and take note of 3 IDs. Let’s refer to them as: AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_CLIENT_SECRET.

5. Grant Local Dev Account Permission to Key Vault

In Azure Portal, let’s go back to MyStageAzureSecretVault blade, click on Access policies on the left menu and + Add Access Policy.

In the next page, go to Secret permissions and select Get and List. You can select more but that’s all that’s required.

Next, click on None selected under Select principal. Then search by typing Local Dev Account in the box as shown. Finally click on Select button on the bottom right.

What we just did is equivalent to granting Local Dev Account the Get and List permissions on all secrets stored in MyStageAzureSecretVault. The secrets in our example are DatabaseUserId and DatabasePassword.

6. Sign on with Local Dev Account on Windows Dev computer

BothNodeJS/Express and ASP.NET WebAPI App Services errored out earlier as expected when we ran them. If you do development work on Windows, there are 3 solutions to address our problem. Note that all 3 solutions work for both NodeJS/Express and ASP.NET WebAPI.

Using Environment Variables

Having said there are 3 solutions, there’s only 1 that work for both running locally and running in Docker container on local computer. So I will focus on that approach. I will briefly mention the other 2 at the end.

Remember the 3 Ids we took note earlier AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_CLIENT_SECRET

We configure them as Environment Variables

7. Sign on with Local Dev Account on Linux Dev computer

On Linux, bash shell, we will add the following to ~/.bashrc file. Please refer to documentation if you are using other shells.

export AZURE_TENANT_ID="..."
export AZURE_CLIENT_ID="..."
export AZURE_CLIENT_SECRET="..."

8. Rerun on Dev computers

Rerun on both Linux and Windows Dev computers and bothNodeJS/Express and ASP.NET WebAPI App Services should work.

9. Create Docker containers and run on local dev computers

When we run the above App Services on Docker containers, the Environment variables on the host computers (Linux and Windows) are no longer visible to them at run-time. So the solution is to inject the values for these 3 Ids at the time we instruct Docker to build the image.

As we will see, when we later deploy to Azure (Azure Container Registry to be exact), the file docker-compose.yml will be used to build the image for production. To run on Docker on local dev computers, we will create docker-compose.dev.yml instead.

But first let’s talk about the Dockerfile. There’s nothing special about the Dockerfile so something like the following (which I copied from https://nodejs.org/en/docs/guides/nodejs-docker-webapp/) should work.

FROM node:14
WORKDIR /usr/src/app
COPY package*.json ./

RUN npm install
# If you are building your code for production
# RUN npm ci --only=production

# Bundle app source
COPY . .

EXPOSE 8080
CMD [ "node", "server.js" ]

Let’s hold off on docker-compose.yml until we discuss about deployment to production. For now the docker-compose.dev.yml will need to be like:

version: '3.4'
services:
app:
build:
context: .
dockerfile: ./Dockerfile
ports:
- 8000:8080
environment:
AZURE_TENANT_ID: ${AZURE_TENANT_ID}
AZURE_CLIENT_ID: ${AZURE_CLIENT_ID}
AZURE_CLIENT_SECRET: ${AZURE_CLIENT_SECRET}
env_file: .env
command: sh -c 'npm run start'

Obviously, the most important lines are the 3 Ids under the environment section which we have seen over and over again.

To run/build and stop in dev, we run the following commands respectively:

docker-compose -f docker-compose.dev.yml up --build
docker-compose -f docker-compose.dev.yml down

For ASP.NET WebAPI, the same idea holds and I won’t include here. We need to setup the environment variables: AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET. Again,they must be under the environment section as in the case of NodeJS/Express.

10. Create Docker containers for Production

We will create a different Azure key vault named MyProdAzureSecretVault which is similar to the one we created earlier MyStageAzureSecretVault. The difference are as follow:

  1. MyProdAzureSecretVault stores DatabaseUserid and DatabasePassword secrets which point to Production Database (and other keys, tokens, et cetera related to Production resources)

2. We won’t grant Local Dev Account any permission to touch anything on MyProdAzureSecretVault.

AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET are associated with Local Dev Account which is only used for Dev. So we need to write the docker-compose.yml file that are free of them. Here’s a docker-compose.yml file:

version: '3.4'
services:
app:
build:
context: .
dockerfile: ./Dockerfile
ports:
- 80:4001
command: sh -c 'npm run start'

So if you’re not familiar with running a Docker-based App Service on Azure, you might find this article useful.

11. Managed Identity for App Service

I am skipping some steps here and picking up where you are assumed to have already deployed your app image to an Azure Container Registry (ACR), already created an App Service and have the App Service pulling the image from the ACR for continuous deployment.

On Azure portal, go to your App Service Identity blade, and turn System assigned Status to On just like the picture below depicts.

12. Grant App Service Managed Identity Permission to Key Vault

As in step 5 above, we perform the similar process in this step. Except here, we want to grant the App Service Managed Identity the Get and List permissions to the key vault MyProdAzureSecretVault.

13. Summary

Here are what we did:

  • We create a Local Dev Account by doing App Registration in Azure Portal. We note the Client Id, Tenant Id and Client Secret associated with this Local Dev Account. We will refer to them as AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_CLIENT_SECRET later.
  • We grant it the Get and List permissions to the secrets stored in the stage key vault MyStageAzureSecretVault.
  • We write code in JS and C#. In the JS code, we import 2 npm packages: @azure/identity and @azure/keyvault-secrets. In C# code, we import Azure.Identity and Azure.Security.KeyVault.Secrets nuget packages. These are the essential libraries that make it work.
  • On Windows, we use the Environment Variables editor in Control Panel to set the AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_CLIENT_SECRET variables. On Linux, we export them in ~/.bashrc.
  • To create Docker containers, we have a separate docker-compose.yml and docker-compose.dev.yml for production and dev, respectively. We don’t do anything special fordocker-compose.yml, but for docker-compose.dev.yml, we set the AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_CLIENT_SECRET in the environment section.
  • When we deploy to production, we enable Managed Identity for the App Service. Then we grant the App Service the same Get and List permissions to the key vault MyProdAzureSecretVault.
  • The code base, both NodeJS/Express and ASP.Net WebAPI remains unchanged and is completely decoupled from the configuration of the resources where they need to access.
  • Production is completely secure because using Managed Identity of App Service eliminates the need for us to keep any Id or token in any .env or config files.
  • Running on Dev or Docker container on dev computers still require us to keep the AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_CLIENT_SECRET in either the Envionment Variables or ~/.bashrc (as there’s no way to get around it). But by maintaining 2 key vaults: MyStageAzureSecretVault (which stores secrets to stage resources) and MyProdAzureSecretVault (production resources), the danger of exposing production data is mostly eliminated.

14. Other points

Instead of using Local Dev Account and its associated AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_CLIENT_SECRET, we could also use Azure CLI, particularly running the az login command using some Admin account or an Azure Active Directory account that was granted the same Get and List permissions.

Another approach is using Visual Studio 2019 or 2022 and go to Tools | Options | Azure Service Authentications and login with an Admin account or an Azure Active Directory account that was granted the same Get and List permissions.

Both of these approaches are not flexible enough. The first one is not Docker friendly and the second one, not only it is not Docker friendly, it also does not work under Linux.

--

--

Kevin Le
Kevin Le

Written by Kevin Le

Driven by passion and patience. Read my shorter posts https://dev.to/codeprototype (possibly duplicated from here but not always)