Securely store and access secrets in Azure KeyVault from Docker-based App Service
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:
- Properly store such secrets in
Azure KeyVault
. - Then we will write a simple
App Service
inNodeJS/Express
and again inASP.NET WebAPI
that programmatically access these secrets. - 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. - Then we will create a Docker container for each
App Service
(one forNodeJS/Express
and one forASP.NET WebAPI
). We must ensure that the Docker container still work and can access the secrets. - 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:
MyProdAzureSecretVault
storesDatabaseUserid
andDatabasePassword
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 doingApp Registration
in Azure Portal. We note the Client Id, Tenant Id and Client Secret associated with thisLocal Dev Account
. We will refer to them asAZURE_CLIENT_ID
,AZURE_TENANT_ID
andAZURE_CLIENT_SECRET
later. - We grant it the
Get
andList
permissions to the secrets stored in the stage key vaultMyStageAzureSecretVault
. - We write code in
JS
andC#
. In theJS
code, we import 2npm
packages:@azure/identity
and@azure/keyvault-secrets
. In C# code, we importAzure.Identity
andAzure.Security.KeyVault.Secrets
nuget
packages. These are the essential libraries that make it work. - On Windows, we use the
Environment Variables editor
inControl Panel
to set theAZURE_CLIENT_ID
,AZURE_TENANT_ID
andAZURE_CLIENT_SECRET
variables. On Linux, we export them in~/.bashrc
. - To create Docker containers, we have a separate
docker-compose.yml
anddocker-compose.dev.yml
for production and dev, respectively. We don’t do anything special fordocker-compose.yml
, but fordocker-compose.dev.yml
, we set theAZURE_CLIENT_ID
,AZURE_TENANT_ID
andAZURE_CLIENT_SECRET
in theenvironment
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
andASP.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 theAZURE_CLIENT_ID
,AZURE_TENANT_ID
andAZURE_CLIENT_SECRET
in either theEnvionment 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) andMyProdAzureSecretVault
(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.