OpenBao en HA sur Debian 12

OpenBao en HA sur Debian 12

OpenBao est un projet dérivé (fork) de HashiCorp Vault, conçu pour offrir une solution de gestion sécurisée des secrets et des données sensibles, adaptée aux besoins spécifiques des entreprises. Reposant sur l'architecture robuste et éprouvée de HashiCorp Vault, OpenBao reprend les principes fondamentaux de sécurité, de chiffrement des données et de gestion des accès tout en apportant des améliorations et des fonctionnalités propres.

Dans ce LAB nous allons en place un cluster OpenBao, que nous atteindrons au travers d'un Loadbalancer HAproxy.

Attention ! La partie HAproxy a pour seul but de montrer comment le configurer pour un backend OpenBao. En effet dans notre Lab, c'est le SPOF, nous vous recommandons d'utiliser soit un Loadbalancer d'un cloud provider qui est résilient à la panne, soit de mettre en place une solution de master/slave sur le HAproxy.


Prérequis:

  • 3 serveurs sur un réseau que l'on nommera "OPENBAO NETWORK"
  • 1 serveur sur les réseaux "OPENBAO NETWORK" et "CLIENT"
  • Un accès root / sudoer à ceux-ci

L'architecture cible est la suivante:

Installation des .debs

Actuellement, il n'y a pas de repository apt pour Debian. Nous devons donc installer l'application via un .deb fourni par OpenBao.
Sur chaque serveurs exécuter les commandes suivantes:

# Télécharger dans le répertoire temporaire le .deb
cd /tmp && wget https://github.com/openbao/openbao/releases/download/v2.0.1/bao_2.0.1_linux_amd64.deb

# Installation du .deb d'OpenBao
dpkg -i /tmp/bao_2.0.1_linux_amd64.deb

# Activation au démarrage
systemctl enable openbao

# Vérification que le binaire est présent
bao -h

Il faut également vérifier que le swap est désactivé sur votre serveur afin d'éviter des problèmes de sécurité (les données sur le swap peuvent être en clair).

# Afficher les parametres du service
root@inframinds-lab-openbao01:/tmp# systemctl cat openbao

La ligne MemorySwapMax=0 doit apparaitre.

Génération de notre PKI

Nous pouvons utiliser OpenBao comme PKI, l'une des fonctionnalités de la PKI est la génération de certificats.

Afin d'avoir une architecture haute disponibilités et sécurisé d'OpenBao nous devons générer des certificats pour les API de celui-ci.

Or, se pose un problème ...

Comment lui donner un certificat si c'est lui-même qui est censé les générer. Cela donne un problème 🥚 et de 🐔

Habituellement, on doit avoir une Autorité de certification Hors-Ligne, cette Autorité de certification est utilisée pour générer des certificats intermédiaires qui eux sont en ligne (et donc utilisable dans OpenBao)

Nous allons donc:

  • Créer un ROOT CA (qui sera notre Autorité hors ligne).
  • Créer un CA Intermediaire pour générer les certificats pour initier notre cluster OpenBao.
  • Créer un certificat pour nos serveurs OpenBao

Génération du CA Root

Comme il s'agit d'une PKI hors ligne, nous allons exécuter ces commandes sur notre poste local pour simuler un serveur hors ligne.

# Préparation de l'environnement
export CA_NAME=CA_INFRAMINDS_LABS
cd && mkdir $CA_NAME && cd $CA_NAME/

# Génération de la clé RSA (Les elliptic curve ne sont pas supporté ...)
openssl genrsa -out ${CA_NAME}.key 2048

# Autosignature du certificat
openssl req -x509 -new -nodes -key $CA_NAME.key -sha256 -days 3650 -out $CA_NAME.crt -subj '/CN=Inframinds Root CA/C=FR/ST=IDF/L=Paris/O=Inframinds' -addext "keyUsage= critical, cRLSign, digitalSignature, keyCertSign" -extensions v3_ca

Explication des paramètres suivant:

-days 3650  -> Expiration du certificat Root dans 10 ans
-sha256     -> Algorithme de digest
-addext     -> Restriction sur l'usage de la clé
-extensions -> Utilisation du template CA (v3_ca)

