La .NET CONF 2024 acaba de terminar. Descubre las NOVEDADES de .NET 9 junto a los CAMBIOS que trae C# 13.
Puedes ver el contenido de este vídeo junto con su curso en el modo vídeo (similar a Udemy) si pulsas aquí.

Gestión de credenciales con Vault

En este post vamos a añadir un servicio a nuestro sistema distribuido para almacenar todas las contraseñas y datos confidenciales dentro de nuestro sistema de una forma segura a través de un gestor de credenciales.

 

1 - Qué es un gestor de credenciales?

Un gestor de credenciales (o secret manager) es un administrador de contraseñas o credenciales. El lugar donde vas a ubicar toda esa información, que es confidencial y debe ser almacenada de forma segura.

Ya bien sean contraseñas, que es el más común, configuraciones o certificados para la comunicación.

 

Podemos  comparar un secret manager a un gestor de contraseñas de usuario, únicamente que el secret manager va a funcionar a nivel de aplicación.

 

Cuando trabajamos a nivel empresarial tenemos varias opciones, si utilizamos AWS o Azure, tienen el suyo propio, Octopus tiene también el suyo propio, y en nuestro caso vamos a ver Vault, de HashiCorp. 

credenciales

 

Nota: Recuerda que toda aquella información que consideramos secreta o segura va a ser una credencial. Por ejemplo, si almacenamos la información de la base de datos, está claro que el usuario y la contraseña son credenciales, pero podemos evaluar como el servidor donde está ubicada la base de datos también como parte de la credencial, así que lo podemos almacenar en el. 

 

1.1 - Por qué necesitamos un gestor de credenciales? 

El motivo principal por el que nuestras aplicaciones van a necesitar un secret manager es principalmente para evitar que dicha información se filtre o llegue a las manos erróneas. 

Esto evita la responsabilidad de los desarrolladores de tener que tener cuidado en no filtrar dichas contraseñas o incluso cambiarlas. 

 

Todos hemos visto casos reales donde la conexión a la base de datos está especificada en los ficheros de configuración, eso no debería suceder, nunca. 

Si que es verdad que si trabajas con kubernetes y octopus, puedes reemplazar los valores en el values.yaml y dichas contraseñas están en el fichero de configuración, pero por detrás estás utilizando los secrets de octopus. 

 

 

Finalmente, tener las credenciales en un servicio único para ello, nos permite añadir políticas a dichas credenciales, por ejemplo, rotar dichas credenciales cada 3 meses. 

Configuramos la credencial para que cada 3 meses la contraseña de la base de datos cambie, y se actualice en la base de datos. Cuando la aplicación vaya a leer dicha credencial leerá el nuevo valor. 

Nota: Para poder conectarte a dicho secret manager deberás pasar las credenciales para el mismo. En la implementación veremos cómo hacerlo.

 

 

2 - Qué es Hashicorp Vault?

Antes de pasar a la implementación me gustaría explicar el motivo que me ha llevado a utilizar Vault

Vault es un servicio creado por HashiCorp, los creadores de terraform (que veremos también) u otros servicios como Consul, el cual utilizaremos para el service discovery. 

 

También he de decir que a nivel profesional he utilizado únicamente AWS secret manager y los secretos de octopus; y sinceramente no quiero enlazar este sistema a AWS y los de octopus no son adecuados para el largo plazo, en mi experiencia por lo menos.

Por esos motivos las opciones se reducen muchísimo, y no sé si no hay más empresas que ofrezcan dicho servicio y que esté disponible en docker, o simplemente no tienen buen SEO, pero no he podido encontrar ninguna otra alternativa. 

 

2.1 - Opciones que nos proporciona Vault

Cuando trabajamos con un secret manager solemos hacerlo con su servicio clave valor (key/value). lo que viene a ser que tu tienes una clave por ejemplo passwordRabbitMQ y le asignas un valor. 

Este valor puede ser un string o un objeto JSON. En nuestro caso en concreto vamos a almacenar el objeto json en el path secrets/rabbitmq.

{
  "password": "DistribtPass",
  "username": "DistribtAdmin"
}

nota: el path es una combinación entre “engine” (key/value) y “clave”.

Los engines son Exclusivos de vault, en este caso, el engine !clave valor" es el mencionado anteriormente.

 

Otros engines permiten añadir una serie de aplicaciones, donde la acción para recibir la credencial va a ser diferente.

engine en vaultPodemos entender un engine como un plugin.

 

 

3 - Implementación de secret manager en el código

Lo primero que tenemos que hacer es crear en nuestro docker-compose donde ya tenemos rabbitMQ un nuevo servicio el cual contendrá Vault

vault:
  image: vault:latest
  container_name: vault
  ports:
    - 8200:8200
  environment:
    VAULT_ADDR: "http://127.0.0.1:8200"
    # in production scenarios the token mechanisim for loggin will be forbidden,
    # it cannot be in source control.
    VAULT_TOKEN: "vault-distribt-token" # to be able to use the cli on dev
    VAULT_DEV_ROOT_TOKEN_ID: "vault-distribt-token"
  cap_add:
    - IPC_LOCK
  depends_on:
    - rabbitmq

