Hashicorp Vault : mise en place sur un kube vanilla

Hashicorp Vault : mise en place sur un kube vanilla
Photo by regularguy.eth / Unsplash

Hashicorp est bien connu des DevOps grâce à ses outils toujours bien foutus, permettant de gagner du temps un peu partout. Ici, intéressons-nous à leur outil Vault.

Cet outil vous permettra de gérer vos secrets de votre infra et voir même de les générer dynamiquement avec une gestion du cycle de vie et droits. Vous pouvez avoir votre app qui utilise un rôle que l'administrateur aura préalablement défini pour en limiter la portée, et qui utilisera un token pour elle seule. Ce token aura une durée de vie limitée dans le temps afin de réduire le risque d'être compromis. Et si celle-ci est compromise alors la portée de la compromission étant déjà basse, selon le rôle que vous lui aurez donné, pourra être révoqué sur ce crédential uniquement sans pour autant remettre en cause l'entièreté du Vault.

Vous pourrez trouver dans la doc ce qu'il vous faut pour l'installer dans votre environnement, de mon côté je vais simplement le mettre en place dans mon cluster Kubernetes et le connecter à mon LDAP pour l'authentification pour y stocker des secrets que je pourrais utiliser dans le cluster par la suite. L'installation passera par un Helm chart, la méthode préconisée par Hashicorp. Pas fan de Helm mais je préfère rester proche de la méthode upstream plutôt de partir sur des installations perso. A la limite, cela pourra venir dans un second temps.

Vous trouverez les instructions détaillées ici, et tout un tas de guide, best practices pour Kubernetes ici. De mon côté, j'utilise un CSI NFS et Longhorn, ce dernier étant ma storage class par défaut. Le problème de NFS étant que la gestion des droits sur les fichiers est très embêtante, je ne vais revenir sur le sujet ici mais vous aurez un aperçu sur ce ticket Github #183.

Installation

Bref, on suit la documentation et on paramètre les valeurs pour customiser un peu son installation. J'aime bien téléchargé la version entière du fichier de valeurs Helm pouvant être surchargée avec ses commentaires pour voir un peu quoi changer. Mon installation se rapproche de l'exemple fourni par Hashicorp avec un setup standalone utilisant TLS.

Une fois installé, nous avons des pods dans l'état suivant:

$ kc get pod
NAME                                    READY   STATUS    RESTARTS   AGE
vault-0                                 0/1     Running   0          20h
vault-agent-injector-55949dd9f4-tnpgg   1/1     Running   0          20h

Une dernière étape consiste à initialiser le Vault, ce qui générera les clés servant à ouvrir à la base à son démarrage. Vault utilise 5 clés afin de sceller la base de données, celle-ci étant chiffrée, lors du démarrage il est nécessaire d'utiliser les clés pour accéder à son contenu.

Les clés ne sont pas utilisées pour déchiffrer directement la base, celles-ci déchiffrent la clé root de la base qui elle même déchiffre la base. Tout est expliqué ici.

Initialisation

Procéder à son initialisation, j'ai eu un petit soucis entre HTTP et HTTPS avant de suivre l'exemple du setup standalone TLS mis plus haut. Bref, je n'avais pas modifié la config du listener tcp pour la config standalone qui avait toujours la ligne:

