Imagen de Tobias Aeppli en Pexel.

Muchas veces estoy viendo el uso de placeholders para guardar secretos o bien para establecer configuraciones en los ficheros de deploy de Kubernetes que luego se montan como volumnes con texto plano (como mucho en base64). Y tantas veces vuelvo a comentar que es una mala práctica… cómo no quiero volver a contarlo de viva voz una y otra vez; sirva este artículo como explicación más plausible de entrada a las buenas prácticas que debemos tener en Kubernetes con lo secretos o este documento de Microsoft.

Existen varias opciones que nos permiten tener bien asegurado nuestro entrono:

En el mundo cloud es una práctica habitual inyectar valores y secretos como variables de entorno. Cumplen el 12Factor. Y es la práctica que se adopta por ejemplo con la implementación estandar de Kubernetes o usando Azure Key Vault.

¡Atención!

La práctica de placeholders la vengo observando mucho últimamente. Si te encuentras con ese mala práctica, contacta conmigo por cualquier canal de los que tengo disponibles y vemos cómo tratar tu caso para que puedas hacer fuerza y explicar por qué no debe realizarse así. La ayuda es 100% desinteresada.

Kubernetes ya nos ofrece un mecanismo para manejar secretos y ponerlos a disposición de la aplicación, ya sea montándolo en pods o como un volumen o inyectándolo en el entorno. Pero tiene una gran desventaja: se almacenan como cadenas base64 y que te obligan a usar kubectl o bien en el manifiesto.

Si estas en Azure y teniendo Azure Key Vault, ¿por qué no lo usas? Esa es una de mis grandes preguntas y lo primero que cuento cuando veo una mala práctica.

Azure Key Vault, ya de por sí tiene un sistema de cifrado muy sólido incluso puedes pagar más para usar HSM.

Por eso en este artículo, sencillo y conciso os voy a explicar como poner una seguridad mínimamente aceptable para tu entorno.

Manos a la obra: Helm y add-pod-identity

Los requisitos:

  • Azure CLI.
  • Acceso a una suscripción de Azure con permisos adecuados.
  • Tener instalado kubectl.
  • Helm.

Yo voy a usar PowerShell en Windows 10.

Y lo primero es entrar en Azure:

az login

No situamos en nuestra suscripción:

$env:subId=az account show --query "id" -o tsv
az account set --subscription $env:subId

Creamos el cluster de Kubernetes:

az group create --name aksakvrg --location westeurope
az aks create --resource-group aksakvrg --name k8sakv
   --node-count 1 
   --generate-ssh-keys 
   --enable-managed-identity 
   --network-plugin azure
az aks get-credentials --resource-group aksakvrg --name k8sakv

Creamos el Key Vault y añadimos un secreto:

az keyvault create --name akvk8s --resource-group aksakvrg --location westeurope
az keyvault secret set --vault-name akvk8s --name "YourSecret" --value "YourVerySecretValue"

Vamos a permitir que AKS pueda acceder al Azure Key Vault. Para ello tendrás que tener Helm (pasos para instalar):

helm repo add csi-secrets-store-provider-azure 
  https://azure.github.io/secrets-store-csi-driver-provider-azure/charts
helm install csi-secrets-store-provider-azure/csi-secrets-store-provider-azure 
  --generate-name --set secrets-store-csi-driver.syncSecret.enabled=true

Revisamos los pods:

Llega el momento de que Kubernetes tenga acceso contra Azure Key Vault para ello debemos dar acceso mediante Azure Active Directory bien via entidades administradas (managed identities) o bien con una endidad de servicio (service principal).

En este ejemplo vamos a ir por la opción de managed identities. Y en esta imagen de la documentación oficial de Microsoft podrás ver que es lo que estamos realizando.

Vamos a otorgar a K8s y sus nodos acceso a las managed identities de Azure. Por tanto, vamos a obtener un ID de cliente y el grupo de recursos de los nodos de K8s. Tras esto, toca asignar roles.

$env:clientId=az aks show --name k8sakv --resource-group aksakvrg 
  --query "identityProfile.kubeletidentity.clientId" -o tsv
$env:nodeResourceGroup=az aks show --name k8sakv --resource-group aksakvrg 
  --query "nodeResourceGroup" -o tsv
$env:subId=az account show --query "id" -o tsv