nota: la imagen que utilizamos es de desarrollo, la enterprise (con licencia) es diferente, en la de desarrollo como podemos ver tenemos una variable llamada VAULT_TOKEN y otra VAULT_DEV_ROOT_TOKEN_ID ambas con el valor “vault-distribt-token” el cual es la credencial para conectarnos al servicio. En producción esta credencial no está disponible por seguridad, y deberás crear un usuario/contraseña.

 

Además, podemos ejecutar en el propio docker scripts o comandos en la CLI para crear dichos secretos. Lo normal es que los secretos se creen fuera de la aplicación, igual que hicimos en el post anterior para la infraestructura de RabbitMQ, separaremos la infraestructura del código, en este caso, las credenciales son la infraestructura.

 

 

Estos comandos los vamos a ejecutar desde un script escrito en bash, y ¿por qué en bash? porque un 20% de los usuarios que visitan mi web lo hacen a través de Linux o Mac y ellos no pueden ejecutar powershell, pero los de windows si podemos ejecutar bash. 

#!/bin/bash
docker exec -it vault vault kv put secret/rabbitmq username=DistribtAdmin password=DistribtPass

Ejecutando este comando ya tendremos nuestras credenciales dentro del secret manager.

 

3.1 - Implementación de vault con .NET

Ahora vamos a pasar a la parte del código, donde abstraemos la funcionalidad; Lo primero de todo es crear el proyecto que va a contener el acceso a Vault.

En mi caso va a estar en Distribt.Shared.Secrets.

 

Dentro de esta librería vamos a importar la librería que se va a comunicar con Vault, en nuestro caso será VaultSharp, la cual no es la oficial de hashicorp, pero tiene el triple de descargas al día desde NuGet y por lo que he estado viendo, es la recomendada.

 

Ahora lo que debemos hacer es una interfaz la cual nos devuelva el resultado al acceder a la información de nuestro engine, osea que nos devuelva la configuración con la clave.

public interface ISecretManager
{
    Task<T> Get<T>(string path) where T : new();
}

 

Después vamos a implementar la clase que implementará la interfaz. Recuerda que ahora nos vamos a conectar a Vault, por lo tanto, necesitamos el token que hemos especificado en el fichero docker-compose anterior. 

 

Esta acción se realiza a través de variables de entorno y se hace así para que dicho valor NO se encuentre en el código fuente de la aplicación. Oobviamente en producción/uat/test, en local da igual.

 

Por ello leemos de la variable de entorno el valor:

internal class VaultSecretManager : ISecretManager
{
    private readonly VaultSettings _vaultSettings;

    public VaultSecretManager(IOptions<VaultSettings> vaultSettings)
    {
        _vaultSettings = vaultSettings.Value with { TokenApi = GetTokenFromEnvironmentVariable() };
    }

    private string GetTokenFromEnvironmentVariable()
        => Environment.GetEnvironmentVariable("VAULT-TOKEN") //"vault-distribt-token"
           ?? throw new NotImplementedException("vault is not implemented into the system");
}

En producción esta variable de entorno será inyectada durante el despliegue, ya bien lo hagas con octopus, manualmente, etc. Pero en desarrollo tienes que modificar cada proyecto donde la quieres utilizar y modificar el fichero launchSettings.json y modificar en tu perfil las variables de entorno para añadir la que necesitamos:

{
  ...
  "profiles": {
    "Distribt.API.Public": {
      ...
      "environmentVariables": {
        ...
        "VAULT-TOKEN": "vault-distribt-token"
      }
    },
  }
}

Ahora únicamente nos queda implementar la función Get la cual se conectara a Vault y obtendrá las credenciales. nota, hay que tener en cuenta que vault nos devuelve la información, aunque sea en un objeto en formato diccionario y debemos crear un método para convertir dicho diccionario en nuestro objeto T

por cierto, la propiedad mountPoint dentro de ReadSecretAsync es lo que hemos mencionado antes sobre el engine key value de vault.

public async Task<T> Get<T>(string path) 
    where T : new()
{
    VaultClient client = new VaultClient(new VaultClientSettings(_vaultSettings.VaultUrl,
        new TokenAuthMethodInfo(_vaultSettings.TokenApi)));

    Secret<SecretData> kv2Secret = await client.V1.Secrets.KeyValue.V2
        .ReadSecretAsync(path: path, mountPoint: "secret");
    var returnedData = kv2Secret.Data.Data;

    return returnedData.ToObject<T>();
}

public static class ObjectExtensions
{
    public static T ToObject<T>(this IDictionary<string, object> source) where T : new()
    {
        var someObject = new T();
        var someObjectType = someObject.GetType();

        foreach (var item in source)
        {
            someObjectType
                .GetProperty(item.Key)!
                .SetValue(someObject, item.Value, null);
        }
        return someObject;
    }
}

 