listener "tcp" {
  tcp_disable: 1
...

Et il manquait aussi les certificats dans cette configuration de listener. Avec la mauvaise configuration l'erreur retournée était:

Get "https://127.0.0.1:8200/v1/sys/init": http: server gave HTTP response to HTTPS client

Pour l'initialisation, il faut exécuter la commande suivante:

$ kubectl exec --stdin=true --tty=true vault-0 -- vault operator init
Unseal Key 1: KEY1
Unseal Key 2: KEY2
Unseal Key 3: KEY3
Unseal Key 4: KEY4
Unseal Key 5: KEY5

Initial Root Token: Root_Token
......
......

Voilà, on a nos clés et le Vault initialisé, désormais ouvrons le Vault avec les "Unseal Key" que nous détenons:

kubectl exec --stdin=true --tty=true vault-0 -- vault operator unseal KEY1
kubectl exec --stdin=true --tty=true vault-0 -- vault operator unseal KEY2
kubectl exec --stdin=true --tty=true vault-0 -- vault operator unseal KEY3
kubectl exec --stdin=true --tty=true vault-0 -- vault operator unseal KEY4
kubectl exec --stdin=true --tty=true vault-0 -- vault operator unseal KEY5

Si vous avez une configuration HA avec plusieurs replicas, il faudra ouvrir la base avec les clés sur chacun des réplicas vault-X.

Voilà, on a un Vault utilisable désormais.

Configuration de l'authentification LDAP et de la politique de sécurité pour l'admin

Après l'initialisation, nous avons reçu le root token nous permettant d'opérer en tant qu'administrateur sur le Vault. La bonne pratique est de révoquer ce token d'utiliser une authentification tierce de confiance, dans mon cas, LDAP. Plusieurs autres backend d'authentification sont disponible si vous préférez utiliser un Github, Active Directory, etc.

Commençons:

$ kubectl exec --stdin=true --tty=true vault-0 -- vault auth enable ldap
Error enabling ldap auth: Error making API request.

URL: POST https://127.0.0.1:8200/v1/sys/auth/ldap
Code: 403. Errors:

* permission denied
command terminated with exit code 2

Ok, petit oubli d'utilisation du token root.

$ kubectl exec --stdin=true --tty=true vault-0 -- sh -c "VAULT_TOKEN=hvs.p24FqUlkm84860KadYSKOnQ9 vault auth enable ldap"

Pour la configuration LDAP, ce sera plus simple de se mettre dans un shell:

$ kubectl exec --stdin=true --tty=true vault-0 -- sh
/ $ vault write auth/ldap/config \
    url="ldap://gilgamesh.uruk.home" \
    userattr="uid" \
    userdn="ou=Users,dc=uruk,dc=home" \
    groupdn="ou=Users,dc=uruk,dc=home" \
    groupfilter="(&(objectClass=person)(uid={{.Username}}))" \
    groupattr="memberOf" \
    starttls=false
Success! Data written to: auth/ldap/config

Testons l'identification avec un utilisateurs LDAP maintenant :

/ $ vault login -method=ldap username=claneys
Password (will be hidden):
WARNING! The VAULT_TOKEN environment variable is set! The value of this
variable will take precedence; if this is unwanted please unset VAULT_TOKEN or
update its value accordingly.

Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                    Value
---                    -----
token                  User.token
token_accessor         a_token_accessor
token_duration         768h
token_renewable        true
token_policies         ["default"]
identity_policies      []
policies               ["default"]
token_meta_username    claneys

On voit un warning car nous utilisons la variable d'environnement VAULT_TOKEN avec le root token pour le bootstrap de notre Vault. Une fois le backend d'authentification configuré avec les politiques de sécurité en place, nous pourrons révoquer ce token. Celui-ci peut être recréer au besoin dans tous les cas.

Maintenant, créons une politique de sécurité pour notre utilisateur. Cette politique sera assez permissive afin d'être administrateurs du Vault. Je reprends et adapte une politique trouver sur une site pour bootstrapper ma politique et cela donne :

# Read system health check
path "sys/health"
{
  capabilities = ["read", "sudo"]
}

# Create and manage ACL policies broadly across Vault

# List existing policies
path "sys/policies/acl"
{
  capabilities = ["list"]
}

# Create and manage ACL policies
path "sys/policies/acl/*"
{
  capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}

# Allow a token to manage its own kv store
path "kv/*" {
    capabilities = ["create", "read", "update", "delete", "list"]
}

# Allow a token to manage its own cubbyhole
path "cubbyhole/*" {
    capabilities = ["create", "read", "update", "delete", "list"]
}

# Enable and manage authentication methods broadly across Vault

# Manage auth methods broadly across Vault
path "auth/*"
{
  capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}

# Create, update, and delete auth methods
path "sys/auth/*"
{
  capabilities = ["create", "update", "delete", "sudo"]
}

# List auth methods
path "sys/auth"
{
  capabilities = ["read"]
}

# Enable and manage the key/value secrets engine at  path

# List, create, update, and delete key/value secrets
path "secret/*"
{
  capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}

# Manage secrets engines
path "sys/mounts/*"
{
  capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}

# List existing secrets engines.
path "sys/mounts"
{
  capabilities = ["read"]
}

Et on fait le lien avec notre utilisateur LDAP, on va simplement prendre le groupe existant Administrators duquel il fait parti pour le lié à la politique admin. Depuis un shell dans le container vault-0 :

$ vault write auth/ldap/groups/Administrators policies=default,admin
Success! Data written to: auth/ldap/config

On se relog pour voir le changement :

/ $ vault login -method=ldap username=claneys
Password (will be hidden):
WARNING! The VAULT_TOKEN environment variable is set! The value of this
variable will take precedence; if this is unwanted please unset VAULT_TOKEN or
update its value accordingly.

Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                    Value
---                    -----
token                  User.token
token_accessor         a_token_accessor
token_duration         768h
token_renewable        true
token_policies         ["admin", "default"]
identity_policies      []
policies               ["admin", "default"]
token_meta_username    claneys

Et on revoke le token root :

$ vault token revoke token_root

Bonus: Création de l'ingressroute Traefik pour accéder à l'interface Web

L'UI est servi par un serveur Web qui gère son certificat TLS et donc si l'on veut pouvoir y accéder nous allons passer par une ingressroute TCP et simplement rerouter le traffic d'un nom d'hôte vers le service Kubernetes. On prendra soin de spécifier de faire du passthrough pour le TLS et on est bon :

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP
metadata:
  name: vault-tls-route
  namespace: vault
spec:
  routes:
  - match: HostSNI(`vault.uruk.home`)
    services:
    - name: vault
      port: 8200
  tls:
    passthrough: true

On applique :

$ kc apply -f vault-ingressroute.yaml
ingressroutetcp.traefik.containo.us/vault-tls-route created