How Best To Secure Secrets When Building .NET Applications on AWS

Learn how to get started with using AWS Secrets Manager using a .NET Application. We will learn to connect to Secrets Manager from .NET using the client SDK and retrieve secrets. We will also see how to integrate Secrets Manager into built-in .NET Configuration and seamlessly use secrets from our ap

Rahul Pulikkot Nath
Rahul Pulikkot Nath

Table of Contents

This article is sponsored by AWS and is part of my AWS Series.

Hard-coding credentials, connection strings, and other sensitive information in the application code are wrong.

But then what is the alternative?

If you build applications on AWS Infrastructure, then AWS Secrets Manager provides a centralized store to manage your application secrets.

Secrets can be information like passwords, credentials, connection strings, API keys, etc. Secrets Manager helps you protect access to your IT resources and data by enabling you to rotate and manage access to your secrets.

In this article, let’s learn how to use AWS Secrets Manager using a .NET Application. We will learn to connect to Secrets Manager from .NET using the client SDK and retrieve secrets. We will also see how to integrate Secrets Manager to built-in .NET Configuration and seamlessly use secrets from our application code and keep the Secrets up to date.

Basic Concepts

Below are the terms that you will come across when working with Secret Manager

  • Secret → Contains the secret information, the value, and associated metadata. Metadata includes ARN, name of the Secret, description, tags, encryption key details, and key rotation policies.
  • Rotation → Process of periodically updating the Secret value to make it more secure
  • Version → Any time a secret value is updated (Rotation), it creates a new version, making it easier to keep track of changes.

Creating a Secret

There are different ways to create a secret, the most straightforward being from the AWS Console. Under the Secrets Manager, you can create a new Secret by specifying the type, value, name, and other optional information.

Create a new secret from AWS Secrets Manager in console
New Secret meta information and name

Secrets can also be created from build-deploy pipelines, using a script, or from application code. Depending on your application use-cases and scenario, you can choose one method that works for you.

AWS Secrets Manager and .NET

For demo purposes, I am using the default ASP NET Web API application template generated using Visual Studio 2022 targeting .NET 6 framework. You can find the complete source code for this post here.

To connect with AWS Secrets Manager from a .NET application, we need to install the Amazon.SecretsManager NuGet package. The package provides the AmazonSecretsManagerClient to connect to the Secrets Manager.

You can connect by passing the credentials explicitly or setting up the credentials for your environment.

The GetSecretValueAsync method takes in a request which requires the SecretId. The SecretId is the secret's name and must be unique within the current Secret Manager.

private static async Task<int> GetCountFromAWSSecretsManager()
{
    var client = new AmazonSecretsManagerClient();
    var response = await client.GetSecretValueAsync(
        new GetSecretValueRequest()
        {
            SecretId = "MySecret",
        });
    var count = int.Parse(response.SecretString);
    return count;
}

Versioning

Any time we update a Secret, it creates a new version. Secrets Manager automatically assigns labels for the current (AWSCURRENT) and previous (AWSPREVIOUS) secret versions.

By default getting a Secret retrieves the latest version, AWSCURRENT, of the Secret in the Secrets Manager.

However, you can also explicitly specify to retrieve a specific version label.

var response = await client.GetSecretValueAsync(
    new GetSecretValueRequest()
    {
        SecretId = "MySecret",
        VersionStage = "AWSPREVIOUS"
    });

Creating Secrets using code also supports explicitly setting the VersionId of the Secret. The  Console UI does not support this when writing this article.

Integrate Secrets Manager with .NET Configuration

When building .NET application, it’s common to have all application configuration values as part of the .NET configuration. The default source of configuration is the appsettings.json file. However, you should not be saving sensitive keys (Secrets) in the file or keep them as part of the source control.

AWS Secrets Manager can be configured as a configuration source in .NET. This enables to easily integrate with .NET application code and other patterns that come with .NET.

When defined in appsettings.json file, the configuration might look like something below. We have two configuration values - Count and ApiKey - used in the WeatherForecastController.

"WeatherForecast": {
    "Count": 10,
    "ApiKey": ""
  },

Options Pattern

Using Options Pattern, we can inject the configuration values into our controllers. The below code registers the WeatherForecastOptions and injects it into the controller using IOptionsSnapshot.

builder.Services.AddOptions<WeatherForecastOptions>().BindConfiguration("WeatherForecast");

public class WeatherForecastOptions
{
    public int Count { get; set; }
    public string ApiKey {get; set; }
}

public WeatherForecastController(
            IOptionsSnapshot<WeatherForecastOptions> weatherOptions,
            ILogger<WeatherForecastController> logger)
        {}

Since the configuration is now bound to the .NET Configuration, its values can come from any of the registered sources - including appsetting.json, environment variables, Secret Manager, etc.

Local Development Environment

For the local development environment, where we don’t require connecting to AWS Secrets Manager, we can set up the application to use .NET Secret Manager.

.NET Secret Manager is only to be used in local development environments and not in a production environment.
if (builder.Environment.IsDevelopment())
    builder.Configuration.AddUserSecrets<Program>();

With Secret Manager enabled, you can have the sensitive information stored in the secrets.json file, which lives outside the source code and repository.

AWS Secrets Manager & .NET Configuration Provider

The Kralizek.Extensions.Configuration.AWSSecretsManager  Nuget package provides a configuration provider for AWS Secrets Manager. The source code is available here.

Registering Secrets Manager as a configuration source is easily done using the AddSecretsManager extension method. It's done in the Program.cs where the application initialization code lives.

builder.Configuration.AddSecretsManager(configurator: ops => {
  // Replace __ tokens in the configuration key name
  opts.KeyGenerator = (secret, name) => name.Replace("__", ":");
});

The code above also specifies mapping Secret key names in the AWS Secrets Manager to .NET Configuration. The .NET config we defined earlier is hierarchical. The Count and ApiKey property live under WeatherForecast.

In .NET, the colon(:) is used to indicate hierarchies. However, AWS Secret Names must contain only alphanumeric characters and the characters /_+=.@-.

So when we have to create hierarchical keys, we need to choose a different separator - double underscore (__). So Count will be ‘WeatherForecast__Count’ and ApiKey will be ‘WeatherForecast__ApiKey’.

These keys need to be mapped back into the .NET Configuration so that it will override the appropriate configuration values. The KeyGenerator allows us to map an AWS Secret Key to a key in the .NET Configuration.

In the example here, since our separator is ‘__’, we are replacing it to be ‘:’, so it maps correctly.

Automatic Refresh

The AWS Secrets Manager acting as another source for configuration, will load all the Secrets at the application start and override the configurations.

However, once the application is up and running, any changes to the Secret value will not reflect in the application unless we restart the application.

To automatically refresh this, we can specify the PollingInterval property when configuring the AWS Secrets Manager as a configuration source.

builder.Configuration.AddSecretsManager(configurator: ops => {
  // Replace __ tokens in the configuration key name
  opts.KeyGenerator = (secret, name) => name.Replace("__", ":");
  opts.PollingInterval = TimeSpan.FromMinutes(30);
});

In the above scenario, the application will automatically poll the AWS Secrets Manager every 30 minutes and update the value to the latest.

I hope this helps you to get started with AWS Secrets Manager and seamlessly use this from .NET Applications.

Photo by James Sutton on Unsplash

AWSServerlessDotnet-Core