Finalmente añadimos nuestro secret manager al contenedor de dependencias:

public static void AddVaultService(this IServiceCollection serviceCollection, IConfiguration configuration)
{
    serviceCollection.Configure<VaultSettings>(configuration.GetSection("SecretManager"));
    serviceCollection.AddScoped<ISecretManager, VaultSecretManager>();
}

///Dentro de nuestro proyecto setup 
public static void AddSecretManager(this IServiceCollection serviceCollection, IConfiguration configuration)
{
    serviceCollection.AddVaultService(configuration); 
}

y ahora podemos inyectar ISecretManager dentro de nuestros servicios para recoger las credenciales.

 

Para ver su funcionamiento podemos ir al tipo RabbitMQMessagePublisher que creamos en el post anterior e inyectar ISecretManager para recoger las credenciales. 

Nota: este código, necesita algo de refactor, ya que estas credenciales son asignadas en el constructor, y no deberían ser asignadas en el constructor ya que el .Result bloquea el hilo, además si la implementación fuera singleton, las credenciales no se refrescaran nunca y tendríamos que parar la aplicación para cargar las nuevas. Pero para el ejemplo, nos sirve.

public RabbitMQMessagePublisher(ISerializer serializer, IOptions<RabbitMQSettings> settings, ISecretManager secretManager)
{
    _settings = settings.Value;
    _serializer = serializer;
    var credentials = secretManager.Get<RabbitMQCredentials>("rabbitmq").Result; //Not ideal (used for the example).
    _connectionFactory = new ConnectionFactory()
    {
        HostName = _settings.Hostname,
        Password = credentials.username,
        UserName = credentials.password
    };
}

 

3.2 - Configurar RabbitMQ con Vault

Como he mencionado, Vault nos permite añadir un secret engine, lo que viene significando un plugin que se va a conectar a nuestra aplicación. En nuestro caso utilizaremos el de RabbitMQ, del que tienes la documentación disponible aquí pero hay muchos más.

 

Lo primero que tenemos que hacer es modificar nuestro script para añadir los comandos que van a conectar Vault con RabbitMQ

docker exec -it vault vault secrets enable rabbitmq

docker exec -it vault vault write rabbitmq/config/connection \
    connection_uri="http://rabbitmq:15672" \
    username="DistribtAdmin" \
    password="DistribtPass" \

docker exec -it vault vault write rabbitmq/roles/distribt-role \
    vhosts='{"/":{"write": ".*", "read": ".*"}}'

 

Como puedes ver, estamos habilitando el plugin de rabbitmq y configurando el usuario y la contraseña a utilizar (en producción esto se haría en la definición de la infraestructura). Si te fijas bien la uri es http://rabbitmq y es debido a que vamos a utilizar la red de docker.

 

Y finalmente un rol que va a tener acceso de lectura y escritura al host de RabbitMQ.

 

Lo que Vault va a hacer en verdad por detrás es, utilizando las credenciales que hemos asignado, crear un usuario de uso “privado”, en este ejemplo el usuario y la contraseña no van a expirar, pero podemos aplicar políticas al rol que lo hagan expirar, etc. 

 

vemos como al ejecutar tenemos el usuario en RabbitMQ:

vault create user in rabbitMQ

Y ahora modificamos el servicio para recoger las credenciales desde el motor de RabbitMQ

public async Task<UsernamePasswordCredentials> GetRabbitMQCredentials(string roleName)
{
    VaultClient client = new VaultClient(new VaultClientSettings(_vaultSettings.VaultUrl,
        new TokenAuthMethodInfo(_vaultSettings.TokenApi)));

    Secret<UsernamePasswordCredentials> secret = await client.V1.Secrets.RabbitMQ
        .GetCredentialsAsync(roleName, "rabbitmq");
    return secret.Data;
}

Y vemos cómo obtenemos el usuario y la contraseña que hemos creado:

implementación vault rabbitmq en c#Y el mensaje se envía sin ningún problema.

 

 

Finalmente, los secrets probablemente sean un elemento que vas a necesitar en cada uno de tus microservicios, ya bien sea para coger la configuración de RabbitMQ, o en el futuro, para la base de datos, asi que no olvides que puedes añadirlo dentro de DefaultDistribtWebApplication, así los desarrolladores que lo utilicen, no tendrán que añadir nada en su código.

public static WebApplication Create(Action<WebApplicationBuilder>? webappBuilder = null)
{
    WebApplicationBuilder builder = WebApplication.CreateBuilder();
    ....
    builder.Services.AddVaultService(builder.Configuration); 

    if (webappBuilder != null)
    {
        webappBuilder.Invoke(builder);
    }

    return builder.Build();
}

 

Conclusión

En este post hemos visto que es un gestor de credenciales y porque es importante tener uno.

Hemos visto cómo implementar un gestor de credenciales con Vault 

Hemos visto cómo implementar diferentes motores en Vault

 

 


Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2024 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café