Cela nous génère 2 fichiers CA_INFRAMINDS_LABS.key & CA_INFRAMINDS_LABS.crt que nous allons utiliser pour signer les intermédiaires

Génération du CA intermediaire pour l'infrastructure OpenBao

Toujours sur notre serveur Hors ligne (on notre laptop) 😄

export CA_NAME=CA_INFRAMINDS_LABS

# Génération de la clé
openssl genrsa -out ${CA_NAME}_inter.key 2048

# Génération du CSR (un formulaire à signer par le ROOT CA)
openssl req -new -nodes -text -out ${CA_NAME}_inter.csr -subj '/CN=Inframinds intermediate OpenBao CA/C=FR/ST=IDF/L=Paris/O=Inframinds' -addext "basicConstraints=critical,CA:TRUE" -addext "subjectKeyIdentifier=hash" -addext "keyUsage= critical, cRLSign, digitalSignature, keyCertSign" -key ${CA_NAME}_inter.key

# Signature du formulaire et génération du certificat intermédiaire
openssl x509 -req -in ${CA_NAME}_inter.csr -text -sha256 -days 1825 -CA $CA_NAME.crt -CAkey $CA_NAME.key -CAcreateserial -out ${CA_NAME}_inter.crt -copy_extensions copyall

Nous nous retrouvons donc avec ces 4 fichiers:

  • CA_INFRAMINDS_LABS.key
  • CA_INFRAMINDS_LABS.crt
  • CA_INFRAMINDS_LABS_inter.crt
  • CA_INFRAMINDS_LABS_inter.key

Les fichiers .key doivent rester secret et ne jamais être divulgué.

Génération du certificat final pour l'infrastructure OpenBao

Dans le but de faciliter le déploiement, nous allons créer un unique certificat pour nos 3 nodes, mais dans le monde de l'entreprise, il est recommandé d'avoir un certificat distinct pour chaque node.

export CA_NAME=CA_INFRAMINDS_LABS

# Génération du CSR et de la clé privé
openssl req -new -nodes -text -out openbao_server.csr -keyout openbao_server.key -subj "/CN=openbao.lab" -addext "subjectAltName = IP:192.168.100.2, IP:192.168.100.3, IP:192.168.100.4, IP:127.0.0.1" -addext "subjectKeyIdentifier=hash" -addext "keyUsage=critical, digitalSignature, keyEncipherment, keyAgreement" -addext "extendedKeyUsage= critical, serverAuth"

# Signature de la CSR par la CA intermediaire
openssl x509 -req -in openbao_server.csr -text -days 365 -CA ${CA_NAME}_inter.crt -CAkey ${CA_NAME}_inter.key -CAcreateserial -out openbao_server.crt -copy_extensions copyall

Ici, nous avons mis les IP de nos nodes OpenBao.

Nous utiliserons cela plutôt que le CN car nous n'avons pas de nom de domaine pour ce lab.

Nous avons donc un certificat final openbao_server.crt & openbao_server.key

Configuration d'OpenBao

