Securing credentials in Azure-hosted applications using managed identities and Azure Key Vault - The traditional approach and its drawbacks
In this post, we explore why the traditional method of storing credentials in web applications can pose significant security risks.
As mentioned in the introduction, web applications frequently depend on external resources to fulfill user requests. More often than not, these are various types of datastores such as MySQL, SQL Server, or CosmosDB, to name a few. Access to these resources is typically granted through connection strings or API keys, which are traditionally stored in configuration files.
The example below is taken from an ASP.NET application, but the approach is essentially the same regardless of the programming language used.
In ASP.NET, the configuration file is now typically called appsettings.json (formerly web.config). Here's an example of what it might look like.
1{
2 "Logging": {
3 "LogLevel": {
4 "Default": "Information",
5 "Microsoft.AspNetCore": "Warning"
6 }
7 },
8 "DatabaseConnectionString": "<credentials_for_database>"
9}
We will now put this concept into practice by demonstrating a concrete example: an ASP.NET application accessing a storage account.
Setting up the environment
We will first of all create the necessary Azure resources and then deploy a simple application.
Creating the resources in Azure
- Create an app service in the Azure portal.
- Create a storage account in the Azure portal.Information
In a real-world application, we would typically use IaC tools such as Bicep, Terraform, or similar to provision and manage these resources.
Deploying a simple application
Open Visual Studio, create a new solution and add to it a new Blazor Web App project.
Add the following code to the appsettings.json file.
In this file, we specify the access key of our storage account under the StorageConnectionString parameter.
This key can be retrieved from the Azure portal. We will not detail the procedure here, as it is assumed the reader is already familiar with this process.
- Create a new class named BlobService.cs and add the following code in it.
1public class BlobService
2{
3 private readonly string _connectionString;
4 private readonly string _containerName;
5
6 public BlobService(IConfiguration config)
7 {
8 _connectionString = config["StorageConnectionString"];
9 _containerName = "eocs";
10 }
11
12 public async Task UploadTextFileAsync(string fileName, string content)
13 {
14 var container = new BlobContainerClient(_connectionString, _containerName);
15 await container.CreateIfNotExistsAsync();
16
17 var blob = container.GetBlobClient(fileName);
18 var bytes = Encoding.UTF8.GetBytes(content);
19 using var stream = new MemoryStream(bytes);
20 await blob.UploadAsync(stream, overwrite: true);
21 }
22}
And we do not forget to register this service.
1builder.Services.AddSingleton<BlobService>();
- Add the following code to the Counter.razor page.
1@page "/counter"
2@rendermode InteractiveServer
3@inject EOCS.ManagedIdentity.Web.Components.Services.BlobService BlobService
4
5<PageTitle>Counter</PageTitle>
6
7<h1>Counter</h1>
8
9<p role="status">Current count: @currentCount</p>
10
11<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
12
13<label>@errorMessage</label>
14
15@code {
16 private int currentCount = 0;
17 private string errorMessage = "";
18
19 private async Task IncrementCount()
20 {
21 currentCount++;
22
23 try
24 {
25 var fileName = Guid.NewGuid().ToString();
26 var fileContent = "simple content";
27
28 await BlobService.UploadTextFileAsync(fileName, fileContent);
29 }
30 catch (Exception ex)
31 {
32 errorMessage = ex.Message;
33 }
34 }
35}
This code simply creates a file in the storage account each time the button is clicked. While not groundbreaking, it provides us with an example of code that includes an external dependency—specifically, a storage account in this case.
And we see that a file is created in the storage account when the button is clicked.
Yes and why is it a problem ?
There’s nothing inherently wrong with this design, in fact, it was widely used for many years. Configuration files are typically secured from external access, making it difficult for malicious actors to retrieve their contents. So why is this straightforward approach no longer considered best practice today ?
However, over time, two major issues have been identified with it.
Developers end up having access to sensitive credentials.
When credentials are hardcoded in configuration files, anyone with access to the source code can retrieve them. Yet, databases and other data stores often contain highly sensitive information. This can lead to serious privacy breaches if accessed by overly curious individuals.
Imagine, for instance, a database containing medical records. It might be tempting for someone to look up a neighbor’s health history, turning sensitive data into a potential target for misuse or even monetization.
But that’s not the only issue. Even if the database doesn’t contain sensitive information, a developer could still take advantage of the credentials.
A somewhat disgruntled developer could potentially put the company at risk if credentials are too easily accessible.
Credentials can accidentally be committed to version control systems like Git.
Each time credentials are written down, there’s a risk of data leakage. A careless developer might accidentally copy them elsewhere and forget to remove them, or the configuration file could unintentionally be uploaded to Git. Consequently, credentials are publicly accessible and potentially exposing sensitive data to anyone.
So, how can we protect against these threats ?
These risks have been known for a long time, but implementing effective countermeasures was often challenging. One approach involved encrypting the secrets stored in configuration files, but a skilled developer could still decrypt them and gain access. Another option was to use certificates, though this method was complex and not easily accessible to the average engineer.
That’s why major cloud providers like Microsoft and Amazon have developed dedicated solutions to address this issue and enhance the overall security of cloud infrastructure. In the following sections, we’ll explore how to achieve this in Azure—though the underlying principles apply similarly across other platforms.