az role assignment create --role "Managed Identity Operator" 
  --assignee $env:clientId --scope /subscriptions/$env:subId/resourcegroups/$env:nodeResourceGroup
az role assignment create --role "Virtual Machine Contributor" 
  --assignee $env:clientId --scope /subscriptions/$env:subId/resourcegroups/$env:nodeResourceGroup

Ahora vamos a añadir el pod de AAD que nos permitirá hacer uso de las managed indentities en los pods de Kubernetes para que se autentiquen con Azure Key Vault.

helm repo add aad-pod-identity https://raw.githubusercontent.com/Azure/aad-pod-identity/master/charts
helm install pod-identity aad-pod-identity/aad-pod-identity

Veamos si esta todo correcto:

El siguiente paso es crear una Azure Managed Identity:

az identity create -g aksakvrg -n akskvIdentity

Establecemos permisos de lectura:

$env:clientId=az identity show --name akskvIdentity 
  --resource-group aksakvrg --query "clientId" -o tsv
$env:principalId=az identity show --name akskvIdentity 
  --resource-group aksakvrg --query "principalId" -o tsv
$env:subId=az account show --query "id" -o tsv
az role assignment create --role "Reader" --assignee $env:principalId 
  --scope /subscriptions/$env:subId/resourceGroups/aksakvrg/providers/Microsoft.KeyVault/vaults/akvk8s
az keyvault set-policy -n akvk8s --secret-permissions get --spn $env:clientId
az keyvault set-policy -n akvk8s --key-permissions get --spn $env:clientId

Creamos un enlace con Azure Identity, para ello deberás crear un fichero llamado AzureIdentity.yaml y sustiturir los valores de las suscripción y el cliente con lo que se guardó en las variables de entorno:

echo $env:clientId
echo $env:subId

El fichero:

apiVersion: aadpodidentity.k8s.io/v1
kind: AzureIdentity
metadata:
  name: "aks-kv-identity"               
spec:
  type: 0                                 
  resourceID: /subscriptions/<subId>/resourcegroups/aksakvrg/providers
    /Microsoft.ManagedIdentity/userAssignedIdentities/akskvIdentity
  clientID: "<clientId>"
---
apiVersion: aadpodidentity.k8s.io/v1
kind: AzureIdentityBinding
metadata:
  name: azure-pod-identity-binding
spec:
  azureIdentity: "aks-kv-identity"      
  selector: azure-pod-identity-binding-selector

Aplicamos el manifiesto:

kubectl apply -f AzureIdentity.yaml

Si todo fue con exito deberías tener un resultado similar a:

azureidentity.aadpodidentity.k8s.io/aks-kv-identity created
azureidentitybinding.aadpodidentity.k8s.io/azure-pod-identity-binding created

Sacamos el valor del tenant, si no lo tienes de antes, puedes usar:

$env:tenantId=az account show --query "tenantId" -o tsv
echo $env:tenantId

Vamos a crear otro manifiesto llamado SecretProviderClass.yaml:

apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: spc-k8skv
spec:
  provider: azure
  secretObjects:
  - secretName: test-secret
    data:
    - key: key
      objectName: YourSecret
    type: Opaque
  parameters:
    usePodIdentity: "true"
    useVMManagedIdentity: "false"
    userAssignedIdentityID: "<clientId>"
    keyvaultName: "akvk8s"
    cloudName: ""       
    objects:  |
      array:
        - |
          objectName: YourSecret
          objectType: secret
          objectVersion: ""
    resourceGroup: "aksakvrg"
    subscriptionId: "<subId>"      
    tenantId: "<tenantId>"            

Aplicamos el manifiesto:

kubectl apply -f SecretProviderClass.yaml

Montamos el secreto en la aplicación. Para ello tenemos el siguiente Pod. Y el manifiesto será Pod.yaml:

kind: Pod
apiVersion: v1
metadata:
  name: busybox-secrets-store-inline-system-msi
  labels:
    aadpodidbinding: azure-pod-identity-binding-selector  
spec:
  containers:
    - name: busybox
      image: k8s.gcr.io/e2e-test-images/busybox:1.29-1
      command:
        - "/bin/sleep"
        - "10000"
      volumeMounts:
      - name: secrets-store-inkeyvault
        mountPath: "/mnt/secrets-store"
        readOnly: true
  volumes:
    - name: secrets-store-inkeyvault
      csi:
        driver: secrets-store.csi.k8s.io
        readOnly: true
        volumeAttributes:
          secretProviderClass: "spc-k8skv"       