OpenBao fourni un system de stockage partagé/répliqué natif utilisant raft (https://thesecretlivesofdata.com/raft/). C'est cette solution que nous allons utiliser.

Préparation des nodes OpenBao

Le dossier /opt/raft sera utilisé pour stocker nos data.

# A executer sur tout les nodes
mkdir -p /opt/raft
chown openbao:openbao /opt/raft

Il faut également ajouter sur nos nodes (et d'ailleurs sur tous les clients qui souhaiteront utiliser OpenBao, et les certificats générés via celui-ci) notre certificat ROOT CA. (CA_INFRAMINDS_LABS.crt)

# A executer sur tout les nodes
cp CA_INFRAMINDS_LABS.crt /usr/local/share/ca-certificates/
update-ca-certificates

La commande update-ca-certificates permet d'ajouter des certificats au bon emplacement pour être utilisé par le système.

Configuration du services

La version 2.0.0 n'inclue pas de WEB Ui celle ci est en refonte...

Sur chaque node Openbao vous devrez éditer le fichier de conf .hcl et faire les changements suivants:
- cluster_addr / api_addr / address: mettre l'IP de votre serveur
- retry_join: mettre l'IP des autres membres du cluster

ui            = true # Pas utile pour le moment car il n'y a pas d'ui...
cluster_addr  = "https://192.168.100.2:8201" # Votre addresse pour communiquer avec les membres du cluster
api_addr      = "https://192.168.100.2:8200" # Votre addresse pour que les client puissent ce connecter ou que les autres membre vous envoie les requetes.
cluster_name  = "lab-inframinds"
storage "raft" {
  path = "/opt/raft"
  node_id = "node1"
  retry_join 
  {
      leader_api_addr = "https://192.168.100.3:8200" # L'adresse du node2
  }
  retry_join 
  {
      leader_api_addr = "https://192.168.100.4:8200" # L'addresse du node3
  }
}

listener "tcp" {
  address       = "192.168.100.2:8200" # Votre addresse
  tls_cert_file = "/opt/openbao/tls/openbao_server_full.crt"
  tls_key_file  = "/opt/openbao/tls/openbao_server.key"
}

Le fichier tls_key_file est celui généré via openSSL pour nos nodes Openbao.
Mais ... ⚠️ le fichier openbao_server_full.crt doit également contenir le certificat de l'intermédiaire en seconde position (CA_INFRAMINDS_LABS_inter.crt) pour valider la chaîne complète.

-----BEGIN CERTIFICATE-----
<openbao_server>
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
<CA_INFRAMINDS_LABS_inter>
-----END CERTIFICATE-----

Une fois les modifications effectuées sur tout les nodes vous pouvez démarrer le service sur ceux-ci

systemctl start openbao

Initialisation d'OpenBao

Sur un des nodes (qui deviendra notre premier master) lancer la commande d'initialisation (nous choisirons le node 1).

bao operator init -address https://192.168.100.2:8200

Cette commande va vous retourner:
- Un token root pour les premières opérations sur le service
- 5 Unseal key (cf: Shamir's Secret Sharing)

Sur chaque noeud il va falloir lancer la commande de unseal 3 fois avec à chaque fois 1 clé différente par itération (vous pouvez utiliser les 3 premières sur tous les noeuds).

# Sur le node 1
bao operator unseal -address https://192.168.100.2:8200
# Sur le node 2
bao operator unseal -address https://192.168.100.3:8200
# Sur le node 3
bao operator unseal -address https://192.168.100.4:8200

Normalement, à cette étape, votre cluster OpenBao est prêt. 🎉

Attention: A chaque redémarrage d'un service OpenBao il vous faudra le unseal.

Status d'OpenBao

Pour voir le statut du cluster raft:

BAO_TOKEN=<TOKEN_ROOT> bao operator raft list-peers -address https://192.168.100.2:8200
Node     Address               State       Voter
----     -------               -----       -----
node1    192.168.100.2:8201    leader      true
node2    192.168.100.3:8201    follower    true
node3    192.168.100.4:8201    follower    true

Pour voir le statut du cluster OpenBao:

BAO_TOKEN=<TOKEN_ROOT> bao operator members -address https://192.168.100.4:8200
Host Name                   API Address                   Cluster Address               Active Node    Version    Upgrade Version    Last Echo
---------                   -----------                   ---------------               -----------    -------    ---------------    ---------
inframinds-lab-openbao01    https://192.168.100.2:8200    https://192.168.100.2:8201    false          2.0.1      2.0.1              2024-10-17T13:27:51Z
inframinds-lab-openbao02    https://192.168.100.3:8200    https://192.168.100.3:8201    true           2.0.1      2.0.1              n/a
inframinds-lab-openbao03    https://192.168.100.4:8200    https://192.168.100.4:8201    false          2.0.1      2.0.1              2024-10-17T13:27:51Z

Pour voir si un noeud est seal (on voit bien le sealed a false), vous pouvez faire cette commande sur chaque node:

bao status -address https://192.168.100.4:8200
Key                     Value
---                     -----
Seal Type               shamir
Initialized             true
Sealed                  false
Total Shares            5
Threshold               3
Version                 2.0.1
Build Date              2024-09-03T19:57:34Z
Storage Type            raft
Cluster Name            lab-inframinds
Cluster ID              1ddfc488-87dc-a3e4-b348-afddee90743f
HA Enabled              true
HA Cluster              https://192.168.100.3:8201
HA Mode                 standby
Active Node Address     https://192.168.100.3:8200
Raft Committed Index    172
Raft Applied Index      172

Création de notre PKI intermédiaire dans OpenBao

Création de la PKI

Comme lors de la création de la Génération du CA intermédiaire pour l'infrastructure OpenBao, nous allons créer une CA intermédiaire pour les clients OpenBao.

Nous allons commencer par créer une PKI dans Openbao (sur 1 des 3 nodes)

export BAO_ADDR=https://192.168.100.2:8200
export BAO_TOKEN=<ROOT TOKEN>
bao secrets enable -path=labs_pki pki
#Success! Enabled the pki secrets engine at: labs_pki/

Ensuite, nous allons générer le CSR (demande de signature) et la clé de cette nouvelle PKI Openbao

# On install JQ pour parser le retour de la commande openbao
apt install jq 

export BAO_ADDR=https://192.168.100.2
export BAO_TOKEN=<ROOT TOKEN>
bao write -format=json labs_pki/intermediate/generate/internal common_name="Bao Intermediate Authority" country="FR" state="IDF" locality="Paris" organization="Inframinds" ttl=43800h | jq -r '.data.csr' > pki_intermediate.csr

On va signer cette CSR avec notre PKI ROOT offline.

Avant de signer cela, il faut modifier les extensions openssl pour avoir les bons attribue lors de la signature. (/etc/ssl/openssl.cnf)

[ v3_ca ]
# Uncomment 
keyUsage = cRLSign, keyCertSign, digitalSignature

Une fois cela fait, on peut signer notre CSR

openssl x509 -req -in pki_intermediate.csr -text -sha256 -days 1825 -CA CA_INFRAMINDS_LABS/CA_INFRAMINDS_LABS.crt -CAkey CA_INFRAMINDS_LABS/CA_INFRAMINDS_LABS.key -CAcreateserial -out bao_inter.crt -extensions v3_ca -extfile /etc/ssl/openssl.cnf

Cela nous génère un certificat nommé bao_inter.crt que nous allons importer dans le service OpenBao

export BAO_ADDR=https://192.168.100.2:8200
export BAO_TOKEN=<ROOT TOKEN> 
bao write labs_pki/intermediate/set-signed certificate=@bao_inter.crt 

# Output
WARNING! The following warnings were returned from Vault:

  * This mount hasn't configured any authority information access (AIA)
  fields; this may make it harder for systems to find missing certificates
  in the chain or to validate revocation status of certificates. Consider
  updating /config/urls or the newly generated issuer with this information.

Key                 Value
---                 -----
existing_issuers    <nil>
existing_keys       <nil>
imported_issuers    [943ca24e-9c9a-e263-8e6b-7ed394081f58]
imported_keys       <nil>
mapping             map[943ca24e-9c9a-e263-8e6b-7ed394081f58:a37db397-7d14-e57d-a751-c0b5a8145cdc]


Test de la PKI Openbao

Afin de pouvoir générer des certificats depuis cette nouvelle PKi, il faut commencer par créer un role (permission sur la PKI)

export BAO_ADDR=https://192.168.100.2:8200
export BAO_TOKEN=<ROOT TOKEN> 
bao write labs_pki/roles/test \
    allowed_domains=labs.inframinds.fr \
    allow_subdomains=true max_ttl=8760h

Ici, nous autorisons uniquement les sous-domaine liés *.labs.inframinds.fr dans un rôle nommé test

export BAO_ADDR=https://192.168.100.2:8200
export BAO_TOKEN=<ROOT TOKEN> 
bao write labs_pki/roles/test \
    allowed_domains=labs.inframinds.fr \
    allow_subdomains=true max_ttl=8760h

Voici un example:

export BAO_ADDR=https://192.168.100.2:8200
export BAO_TOKEN=<ROOT TOKEN> 

bao write labs_pki/issue/test \
    common_name=node1.labs.inframinds.fr

La commande retourne:
- private_key: Une clé privée générée
- issuing_ca: Le certificat intermédiaire ayant signé votre certificat
- certificate: Le certificat
- ca_chain: Les certificats de la chaîne (actuellement, il doit y avoir uniquement le même que le issuing_ca)

⚠️ Il faut penser à ajouter le certificat intermédiaire à la suite du certificat lors de son utilisation.

Si l'on essaie un autre domaine, nous avons une erreur ❌

export BAO_ADDR=https://192.168.100.2:8200
export BAO_TOKEN=<ROOT TOKEN> 

bao write labs_pki/issue/test \
    common_name=blah.example.com

Error writing data to labs_pki/issue/test: Error making API request.

URL: PUT https://192.168.100.5/v1/labs_pki/issue/test
Code: 400. Errors:

* common name blah.example.com not allowed by this role

HAproxy

Pour finir notre architecture cible 🎯, nous devons mettre en place un HAproxy qui servira de proxy et Loadbalancing entre nos noeuds OpenBao et nos clients.

Nous allons donc prendre un nouveau serveur qui sera à la fois sur le réseau d'OpenBao et à la fois joignable par les clients.

Prérequis

Avant de commencer, il faut installer notre Root CA sur ce serveur

# A executer sur tout les nodes
cp CA_INFRAMINDS_LABS.crt /usr/local/share/ca-certificates/
update-ca-certificates

Il faut aussi lui générer un certificat qui sera utilisé pour la communication avec les clients. Soit avec un domaine soit avec une IP (ou les deux 😄). Si on fait un domaine, il faudra bien sûr faire un enregistrement DNS.

Sur le node01 openbao.

export BAO_ADDR=https://192.168.100.2:8200
export BAO_TOKEN=<ROOT TOKEN> 

bao write labs_pki/issue/test \
    common_name="openbao.labs.inframinds.fr", ip_sans="10.0.0.5"

Créer un fichier sur le serveur HAproxy /etc/haproxy/certs/haproxy.pem (Si le dossier certs n'existe pas vous pouvez le créer.
Ce fichier doit contenir dans l'ordre:
- Le certificat
- Le certificat intermediaire (issuing_ca)
- La clé privée

-----BEGIN CERTIFICATE-----
<Certificat openbao.labs.inframinds.fr>
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
<Certificat intermédiaire Openbao PKI(pas celui de l'infra!>
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
<Clé privé openbao.labs.inframinds.fr>
-----END RSA PRIVATE KEY-----

Installation

Pour installer HAproxy, suivre les instructions officielle sur: https://haproxy.debian.net/#distribution=Debian&release=bookworm&version=3.0

Configuration

La configuration de HAproxy se trouve ici: /etc/haproxy/haproxy.cfg

global
	log /dev/log	local0
	log /dev/log	local1 notice
	chroot /var/lib/haproxy
	stats socket /run/haproxy/admin.sock mode 660 level admin
	stats timeout 30s
	user haproxy
	group haproxy
	daemon

	# Default SSL material locations
	ca-base /etc/ssl/certs
	crt-base /etc/ssl/private

	# See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate
        ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
        ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
        ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets

defaults
	log	global
	mode	http
	option	httplog
	option	dontlognull
        timeout connect 5000
        timeout client  50000
        timeout server  50000
	errorfile 400 /etc/haproxy/errors/400.http
	errorfile 403 /etc/haproxy/errors/403.http
	errorfile 408 /etc/haproxy/errors/408.http
	errorfile 500 /etc/haproxy/errors/500.http
	errorfile 502 /etc/haproxy/errors/502.http
	errorfile 503 /etc/haproxy/errors/503.http
	errorfile 504 /etc/haproxy/errors/504.http

frontend openbao
    bind 10.0.0.5:80 # IP atteignable par les clients
    bind 10.0.0.5:443 ssl crt /etc/haproxy/certs/haproxy.pem # Certificat généré dans les prérequis
    http-request redirect scheme https unless { ssl_fc } # redirection SSL
    mode http
    default_backend openbao_back

backend openbao_back
    mode http
    option httpchk HEAD /v1/sys/health?standbyok=true # Controle de l'état des membres
    option forwardfor
    http-check expect status 200 
    server node1 192.168.100.2:8200 check ssl ca-file /etc/ssl/certs/ca-certificates.crt # Définition des backends en SSL avec le CA généré par ca-certificates
    server node2 192.168.100.3:8200 check ssl ca-file /etc/ssl/certs/ca-certificates.crt
    server node3 192.168.100.4:8200 check ssl ca-file /etc/ssl/certs/ca-certificates.crt

Vous pouvez maintenant activer et démarrer HAproxy

systemctl enable haproxy
systemctl start haproxy

Les logs sont disponible  /var/log/haproxy.log
Vous pouvez voir l'état des backend via la commande:

echo "show stat" | sudo nc -U /run/haproxy/admin.sock | cut -d "," -f 1,2,8-10,18 | column -s, -t

# pxname      svname    stot  bin  bout  status
openbao       FRONTEND  0     0    0     OPEN
openbao_back  node1     0     0    0     UP
openbao_back  node2     0     0    0     UP
openbao_back  node3     0     0    0     UP
openbao_back  BACKEND   0     0    0     UP

Test

Depuis un client qui est sur le même réseau 10.0.0.0/24, essayer d'atteindre le service Openbao pour voir si ça fonctionne bien 😄

# On met la nouvelle adresse (celle du LB)
export BAO_ADDR=https://openbao.labs.inframinds.fr
export BAO_TOKEN=<ROOT TOKEN>

# On repete cette commande 3 fois
bao write labs_pki/issue/test     common_name="test.labs.inframinds.fr"
bao write labs_pki/issue/test     common_name="test.labs.inframinds.fr"
bao write labs_pki/issue/test     common_name="test.labs.inframinds.fr"

Les requêtes doivent passer correctement ✅ , et l'on peut vérifier la répartition de charge sur HAproxy via la commande précédente.

Test sur la fonction KV

Une des autres fonctionnalités utiles d'Openbao est le KV (key/value). Cela permet de stocker de donnée de manière chiffrée (sur le disque) qui sont accessible, versionnable, et peuvent être restreinte par des rôles et policies. Vu que nous sommes dans un Lab, nous allons utiliser le token root pour faire les test de notre infrastructure (le sujet de l'article n'est pas comment utiliser OpenBao 😛)

Création du KV

export BAO_ADDR=https://openbao.labs.inframinds.fr
export BAO_TOKEN=<ROOT TOKEN>

bao secrets enable -path=lab_kv kv-v2
# Output
Success! Enabled the kv-v2 secrets engine at: lab_kv/

Pour écrire un secret:

# lab_kv est notre path et web-app notre objet pouvant contenir plusieur key/value
bao kv put lab_kv/web-app api-token="test"

Pour les lire via la CLI

bao kv get lab_kv/web-app

Sauvegarde & Restauration

Sauvegarde

Comme nous utilisons le système de stockage natif de Bao (raft) et que celui-ci est partagé/répliqué en se basant sur le master, nous devons réaliser la sauvegarde sur le node leader (⚠️ Sans passer par le Loadbalancer ! ). (CF: https://developer.hashicorp.com/vault/tutorials/standard-procedures/sop-backup )

# On met l'adresse du leader
export BAO_ADDR=https://192.168.100.3:8200
export BAO_TOKEN=<ROOT TOKEN>

bao operator raft snapshot save /opt/backups/bao-$(date +%F_%R).snap

Restauration

Sur un cluster fraîchement initié, vide, vous avez des nouvelles unseal key et un nouveau token root.
Ceux-ci vont être remplacés par les anciens lors de l'importation du snapshot.

bao operator raft snapshot restore -force backup.snap

L'option -force permet de prendre en compte le fait que les unseal key ne sont pas les mêmes. Une fois fait, il faudra unseal chaque node avec les "anciennes" unseal key.

bao operator unseal [unseal_key]