Obtain Azure access token from a local Docker container
Tags: devops, docker, azure, dotnet
Published: 2022-07-30 02:43:59

Apps talking to Azure need to obtain an access token at runtime. For example, reading secrets from [Azure Key Vault](https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0#use-managed-identities-for-azure-resources) as part of your configuration. There's a whole host of Azure services you can talk to using tokens: Azure Key Vault, Azure App Config, Azure SQL, Azure Event Hubs, Azure Service Bus, Azure Storage Account etc.

All follow the same basic flow: obtain an access token as an Azure Identity and attach that token to API requests for that Azure service. In .NET apps you can use the new [Azure.Identity](https://www.nuget.org/packages/Azure.Identity) library and it's `DefaultAzureCredential` type.

This type will automatically try to obtain an Azure access token using various methods, but 3 are of particular interest:
- Environment variables - for logging in as a Service Principal using Client ID, Client Secret and Tenant ID environment variables
- Managed Identity - for logging in using a system or user-assigned managed identity in Azure systems like Azure App Service, Azure Kubernetes Service, Azure VM etc.
- Azure CLI - for logging in on your local machine for development work

**Q: So how am I supposed to log in to Azure so that my app can obtain tokens?**
A: I tell devs: For local development log in to Azure CLI with your normal user account. It has Contributor over your Dev/Test subscription and you can access secrets and configuration from their Dev/Test Key Vaults.
For staging and production running in Azure (in our case Docker containers running on AKS) we use User-Assigned Managed Identity and [aad-pod-identity](https://github.com/Azure/aad-pod-identity) project. This managed identity has least-privilege permissions over staging and production environments to do it's job at runtime.

**Q: I can obtain tokens locally using Azure CLI and Azure.Identity library when I run on the host machine, but not when inside Docker container because it doesn't have Azure CLI installed! What do I do?**
A: This has already been asked about by many people [here](https://github.com/Azure/azure-sdk-for-net/issues/19167) with various interesting solutions [here](https://github.com/ClrCoder/ClrPro.AzureFX) and [here](https://github.com/jongio/azureclicredentialcontainer).

I decided to write another solution of my own because I would like locally run Docker containers to be as closer to staging as possible, i.e. use Managed Identity flow to obtain Azure access tokens. Obviously I cannot use Managed Identities on a laptop but I can spoof the process.

**Let's reproduce the problem with least code.**
Made a little [sample console app](https://github.com/xenalite/local-docker-azure-token-endpoint/tree/main/src/SampleConsumerApp) that connects to Azure Key Vault, adds secrets to `IConfiguration` and then prints all config to console (not a great practice in prod btw!).

Running it directly on my laptop with `dotnet` is fine because my laptop has Azure CLI installed, and I am logged into it.
```
$env:AZURE_KEY_VAULT_URI="https://<redacted>.vault.azure.net/"
dotnet run .\SampleConsumerApp.csproj
mysecret=secretsauce (AzureKeyVaultConfigurationProvider)
```

**Q: What happens when I build a Docker image and try to run it?**
```
docker build -t dev:consumer -f .\Dockerfile .
<truncated>
docker run -e AZURE_KEY_VAULT_URI=https://<redacted>.vault.azure.net/ dev:consumer
Unhandled exception. Azure.Identity.CredentialUnavailableException: DefaultAzureCredential failed to retrieve a token from the included credentials. See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/defaultazurecredential/troubleshoot
- EnvironmentCredential authentication unavailable. Environment variables are not fully configured. See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/environmentcredential/troubleshoot
- ManagedIdentityCredential authentication unavailable. Multiple attempts failed to obtain a token from the managed identity endpoint.
- Operating system Linux 5.10.102.1-microsoft-standard-WSL2 #1 SMP Wed Mar 2 00:30:59 UTC 2022 isn't supported.
- Stored credentials not found. Need to authenticate user in VSCode Azure Account. See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/vscodecredential/troubleshoot
- Azure CLI not installed
- PowerShell is not installed.
<truncated>
```

**Q: Why use Azure CLI at all? Why not just configure my Docker container with environment variables for ClientId, ClientSecret and TenantID?**
A: While I give devs a service principal for Dev/Test to use in pipelines, this principal's credentials are not supposed to be used for local development. Configuring SP credentials with environment variables would prevent us from rotating them frequently too. Not everyone has a service principal to hand either!

**Q: I don't want to install Azure CLI into my dev Docker images just to obtain a token. So what's next?**
A: It seems getting an access token using a Managed Identity token endpoint is the easiest because you can just [curl a URL](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-curl) and get a token. One problem is that nearly all authentication libraries hardcode this URL as `http://169.254.169.254/metadata/identity/oauth2/token?<uri_params>`

So, I am going to create a "proxy" managed identity token endpoint and return a token just like the app expects.

**Q: How will this proxy provider get a token in the first place?**
A: This proxy provider is a tiny ASP.NET Core web API and can use Azure.Identity library just like the sample console app. So let's make it get this token from Azure CLI. I can install Azure CLI into that token provider since it's only a development tool and not my app's production image. 

**Q: Ok but how would the Azure CLI inside proxy's Docker container get an access token? You'd have to log into Azure CLI every time you start the container!**
A: I can mount my laptop's `.azure` folder as a volume into the token provider's container. As long as I am logged into Azure CLI on my laptop I can obtain tokens using Azure CLI inside proxy's container! Combine that with a little web API and we're in business.

If this is confusing, here's a handy diagram:
![diagram1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6t5kqp5271ji3bpamktc.png)

There is a small problem with this that someone already spotted. It only works with Azure CLI version up to 2.29 as described [here](https://github.com/jongio/azureclicredentialcontainer/issues/2). So you'd need to install that version of Azure CLI on host machine and in the token provider's Docker image.

[Here](https://github.com/xenalite/local-docker-azure-token-endpoint/tree/main/src/LocalDockerAzureTokenEndpoint) is the least effort code for this token provider. Let's build it and run it.
```
docker build -t dev:token -f .\Dockerfile .
<truncated>
docker run -e ASPNETCORE_URLS=http://+:80 -p 8080:80 -v C:\Users\OskarMamrzynski\.azure:/root/.azure:rw dev:token
<truncated>
      Now listening on: http://[::]:80
<truncated>
      Request finished HTTP/1.1 GET http://localhost:8080/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F - - - 200 - application/json;+charset=utf-8 1196.6707ms
```

My container listens on localhost:8080 on the host machine, so we can curl it and obtain a token impersonating the user logged in with Azure CLI:
```
curl 'http://localhost:8080/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F' -H Metadata:true -s
{"access_token":"eyJ0<redacted>","refresh_token":"","expires_in":5335.8911044,"expires_on":1659150718,"not_before":1659145382,"resource":"https://management.azure.com/","token_type":"Bearer"}
```

**Q: Fine, but your proxy container is still not using that `169.254.169.254` IP address. How are you going to get that working?**
A: We can create a local Docker network and assign a static IP to our token provider container. Consumer app would also run in this network.
```
docker network create omtest --subnet=169.254.0.0/16
<truncated>
docker run -d --net omtest --ip 169.254.169.254 -e ASPNETCORE_URLS=http://+:80 -p 8080:80 -v C:\Users\OskarMamrzynski\.azure:/root/.azure:rw dev:token
<truncated>
docker run -it -e AZURE_KEY_VAULT_URI=https://<redacted>.vault.azure.net/ --net omtest dev:consumer
mysecret=secretsauce (AzureKeyVaultConfigurationProvider)
```

**In summary**
You can use Azure CLI in conjunction with this token provider container just always running there in the background on your laptop. Any Docker containers you spin up locally will be able to use this spoofed token provider endpoint as long as they are on the same Docker network. This makes it appear as if the app is assigned a Managed Identity.