Aplicamos el manifiesto:

kubectl apply -f Pod.yaml

Observamos que tenemos todo correctamente:

Si todo fue como es debido, ya podremos leer la variable de Key Vault como variable de entorno:

Manos a la obra: azure-keyvault-secrets-provider y VM Managed Identity

Esto es una versión mucho más sencilla y como verás con menos pasos.

Los requisitos:

  • Azure CLI.
  • Acceso a una suscripción de Azure con permisos adecuados.
  • Tener instalado kubectl.

Yo voy a usar PowerShell en Windows 10.

Y a continuación todos los pasos con comenarios:

#Step 1
az group create --name aksdemo2-rg --location westeurope

#Step 2
az keyvault create --name aksdemocluster-kv2 
  --resource-group aksdemo2-rg --location westeurope
az keyvault secret set --vault-name aksdemocluster-kv2 
  --name "password-name" --value "password-value"

#Step 3
az aks create --resource-group aksdemo2-rg --name aksdemocluster2 
  --node-count 1 --generate-ssh-keys 
  --enable-managed-identity --network-plugin azure 

#Step 4: New. We are using aks addon for Azure Key Vault
az aks enable-addons --addons azure-keyvault-secrets-provider 
  --name aksdemocluster2 --resource-group aksdemo2-rg

#Step 5
az aks get-credentials --resource-group aksdemo2-rg --name aksdemocluster2

#Step 6: If you haven't got aks-secrets-store-* (2 pods) something is wrong
kubectl get pods -n kube-system -l 'app in (secrets-store-csi-driver, secrets-store-provider-azure)'

#Step 7
$env:cliendId2=az aks show -n aksdemocluster2 -g aksdemo2-rg 
  --query "addonProfiles.azureKeyvaultSecretsProvider.identity.clientId" -o tsv
$env:tenantId2=az account show --query "tenantId" -o tsv
echo $env:cliendId2
echo $env:tenantId2

#Step: 8
az keyvault set-policy -n aksdemocluster-kv2 --secret-permissions get --spn $env:cliendId2    

Ahora creamos los dos manifiestos necesarios.

El primero es SecretProviderClass2.yaml:

apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: azure-kvname-system-msi
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    useVMManagedIdentity: "true"
    userAssignedIdentityID: "<clientId2>"
    keyvaultName: aksdemocluster-kv2
    objects:  |
      array:
        - |
          objectName: password-name
          objectType: secret
          objectVersion: ""
    tenantId: "<tenantId2>"

Y el Pod2.yaml:

kind: Pod
apiVersion: v1
metadata:
  name: busybox-secrets-store-inline-system-msi
spec:
  containers:
    - name: busybox
      image: k8s.gcr.io/e2e-test-images/busybox:1.29-1
      command:
        - "/bin/sleep"
        - "10000"
      volumeMounts:
      - name: secrets-store-inkeyvault
        mountPath: "/mnt/secrets-store"
        readOnly: true
  volumes:
    - name: secrets-store-inkeyvault
      csi:
        driver: secrets-store.csi.k8s.io
        readOnly: true
        volumeAttributes:
          secretProviderClass: "azure-kvname-system-msi"

Desplegamos:

kubectl apply -f SecretProviderClass2.yaml
kubectl apply -f Pod2.yaml

Y revisamos que todo funciona correctamente:

kubectl exec busybox-secrets-store-inline-system-msi -- ls /mnt/secrets-store/
kubectl exec busybox-secrets-store-inline-system-msi -- cat /mnt/secrets-store/YourSecret
Os dejo una pregunta o duda:

¿Qué sería mejor un volumen volatil o un volumen persistente?

Conclusión:

¿Qué se puede hacer mejor? Eso no lo dudo, pero al menos ya tenéis una base sólida para poder tener el mínimo.

Pero como verás os he dejado 2 opciones y si lo combinas son 4 posibilidades.

Que te impidien usarl Helm, pues tiramos de addons, que no puedes usar Helm pues usamos el addons. Pero lo que nunca debes permitir es que si no puedes ni usar Helm ni el addons, es usar los Placheholers… que añaden un punto más inseguridad en tu despliegue, cuantas menos brechas de seguridad mejor.