AKS, KeyVault y netcore

¡Buenas! Vamos a ver en este artículo como podemos leer secretos almacenados en un Azure Key Vault desde nuestro código netcore ejecutándose en un AKS.

A diferencia de ACR donde contamos con una integración nativa en la cual nos basta con usar un service account de AKS vinculado a un service principal de Azure que tenga permisos de lectura contra el ACR (escribiéndolo veo que esto da para un pequeño futuro post), para Key Vault no tenemos integración nativa.

Por supuesto siempre puedo usar la API de Key Vault y acceder al Key Vault por código. Así, si estoy usando .Net Core puedo usar el proveedor de configuración para Azure Key Vault y santas pascuas. Para otros lenguajes pues me tocará pelearme con la API de Key Vault o buscar a alguien que lo haya hecho.

No es lo que veremos en este post. Aquí quiero mostrar como usar una librería que nos permite mapear los secretos de un Key Vault a un volúmen de nuestros pods. De este modo, acceder a Key Vault se traduce en «leer de un directorio». No puede ser más sencillo.

Esta librería usa una característica de Kubernetes llamada FlexVolume. Básicamente FlexVolume es un mecanismo que nos permite crear adaptadores de volúmenes para distintas fuentes de información. No quiero entrar en demasiados detalles sobre como funciona FlexVolume (daría para otro post), para lo que nos ocupa basta saber eso, que nos permite, de forma sencilla, crear adaptadores para crear volúmenes para determinadas fuentes de información.

Usando el FlexVolume para Azure Key Vault

Puedes instalar la librería ya sea como add-on de AKS Engine o bien «a mano». Como nunca he hablado de AKS Engine en este blog (todo llegará), vamos a ver la manera de instalarlo a mano que además no puede ser más sencillo:

kubectl create -f https://raw.githubusercontent.com/Azure/kubernetes-keyvault-flexvol/master/deployment/kv-flexvol-installer.yaml

Esto te creará un namespace nuevo (kv) que contiene un daemonset (keyvault-flexvolume) que ejecuta un pod por cada nodo:

kubectl mostrando el daemonset y los pods

Para configurar keyvault-flexvol y darle acceso al Key Vault, podemos usar dos mecanismos: un service principal o bien pod identity. No hemos hablado de pod identity, así que voy a usar un service principal. Cuando en un futuro post introduzca pod identity, ya veremos el correspondiente ejemplo (ufff, cuantos futuros posts están saliendo, ¿no? xD).

Usar un service principal implica básicamente pasarle a Kubernetes (usando un secreto) las credenciales de un service principal que tenga acceso al Key Vault. Así que lo primero es tener a mano un service principal. Si no lo tienes puedes crear uno usando:

az ad sp create-for-rbac

Y te devolverá un JSON con el «appId» y el «Password» (login y password).

Ahora debes crear el secreto en el clúster:

kubectl create secret generic keyvaultreader --from-literal clientid=<appId> --from-literal clientsecret=<password> --type=azure/kv

Esto crea el secreto llamado keyvaultreader. Si haces «kubectl get secret» te debería salir un secreto llamado keyvaultreader de tipo azure/kv.

Vale, el siguiente paso claro, es crear un keyvault, ya que si no poca cosa vamos a probar. Con el siguiente código creas el keyvault y le damos permisos de lectura al service principal:

az keyvault create -g <grupo-recursos> -n <nombre-keyvault>
az role assignment create --role Reader --assignee <service-principal-appid> --scope /subscriptions/<id-suscripcion>/resourcegroups/<grupo-recursos>/providers/Microsoft.KeyVault/vaults/<nombre-keyvault>
az keyvault set-policy -n <nombre-keyvault> --key-permissions get --spn <service-principal-appid>
az keyvault set-policy -n <nombre-keyvault> --secret-permissions get --spn <service-principal-appid>
az keyvault set-policy -n <nombre-keyvault> --certificate-permissions get --spn <service-principal-appid>

Vamos a crear dos secretos (secret1 y secret2) para poder hacer la prueba:

az keyvault secret set --vault-name <nombre-keyvault> -n secret1 --value "42 is the answer"
az keyvault secret set --vault-name <nombre-keyvault> -n secret2 --value "But the question remains unknown"

¡Perfecto! Ya tenemos las credenciales del service principal en el clúster (keuvaultreader), y el service principal tiene permisos de lectura al Key Vault donde tenemos dos secretos.

Configurar el volúmen en el deployment

Para montar el volúmen flexvolume y así tener acceso a los secretos del KeyVault debemos definir el volumen. Nuestra definición del deployment queda así:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: test-kv
spec:
  template:
    metadata:
      labels:
        app: test-kv
        component: test-kv
    spec:
      containers:
      - name: test-kv
        image: testedu.azurecr.io/demokv
        imagePullPolicy: Always
        ports:
        - containerPort: 80        
        volumeMounts:
        - name: my-kv
          mountPath: /kv-data
          readOnly: true        
      imagePullSecrets:
      - name: acrsecret
      volumes:
      - name: my-kv
        flexVolume:
          driver: "azure/kv"
          secretRef:
            name: keyvaultreader               
          options:
            keyvaultname: <nombre-keyvault>            
            keyvaultobjectnames: secret1;secret2    
            keyvaultobjecttypes: secret;secret  
            resourcegroup: <grupo-recursos-keyvault>              
            subscriptionid: <id-subscripción-azure>
            tenantid: <id-tenant-keyvault>

