Skip to content Skip to footer

Application Configuration and Secrets Management in Azure Container Apps

How to configure and provide secrets management securely in Azure Container Apps

In the previous post, we provisioned an Azure Container Apps environment and deployed the ASP.NET sample application into a Container App.

Let’s take it a step further use a .NET Core application and demonstrate how we can inject configuration and provide secrets securely in Azure.

This post assumes you have worked through the previous post:

https://www.playtimesolutions.com.au/how-to-deploy-a-sample-container-apps-application

The source code is available here:

https://github.com/playtimesolutions/ContainerAppsDemo

Step 1 – Local configuration settings

1.1. Inspect the application configuration

Our application will use hard-coded app settings unless we specifically override them.

src/dotnet-api/appsettings.json

				
					{
	"Settings":{
	  "ConfigValue": "Defined in appsettings.json"
	}
}
				
			

Program.cs

The following API endpoint will log the application’s configuration to the console. This is an insecure thing to do, so we’re only doing this here you can conveniently see how the application is configured.

				
					app.MapGet("/config", () =>
{
    foreach (var k in builder.Configuration
                 .AsEnumerable()
                 .OrderBy(k => k.Key))
    {
        Log.Information($"{k.Key}: {k.Value}");
    }
});
				
			

1.2. Use the Environment Variables Configuration Provider

				
					dotnet run
curl http://localhost:8080/config
				
			

You’ll see the application’s configuration in the console, which includes all of the app settings and environment variables in the process.

				
					..
Settings:
Settings:ConfigValue: Defined in appsettings.json
..
				
			

We can override these values at runtime in various ways.

A common way to override app settings at runtime is to use the EnvironmentVariablesConfigurationProvider. This provider is baked into .NET Core applications, so we can leverage this behavior without having to write any additional code.

See: https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers

Add the following environment variable into your shell and re-run the application.

				
					export Settings__ConfigValue="Defined in an environment variable"
dotnet run
curl http://localhost:8080/config
				
			

The configuration provider has overridden the hard-coded app setting with the environment variable.

				
					..
Settings:
Settings:ConfigValue: Defined in an environment variable
..
				
			

By following this approach, an application’s configuration can be easily externalized and driven from the environment instead of being hard-coded.

See: https://12factor.net/config

Step 2 – Deploy the application to Container Apps to Azure and inject settings

2.1. Build the container image

The following code creates a container image and pushes the image to the Azure Container Registry.

				
					cd src/dotnet-api

# tag the image
TAG=$(date '+%Y%m%d%H%M%S')  
IMAGE_NAME="dotnet-api:$TAG"
CONTAINER_IMAGE="$CONTAINER_REGISTRY_URL/$IMAGE_NAME"

# build dotnet-api container image
docker build -t dotnet-api:latest .
docker tag  dotnet-api:latest $CONTAINER_IMAGE

# login
az acr login --name $CONTAINER_REGISTRY_NAME

# push
docker push $CONTAINER_IMAGE
				
			

2.2. Deploy the container app.

Deploy the container image.

				
					az deployment group create \
--resource-group $RESOURCE_GROUP \
--name $DEPLOYMENT_NAME \
--template-file devops/deploy.bicep \
--parameters "configValue=Defined in a Container App environment variable" \
--parameters "containerRegistryName=$CONTAINER_REGISTRY_NAME" \
--parameters "environmentName=$ENVIRONMENT_NAME" \
--parameters "keyVaultName=$KEYVAULT_NAME" \
--parameters "imageName=$IMAGE_NAME" \
--query properties.outputs.fullyQualifiedDomainName.value
				
			

Store the fullyQualifiedDomainName in a variable for use later

				
					URL="the_fdqn"
				
			

src/dotnet-api/devops/deploy.bicep

The bicep definition for the container app should look familiar – it’s very similar to the example in the previous post. However, this time we will define a variable in the container’s environment via a CLI-provided parameter.

				
					...
param configValue string;
...
containers: [
        {
          ...
          env: [
			...
			...
            {
              name: 'Settings__ConfigValue'
              value: configValue
            }
            ...
            ...
          ]
        }
      ]
...
...
				
			

To view the logs from the Container App, load the /config endpoint in the browser to produce the logs, then issue the following command to view the logs.

				
					curl $URL/config
az containerapp logs show --name dotnet-api --resource-group $RESOURCE_GROUP
				
			

You’ll see the following included in the logs.

				
					..
Settings:
Settings:ConfigValue: Defined in a Container App environment variable
..
				
			

With this approach, we can build a single container image with the default configuration and override settings on a case-by-case basis in order to customize the application for each environment (e.g. dev/test, prod).

Step 3 – Configure Secrets in Azure

Our objectives for secure secrets management are as follows:

  1. Use a key vault to store secrets
  2. Do not allow access to secrets unless it is specifically granted (least privilege)
  3. Do not expose secrets via configuration files or environment variables

Our secrets configuration approach will leverage the same configuration provider pattern as environment variables, however this time we have to jump through some additional hoops in order to secure the solution.

3.1. Setup a managed identity for the container app

In order to securely access the key vault, we need to set up a managed identity and associate it with the container app.

				
					...
