Securing Secrets in Application Settings using .NET

Facebook
Twitter
LinkedIn

Storing secrets inside configuration files is bad practice. Anyone with read access to the file can gain access to the target system, for example; a database or service.

It is far better to use a secret manager, such as Azure Key Vault, for these purposes. The application can then use a service identity which has specific rights to the secret keys in the secured vault. However, in some cases we do not have access to such systems, as when running on an isolated machine without network access. A simpler way to secure these secrets is to automatically encrypt the values within the settings file.

Background

In a previous article, I discussed using the machine key to encrypt configurations in the web.config application settings section. In this updated example, I use the same approach but update the appsettings.json file in a .NET 6 application.

The process is the same as before:

  • Checks for an unencrypted value
  • Encrypts the value using a salt and in the scope of the local machine account
  • Saves the new encrypted value and deletes the unencrypted value

Implementation

First, I have created a configuration class and saved the data as JSON inside my appsettings.json file. I then load the settings.
				
					var config = new ConfigurationBuilder()
    .SetBasePath(AppContext.BaseDirectory)
    .AddJsonFile("appsettings.json", false, false)
    .Build();

var settings = config.GetSection("DatabaseService").Get<DatabaseServiceSettings>();
				
			

I use a special object for secrets in the configuration class, named ConfigurationSecret. This class has a value and secret property, which are set using the Encrypt method. We can then return the value using the Decrypt method. 

This works by using the System.Security.Cryptography.ProtectedData module.

				
					/// <summary>
/// A secret configuration which allows encryption and decryption.
/// </summary>
public class ConfigurationSecret
{
    /// <summary>
    /// Gets or sets the unencrypted value.
    /// This value will be deleted once encrypted.
    /// </summary>
    /// <value>
    /// The initial, unencrypted value.
    /// </value>
    public string? Value { get; set; }

    /// <summary>
    /// Gets or sets the encrypted secret value. 
    /// This is set by calling the <see cref="Encrypt"/> method.
    /// Get the decrypted value by using <see cref="Decrypt"/>.
    /// </summary>
    /// <value>
    /// The encrypted secret value.
    /// </value>
    public string? Secret { get; set; }

    /// <summary>
    /// Decrypts the secret value.
    /// </summary>
    /// <param name="entropy">The encryption entropy seed used for securing the configuration file.</param>
    /// <returns>
    /// The decrypted string.
    /// </returns>
    public string Decrypt(string entropy)
    {
        try
        {
            var decryptedData = ProtectedData.Unprotect(
                Convert.FromBase64String(Secret ?? string.Empty),
                Encoding.UTF8.GetBytes(entropy),
                DataProtectionScope.LocalMachine);
            return Encoding.UTF8.GetString(decryptedData);
        }
        catch
        {
            return string.Empty;
        }
    }


    /// <summary>
    /// Encrypts the value.
    /// </summary>
    /// <param name="entropy">The encryption entropy seed used for securing the configuration file.</param>
    /// <returns>
    /// The encrypted string.
    /// </returns>
    public void Encrypt(string entropy)
    {
        if (string.IsNullOrEmpty(Value))
        {
            return;
        }

        var encrypted = ProtectedData.Protect(
            Encoding.UTF8.GetBytes(Value),
            Encoding.UTF8.GetBytes(entropy),
            DataProtectionScope.LocalMachine);

        Secret = Convert.ToBase64String(encrypted);
        Value = string.Empty;
    }
}
				
			

By checking if config.Password.Value is not an empty string, we can then call the Encrypt method, which wipes the original value and sets the secret value.

I created a Save method to update the settings file using code. In my case, the settings was used within a Windows Service instance on an isolated server in a DMZ. The code module was installed as a service using remote PowerShell, the password was set initially and was encrypted immediately when the service was first started.

				
					// Fetch existing file
    var path = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
    var json = File.ReadAllText(appSettingsPath);

    // Convert file into a dynamic object so we preserve other settings
    var settings = new JsonSerializerSettings();
    settings.Converters.Add(new ExpandoObjectConverter());
    settings.Converters.Add(new StringEnumConverter());
    dynamic config = JsonConvert.DeserializeObject<ExpandoObject>(json, settings)!;

    // Update configuration
    config.DatabaseService = this; // Store current config object 

    // Save back to disk
    var update = JsonConvert.SerializeObject(config, Formatting.Indented, settings);
    File.WriteAllText(path, update);
				
			

We will now have an encrypted value within the configuration file that is not readable.

				
					{
  "DatabaseService": {
    "Server": "server-address",
    "Password": {
      "Value": "",
      "Secret": "AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAA8eo73CD9qE6HiDdGkhemMgQAAAACAAAAAAAQZgAAAAEAACA...=="
    }
  }
}
				
			
3 Comments
  1. How all these code are connected, I am not seeing you calling Encrypt or Decrypt function any where in the other code.
    Can please let me know

    1. Apologies, the previous article was dropped, as my old blog platform was archived. It can be found here: https://web.archive.org/web/20211020232457/https://blog.lekman.com/2018/02/securing-webconfig-passwords-and.html

      This is implemented as a base class. I would expose a `Configuration` property with a `Get` accessor. Then I would return an interface and, while reading it, seeing if it needs to be encrypted.

      For example:

      public CustomAppSettings GetConfiguration(string encryptionEntropy)
      {
      try
      {
      var config = new ConfigurationBuilder()
      .SetBasePath(AppContext.BaseDirectory)
      .AddJsonFile(“appSettings.json”, false, false)
      .Build();

      var settings = config.GetSection(“CustomApp”).Get();
      if (string.IsNullOrEmpty(settings.Key?.Secret) || !string.IsNullOrEmpty(settings.Key?.Value))
      {
      settings.Key?.Encrypt(encryptionEntropy);
      settings.Save();
      }

      return settings;
      }
      catch (Exception e)
      {
      throw new FileNotFoundException(“Unable to locate or load configuration file for service”,
      “appsettings.json”, e);
      }
      }

Leave a Reply

Your email address will not be published. Required fields are marked *