En el volumen debemos pasarle:

  1. El nombre, grupo de recursos, id suscripción y id del tenant del KeyVault
  2. El nombre de los secretos a recuperar separados por punto y coma (en este caso secret1 y secret2).
  3. El tipo de los recursos (si son certificados o secretos)

Luego usando volumeMounts montamos este volumen en el path /kv-data del pod.

No es necesario nada más. Ahora si creamos este deployment y ponemos en marcha el pod, veremos que en el directorio /kv-data aparece un fichero por cada secreto del Key Vault. El nombre del fichero es el nombre del secreto y su contenido es el secreto en sí:

Se muestra como el pod tiene el directorio con los secretos

Leer los secretos desde ASP.NET Core

Vale, hemos visto como podemos obtener los secretos como un volúmen de nuestro pod. Veamos ahora como leerlos desde ASP.NET Core. Por supuesto podríamos leer «a mano» los ficheros del directorio cuando necesitemos el valor de un secreto, pero así no es como hacemos las cosas en ASP.NET Core. Lo que nos interesa es que estos valores estén en la configuración de la aplicación y que se integren con el resto de valores que provengan de otras fuentes (variables de entorno, fichero appsettings.json, etc).

Por suerte eso es muy sencillo, basta con crear un proveedor de configuración nuevo y añadirlo a nuestra aplicación. Vamos a partir de un proyecto ASP.NET Core con la plantilla Empty.

El primer paso es crear el IConfigurationSource que nos indica desde donde leemos los valores de configuración y devuelve el objeto que debe leer desde esa fuenteEn nuestro caso se trata de un directorio:

public class FolderConfigurationSource : IConfigurationSource
{
    public string Folder { get; }

    public FolderConfigurationSource(string folder)
    {
        Folder = folder;
    }

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new FolderConfigurationProvider(this);
    }
}

Bien, ahora nos toca crear el FolderConfigurationProvider que es el encargado de leer los valores de configuración a partir de un FolderConfigurationSource:

public class FolderConfigurationProvider : ConfigurationProvider
{

    private readonly FolderConfigurationSource _source;
    public FolderConfigurationProvider(FolderConfigurationSource source)
    {
        _source = source;
    }

    public override void Load()
    {
        var entries = Directory.EnumerateFiles(_source.Folder);
        foreach (var entry in entries)
        {
            var name = Path.GetFileName(entry);
            Data.Add(name, File.ReadAllText(entry));
        }
    }
}

Más fácil imposible, ¿no? Leemos todos los ficheros de la carpeta (representada por el FolderConfigurationSource) y los agregamos al diccionario Data que obtenemos de la clase base ConfiurationProvider.

Finalmente nos queda ver como agregar el objeto FolderConfigurationSource, la forma habitual es usando un método de extensión sobre IConfigurationBuilder, aunque este método es opcional, pero facilita el uso de nuestro proveedor a los clientes:

public static class FolderConfigurationExtensions
{
    public static IConfigurationBuilder AddFolder(this IConfigurationBuilder builder, string folderName) =>
        builder.Add(new FolderConfigurationSource(folderName));
}

Ahora debemos añadir este proveedor de configuración a nuestra aplicación. Para ello editamos el método CreateWebHostBuilder de la clase Program:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration(cb => cb.AddFolder("/kv-data"))
        .UseStartup<Startup>();

Observa como usamos el método de extensión «AddFolder» que hemos definido antes.

Ahora simplemente edita la clase Startup para que quede como a continuación:

public class Startup
{
    public IConfiguration Configuration { get; }
    public Startup(IConfiguration conf)
    {
        Configuration = conf;
    }

    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.Run(async (context) =>
        {
            var entries = Configuration.AsEnumerable();
            foreach (var entry in entries)
            {
                await context.Response.WriteAsync($"{entry.Key} = {entry.Value}\n");
            }
            
        });
    }
}

No hay ningún secreto: esto levanta un servidor que para cualquier petición que reciba hará lo mismo: mostrar todos los valores que hay en el objeto de configuración global.

Bien, este código es el que tengo yo en la imagen testedu.azurecr.io/demokv que hemos usado antes. En el clúster no he desplegado ningún servicio ni nada, así que no tengo acceso al pod desde el exterior. Pero para mostrar que funciona tampoco lo necesito: puedo abrir una sesión interactiva de terminal contra el pod y ejecutar curl (que está presente en las imágenes de runtime de ASP.NET Core), hacer una petición a localhost y ver el resultado:

Sesión interactiva contra el pod donde se ve que el objeto IConfiguration contiene los valores del keyvault

Observad como se me muestra todo el contenido del objeto IConfiguration que contiene las variables de entornio, entradas que hubiese en appsettings.json pero también los dos secretos del Key Vault. De este modo, los valores del Key Vault participan del sistema de configuración estándard de ASP.NET Core.

Más fácil… imposible, ¿no?

¡Espero que os haya resultado interesante!

 

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *