¿Estás usando appsetings.json o appsettings.development.json para guardar información sensible? ¿Has “commiteado” alguna vez cadenas de conexión, secretos privados o llaves de aplicación por accidente?

Si la respuesta a las preguntas anteriores es afirmativa y nunca habías escuchado hablar de User Secrets, entonces este artículo te puede interesar.

Contexto

En la mayoría de nuestras aplicaciones necesitamos interactuar con algún tipo de API, servicio o base de datos que requiere la utilización de credenciales ya sea en forma de cadena de conexión (connection string), llaves de API (API Keys) o secretos (secrets).

Cómo programadores, no nos corresponde ocuparnos de la gestión de esas credenciales en ambientes productivos, pero sí nos corresponde manejarlas y protegerlas correctamente en nuestros ambientes locales.

Si bien hay diferentes formas de manejar credenciales (variables de entorno para producción por ejemplo), es muy frecuente la utilización de archivos appsettings.json y/o appsettings.development.json.

Problema

Para ejecutar nuestra aplicación en forma local, solemos utilizar el archivo appsettings.development.json para colocar aquellas configuraciones que dependen del ambiente, como por ejemplo las cadenas de conexión a base de datos. Por ejemplo:

{
  "ConnectionStrings": {
    "Database": "Server=dev.server;Database=Products;User Id=dev;Password=Passw0rd;"
  },
  "PricesEndpoint": "https://dev.server.com/api/prices?key=xkihf57845addg"
}

El problema con este método es que al momento de subir nuestros cambios al repositorio es muy fácil olvidarnos de este archivo e incluirlo como parte de la implementación.

.gitignore como alternativa

Un posible solución al problema anterior es incluir el archivos appsettings.development.json en el archivo .gitignore de forma que git ignore los cambios. En escenarios muy simples esto puede funcionar sin problemas.

Sin embargo, es común que haya ciertas configuraciones de facto que se van agregando o modificando con el tiempo, y que luego cada programador puede ir modificando en forma local según sea el caso. Por ejemplo:

{
  // Default for development environment.
  "UseCache":  false
}

Esto puede resultar bastante tedioso y confuso de manejar si el archivo appsettings.development.json se encuentra ignorado.

Solución: User Secrets

El Secret Manager es una funcionalidad desarrollada específicamente para manejar información sensible en ambientes locales de desarrollo de una forma más segura, y se ocupa de la gestión de los User Secrets.

Este guarda las piezas de información sensible dentro de un archivo que se encuentra en un directorio totalmente diferente al directorio del proyecto, de forma que nuestro sistema de control de cambios no lo vea como parte de las modificaciones.

¡Jamás volverás a “commitear” cadenas de conexión o secretos!

Activar User Secrets

Secret Manager funciona por proyecto, es decir, lo tenemos que activar en todos los proyectos donde lo necesitamos.

Podemos hacerlo mediante la línea de comandos utilizando .NET Cli ejecutando el siguiente comando en la carpeta del proyecto:

dotnet user-secrets init

O también podemos hacerlos desde Visual Studio simplemente haciendo clic derecho sobre el proyecto y luego seleccionando la opción Administrar secretos de usuario:

Administrar secretos de usuario desde Visual Studio
Administrar secretos de usuario desde Visual Studio

Al activar los secretos de usuario, se genera una entrada en el archivo del proyecto (.csproj) llamada UserSecretsId con un guid:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <UserSecretsId>0dc8fc18-564f-4c6e-9602-65fae16ab231</UserSecretsId>
  </PropertyGroup>
</Project>

Ese guid corresponde al nombre de un directorio creado %APPDATA%\Microsoft\UserSecrets\ (Windows) o ~/.microsoft/usersecrets/ (Linux / MacOS), dentro del directorio encontraremos un archivo en formato JSON llamado secrets.json con las configuraciones que hayamos agregado.

Directorio de secretos de usuario
Directorio de secretos de usuario

Agregar User Secrets

Podemos agregar secretos de usuario mediante el cliente .NET Cli utilizando el siguiente comando:

dotnet user-secrets set "ConnectionStrings:Database" "Server=..."

dotnet user-secrets set "PricesEndpoint" "https://dev..."

En el primer comando los “:” (dos puntos) se utilizan para indicar los niveles de objetos de un JSON

También podemos hacerlo desde Visual Studio mediante la opción Administrar secretos de usuario como vimos anteriormente. La ventaja de esta alternativa es que Visual Studio nos permite manejar los secretos directamente como un archivo JSON, exactamente igual al archivo appsettings.json:

{
  "ConnectionStrings": {
    "Database": "Server=..."
  },
  "PricesEndpoint": "https://dev..."
}

Leer User Secrets

Los secretos de usuario se leen como cualquier otra configuración una vez que fueron cargados apropiadamente. Por ejemplo, podemos acceder a la configuración PricesEndpoint mediante la interfaz IConfiguration:

public class Startup
{
    public Startup(IConfiguration configuration) =>
        Configuration = configuration;

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        var pricesEndpoint = Configuration["PricesEndpoint"];
        // O Configuration.GetConnectionString("Database")        
        var connectionString = Configuration["ConnectionStrings:Database"];
        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
    }
}

Para que esto funcione es necesario cargar primero los secretos de usuario. Si estamos utilizando Host.CreateDefaultBuilder(args) en la clase Program.cs, entonces no es necesario hacer nada mas ya que los secretos de usuario se cargar en forma automática.

public class Program
{
    public static void Main(string[] args) =>
        CreateHostBuilder(args).Build().Run();

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

En el caso de que no estemos utilizando Host.CreateDefaultBuilder(args) o querramos cambiar el comportamiento, tenemos que utilizar el método de extensión AddUserSecrets().

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostContext, builder) =>
        {
            builder.SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile("appsettings.development.json", optional: true, reloadOnChange: true)
                .AddUserSecrets<Program>(optional: true, reloadOnChange: true)
                .AddEnvironmentVariables();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

El orden en el que se incluyen las distintas fuentes de configuración es importante, ya que las ultimas sobrescriben a las primeras.

Debido a que los User Secrets sólo se usan en el ambiente local de cada desarrollador, lo recomendable es incluirlos sólo y sólo si el ambiente es Desarrollo:

Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((hostContext, builder) =>
    {
        var environment = hostContext.HostingEnvironment;
        // ...
        if(environment.IsDevelopment())
        {
            builder.AddUserSecrets<Program>(optional: true, reloadOnChange: true);
        }
        // ...
    })
    // ...

Conclusión

En este artículo aprendimos que incluso en ambientes locales de desarrollo debemos manejar información sensible, como cadenas de conexión y secretos, con el mismo cuidado que lo hacemos en producción.

Además aprendimos cómo User Secrets puede ayudarnos a evitar dolores de cabeza en la gestión de información sensible en nuestros ambientes de desarrollo locales de una forma muy simple y práctica.