resource containerAppIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
  name: '${containerAppName}-identity'
  location: location
}

resource containerApp 'Microsoft.App/containerApps@2022-03-01' = {
  name: containerAppName
  location: location
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${containerAppIdentity.id}' : {}
    }
  }
...
				
			

The next step is to grant access to the key vault’s managed identity for ‘get’ and ‘list’ operations, so the application can retrieve the secrets at runtime.

				
					...
resource keyVault 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = {
  name: keyVaultName
}

resource keyVaultAccessPolicy 'Microsoft.KeyVault/vaults/accessPolicies@2019-09-01' = {
  name: 'add'
  parent: keyVault
  properties: {
    accessPolicies: [
      {
        tenantId: subscription().tenantId
        objectId: containerAppIdentity.properties.principalId
        permissions: {
          secrets: [
            'get'
            'list'
          ]
        }    
      }
    ]
  }
}
...
				
			

This is an example of least privilege.

See: https://en.wikipedia.org/wiki/Principle_of_least_privilege#Details

Now, create a secret in the key vault that our application can consume.

				
					resource runtimeSecret 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
  name: 'dotnet-api-Settings--SecretValue'
  parent: keyVault
  properties: {
    attributes: {
      enabled: true
    }
    value: 'Defined in a key vault secret'
  }
}
				
			

3.2. Create additional environment variables

The following environment variables are required to drive the process.

				
					...
param keyVaultName string
...
containers: [
        {
          ...
          env: [
						...
						...
            {
              name: 'ASPNETCORE_ENVIRONMENT'
              value: 'Azure'
            }
            {
              name: 'AzureADManagedIdentityClientId'
              value: containerAppIdentity.properties.clientId
            }
            {
              name: 'AzureKeyVaultUri'
              value: 'https://${keyVaultName}.vault.azure.net'
            }
            ...
            ...
          ]
        }
      ]
...
...
				
			

3.3. Wire up the KeyVault

Using the AzureKeyVaultConfigurationExtensions, we can extract values from a specified key vault and securely load them into the IConfiguraiton.

The extension works as follows:

  1. If ASPNETCORE_ENVIRONMENT is Azure, then proceed.
  2. Retrieve the key vault URL and the managed identity from the environment.
  3. Add the key vault integration, and specify that the only secrets required are those prefixed with dotnet-api as per the secret we created in step 3.1
				
					public static class Extensions
{
    public static IConfigurationBuilder AddKeyVaultSecrets(this IConfigurationBuilder builder, string prefix)
    {
        var settings = builder.Build();

        var environment = settings["ASPNETCORE_ENVIRONMENT"];
        if (environment == "Azure")
        {
            var keyVaultEndpoint = settings["AzureKeyVaultUri"];
            var azureADManagedIdentityClientId = settings["AzureADManagedIdentityClientId"];
        
            var credentials = new DefaultAzureCredential(new DefaultAzureCredentialOptions
            {
                ManagedIdentityClientId = azureADManagedIdentityClientId
            });
        
            builder.AddAzureKeyVault(new Uri(keyVaultEndpoint), credentials,
                new AzureKeyVaultConfigurationOptions
                {
                    Manager = new PrefixKeyVaultSecretManager(prefix)
                });
        }
        
        return builder;
    }

    private class PrefixKeyVaultSecretManager : KeyVaultSecretManager
    {
        private readonly string _prefix;

        public PrefixKeyVaultSecretManager(string prefix) => _prefix = $"{prefix}-";

        public override bool Load(SecretProperties properties) => properties.Name.StartsWith(_prefix);

        public override string GetKey(KeyVaultSecret secret) 
            => secret.Name[_prefix.Length..].Replace("--", ConfigurationPath.KeyDelimiter);
    }
}
				
			

Include this code on startup.

				
					builder.Configuration.AddKeyVaultSecrets("dotnet-api");
				
			

Ensure the application has been deployed to azure and call the /config endpoint as before, observe that we have the secret value available in the logs – it has been loaded into the application process’s memory in the IConfiguration during startup.

				
					..
..
Settings:
Settings:SecretValue: Defined in a key vault secret
..
..
				
			

We don’t want secrets residing in the environment variables for the container – doing so would be a security vulnerability if a malicious actor gained access to the container and was able to extract the environment.

In the Azure portal we can extract the container’s environment through a terminal session.

Azure Container Apps Console

Run the following command

				
					env
				
			

You’ll see that Settings__SecretValue is not exposed in the container’s environment. We’ve met our three objectives for secure secret handling.

Conclusion

In this post, we’ve seen a common way to manage configuration settings and secrets in containerized .NET Core applications.

Overriding secrets with a least-privilege approach while not exposing them to malicious actors can be achieved fairly easily with the approach outlined above.

Stay tuned for the next post in the series where we’ll demonstrate how to lock down your environment even further with the use of VNET and Private Endpoints.

Playtime Solutions AKS Best Practice Guideline eBookPlaytime Solutions’
AKS Best Practice Guide

A 23 page best-practice checklist, leveraging Playtime Solutions’ hands-on experience in designing, developing and delivering enterprise-grade application. This guide assists IT and DevOps professionals in creating an enterprise-grade Kubernetes environment in Microsoft AKS.