Secrets GitOps légers pour Kubernetes

13 min de lecture
Secrets GitOps légers pour Kubernetes

Chaque jour chez Sokube, nous prônons GitOps comme moyen de simplifier, d’améliorer et d’accélérer les opérations numériques, et nous accompagnons nos clients dans cette démarche. Avec l’abondance des briques technologiques qui composent aujourd’hui le paysage “Cloud Native”, ce qui paraissait difficile et hasardeux il y a quelques années se réduit désormais simplement au choix de la combinaison de produits / plateformes la plus adaptée à un cas d’usage.

Cependant, lorsqu’il s’agit de préserver la confidentialité d’informations sensibles dans une démarche GitOps, les choix ne sont ni nombreux ni simples. Avec son encodage base64 réversible, Kubernetes Secrets reste le talon d’Achille de la plateforme en matière de sécurité ou de confidentialité de l’information, que le RBAC soit correctement configuré ou non. L’utilisation native de Kubernetes Secrets pour stocker des informations confidentielles ou sensibles devrait être purement proscrite sur des systèmes de production, à minima.

Plusieurs alternatives compatibles GitOps préservant la confidentialité ont naturellement émergé comme solutions de contournement : la première partie de cet article se concentrera sur un rapide panorama de l’existant. Si Hashicorp Vault reste une solution réellement sécurisée, riche en fonctionnalités et largement adoptée, nous pensons qu’elle se positionne plutôt comme une solution transverse à l’échelle de l’entreprise ou de l’organisation pour l’information confidentielle. Dans le contexte de GitOps toutefois, elle viendrait fausser le principe du “git comme source unique de vérité”. Enfin, un déploiement pertinent de HashiCorp Vault nécessite souvent des changements organisationnels adéquats ainsi qu’un budget conséquent pour son exploitation.

Dans la seconde partie de l’article, nous nous concentrerons sur deux solutions légères qui offrent exactement ce dont on a besoin pour GitOps sans bouleverser l’organisation, le projet ni les budgets : Bitnami Sealed Secrets et Kamus de Soluto.

Panorama des solutions de secrets Kubernetes

La plupart de nos cas d’usage concernent des installations exclusivement on-premise. Ainsi, si Amazon, Azure et Google proposent généralement leur propre gestion de secrets pour leur plateforme, nous nous sommes concentrés en priorité sur les solutions on-premise, et nous les avons évaluées selon les critères suivants :

  • Complexité : nous avons globalement estimé l’effort nécessaire pour prendre en main la solution, l’installer et l’exploiter.
  • Maturité : basée sur le nombre et la fréquence des versions, l’ancienneté de la solution, les exemples concrets trouvés sur le web démontrant son adoption.
  • Portée : classifie la portée de validité des secrets ; organisation signifie qu’il n’existe aucune restriction spécifique de validité du secret, contrairement à cluster ou compte de service, où le secret ne peut pas être déchiffré sur un autre cluster spécifique ou avec un autre compte de service.
  • Dépendance : c’est un aspect important de la solution qui la rend plus ou moins applicable dans des contextes particuliers.
SolutionCatégorieComplexitéMaturitéPortée des secretsDépendanceNote
Helm SecretsOn PremiseMEDIUMAbandonnéOrganisationAucuneAbandonné, fork jkroepke/helm-secrets
KamusOn PremiseLOWAlphaCompte de service / ClusterAucunePrometteur
External Secrets OperatorOn Premise (nécessite un backend KMS cloud)LOWAlphaOrganisationNécessite un backend KMSTrès jeune, nécessite un KMS derrière
aws-secret-operatorOn Premise (nécessite un backend AWS SM)LOWAlphaOrganisationNécessite AWS SM derrièreNécessite AWS Secrets Manager
KSOPSOn PremiseLOWAlphaOrganisationSOPS KustomizePeut s’intégrer avec ArgoCD
Kapitan avec TesoroOn PremiseLOWAlphaOrganisationBackend KMS KapitanSympa mais n’a de sens qu’avec kapitan
Bitnami Sealed SecretsOn PremiseVERY LOWStableClusterAucuneSimple mais nécessite de copier les clés entre clusters
Godaddy Kubernetes External SecretsOn Premise (nécessite un backend KMS cloud)LOWStableOrganisationNécessite un backend KMSBon wrapper mais nécessite un KMS derrière
Hashicorp VaultOn Premise HostedHIGHStableOrganisationAucuneSolution éprouvée mais coût de mise en œuvre / gestion ÉLEVÉ
Banzai Cloud Bank-VaultsOn PremiseMEDIUMStableOrganisationHashicorp VaultSimplifie Vault mais nécessite toujours Vault
Kustomize secret generator pluginsOn PremiseHIGHStableOrganisationNécessite un service derrièreNécessite de développer un plugin et un service derrière

Bitnami Sealed Secrets

La solution Bitnami Sealed Secrets repose sur un contrôleur qui s’exécute dans le cluster cible, où une paire de clés privée/publique spécifique au cluster est générée.

Chiffrement

La clé publique est utilisée par un binaire client (kubeseal) pour chiffrer les données à la volée (sous la forme de ressources personnalisées nommées SealedSecret). Cette opération peut également être réalisée hors ligne (sans avoir besoin d’un cluster avec un contrôleur bitnami) en utilisant le certificat du contrôleur (clé publique) qui peut être téléchargé depuis le cluster. La ressource résultante est un SealedSecret qu’il est sûr de stocker dans un dépôt git.

Déchiffrement

Le contrôleur détecte la présence de ressources SealedSecret et déchiffre automatiquement le contenu en une ressource Secret standard équivalente.

Installation

L’installation de la solution est relativement simple :

$ kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.13.1/controller.yaml

L’outil client (kubeseal) permettant d’interagir avec le contrôleur à l’intérieur du cluster cible est un binaire autonome qui peut être téléchargé séparément :

$ wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.13.1/kubeseal-linux-amd64 -O kubeseal

Usage

Nous allons utiliser un namespace dédié :

$ kubectl create namespace my-namespace
namespace/my-namespace created

Le kubeseal de Bitnami peut être utilisé comme une commande en pipe, et lorsqu’il est combiné avec les capacités de dry-run de kubectl, nous pouvons générer des sealed secrets sans même de ressources intermédiaires stockées localement sous forme de fichiers :

$ kubectl create secret generic db-credentials\
  -n my-namespace\
  --from-literal=DBUSER=mydbuser\
  --from-literal=DBPWD=highlysecret\
  --dry-run=client\
  -o yaml\
  | kubeseal -o yaml > db-credentials-sealed-secret.yaml

Le SealedSecret résultant peut être stocké en toute sécurité dans un dépôt git :

apiVersion:  bitnami.com/v1alpha1
kind:  SealedSecret
metadata:
 creationTimestamp:  null
 name:  db-credentials
 namespace:  my-namespace
spec:
 encryptedData:
  DBPWD:  <ENCRYPTED DATA>
  DBUSER:  <ENCRYPTED DATA>
 template:
 metadata:
 creationTimestamp:  null
 name:  db-credentials
 namespace:  my-namespace

Application de la ressource :

$ kubectl apply -f db-credentials-sealed-secret.yaml

Vérification de la ressource :

$ kubectl get sealedsecrets -n my-namespace db-credentials
NAME             AGE
db-credentials   2m3s
​
$ kubectl get events -n my-namespace
LAST SEEN   TYPE     REASON     OBJECT                        MESSAGE
2m9s        Normal   Unsealed   sealedsecret/db-credentials   SealedSecret unsealed successfully

Vérification du Secret équivalent (et des données) généré par le contrôleur :

$ kubectl get secrets -n my-namespace
NAME                  TYPE                                  DATA   AGE
...
db-credentials        Opaque                                2      3m18s
​
$ kubectl get secrets -n my-namespace db-credentials -o jsonpath='{.data.DBUSER}' | base64 --decode
mydbuser
​
$ kubectl get secrets -n my-namespace db-credentials -o jsonpath='{.data.DBPWD}' | base64 --decode
highlysecret

Remarques

Important à savoir : chaque sealed secret est chiffré avec sa propre clé asymétrique aléatoire, spécifique au nom et au namespace du sealed secret. Copier-coller les données chiffrées pour un autre secret ou dans un autre namespace ne fonctionnera pas.

Par défaut, les SealedSecrets générés pour un cluster ne fonctionnent pas avec un autre cluster (l’installation du contrôleur Bitnami crée une nouvelle paire de clés publique/privée), ce qui peut rendre les opérations globales de mise en staging GitOps fastidieuses (chaque environnement devrait disposer de ses propres SealedSecrets personnalisés). Une solution consiste à exporter/importer le même jeu de paires de clés d’un cluster à l’autre. Toutefois, une clé compromise sur un cluster donnerait également accès à tous les sealed secrets de tous les clusters utilisant la même paire de clés.

Résumé

➕ Simplicité de mise en place et d’usage

➕ Solution mature et bien maintenue

➖ Les Secrets classiques restent exposés

➖ Aucun usage en dehors des Secrets (par exemple ConfigMap)

Kamus

La solution Kamus propose deux mécanismes différents de génération de secrets. L’un est similaire à Bitnami Sealed Secrets, l’autre est une véritable solution de gestion de secrets en zero-trust.

Premier mode : KamusSecret

Les KamusSecret tendent à imiter Bitnami Sealed Secrets : un contrôleur dans le cluster détecte les KamusSecrets et fournit une version déchiffrée sous forme de Secret.

La partie encodage est un peu plus fastidieuse, car le kamus-client ne permet pas de passer une ressource Secret entière mais fonctionne plutôt comme un service de chiffrement brut : chaque élément doit être chiffré séparément avec le client, puis agrégé manuellement dans une définition KamusSecret sous forme d’“enveloppe”.

Comme avec Bitnami, le contrôleur est responsable du déchiffrement d’un KamusSecret en un Secret classique.

Second mode : Secrets Zero-Trust

Contrairement à un Bitnami Sealed Secret ou à un KamusSecret, le mode zero-trust de Kamus présente l’immense avantage de ne jamais révéler les données déchiffrées, sauf au service concerné, à l’exécution.

La partie chiffrement du mode secrets zero-trust est très similaire à l’exemple précédent : au lieu d’envelopper les données encodées dans un KamusSecret, une ConfigMap sera utilisée pour stocker les secrets sous forme de fichiers chiffrés dans un volume ConfigMap monté.

Pour la consommation des secrets, un initContainer invoque le service kamus-decryptor et les résultats déchiffrés sont stockés dans un volume emptyDir éphémère accessible uniquement au conteneur applicatif du pod.

Installation

L’installation du contrôleur et de ses CustomResourcesDefinitions est relativement simple :

$ helm repo add soluto https://charts.soluto.io
​
$ key=$(openssl rand -base64 32  | tr -d '\n')
$ helm upgrade --install kamus soluto/kamus --set keyManagement.AES.key=$key

Nous avons ajouté ici la génération de clé aléatoire, car la solution est fournie (dangereusement) avec une clé prédéfinie. De plus, contrairement à Bitnami, la solution installe ses workloads dans le namespace par défaut, nous recommandons donc d’ajuster la release helm en conséquence. Enfin, le nombre de replicas des déploiements encryptor/decryptor est fixé à 2 par défaut, ce qui peut s’avérer superflu.

Le client Kamus est malheureusement un package npm (un choix discutable qui oblige les utilisateurs à installer le gestionnaire de paquets npm). Une image de conteneur Docker semble disponible, cependant elle n’est pas mentionnée sur le site de Kamus, nous avons donc choisi de nous en tenir à l’approche documentée :

$ npm install -g @soluto-asurion/kamus-cli

Nous allons utiliser un namespace et un compte de service dédiés :

$ kubectl create namespace my-namespace
namespace/my-namespace created
​
$ kubectl create serviceaccount dummy-sa -n my-namespace
serviceaccount/dummy-sa created

Le kamus-cli a également besoin d’un pod encryptor exposé sous forme d’URL :

$ export POD_NAME=$(kubectl get pods --namespace default  -l "app=kamus,release=kamus,component=encryptor"  -o jsonpath="{.items[0].metadata.name}")
​
$ kubectl port-forward $POD_NAME 8880:9999 &

Usage de KamusSecret

Premier scénario, mise en place d’un KamusSecret contenant nos informations sensibles :

$ kamus-cli encrypt --secret mydbuser --service-account dummy-sa --namespace my-namespace --allow-insecure-url --kamus-url http://localhost:8880
[info  kamus-cli]: Encryption started...
[info  kamus-cli]: service account: dummy-sa
[info  kamus-cli]: namespace: my-namespace
[warn  kamus-cli]: Auth options were not provided, will try to encrypt without authentication to kamus
Handling connection for 8880
[info  kamus-cli]: Successfully encrypted data to dummy-sa service account in my-namespace namespace
[info  kamus-cli]: Encrypted data:
0XZm9vDLVvjNGCgeFZqNnA==:GoaTZCXkwyfoBNRwjtgsOQ==
​
$ kamus-cli encrypt --secret highlysecret --service-account dummy-sa --namespace my-nymespace --allow-insecure-url --kamus-url http://localhost:8880
[info  kamus-cli]: Encryption started...
[info  kamus-cli]: service account: dummy-sa
[info  kamus-cli]: namespace: my-nymespace
[warn  kamus-cli]: Auth options were not provided, will try to encrypt without authentication to kamus
Handling connection for 8880
[info  kamus-cli]: Successfully encrypted data to dummy-sa service account in my-nymespace namespace
[info  kamus-cli]: Encrypted data:
NlHRuvaqFlip5mDP6d5cDw==:OVW3C2hY/wZW6o2I+YjL5Q==

Nous devons ensuite créer un objet enveloppe KamusSecret basé sur la structure suivante (notez que la documentation fait référence à une section data qui ne semble pas pertinente ou fonctionnelle, nous avons donc utilisé la syntaxe stringData).

$ cat <<EOF >kamus.yaml
apiVersion: "soluto.com/v1alpha2"
kind: KamusSecret
metadata:
  name: db-credentials
  namespace: my-namespace
type: Generic
stringData:
  DBUSER: 0XZm9vDLVvjNGCgeFZqNnA==:GoaTZCXkwyfoBNRwjtgsOQ==
  DBPWD: NlHRuvaqFlip5mDP6d5cDw==:OVW3C2hY/wZW6o2I+YjL5Q==
serviceAccount: dummy-sa
EOF
​```


Cette ressource KamusSecret peut désormais être stockée en toute sécurité dans un système de gestion de versions comme git.

Application de la nouvelle ressource :

$ kubectl apply -f kamus.yaml


Vérification des ressources créées :

$ kubectl get kamussecrets.soluto.com -n my-namespace NAME AGE db-credentials 20s ​ $ kubectl get secrets -n my-namespace NAME TYPE DATA AGE default-token-sc4ds kubernetes.io/service-account-token 3 9m3s db-credentials Generic 2 53s


Vérification du déchiffrement :

$ kubectl get secrets -n my-namespace db-credentials -o jsonpath=’{.data.DBUSER}’ | base64 –decode mydbuser ​ $ kubectl get secrets -n my-namespace db-credentials -o jsonpath=’{.data.DBPWD}’ | base64 –decode highlysecret


### Usage des secrets Zero-Trust

Dans ce mode, nous allons réutiliser les données chiffrées du premier exemple, mais cette fois nous utiliserons une ConfigMap que nous exploiterons ensuite comme volume monté de fichiers chiffrés (DBUSER et DBPWD)

$ cat «EOF >kamus-encrypted-secrets-cm.yaml apiVersion: v1 kind: ConfigMap metadata: name: kamus-encrypted-secrets-cm namespace: my-namespace data: DBUSER: 0XZm9vDLVvjNGCgeFZqNnA==:GoaTZCXkwyfoBNRwjtgsOQ== DBPWD: NlHRuvaqFlip5mDP6d5cDw==:OVW3C2hY/wZW6o2I+YjL5Q== EOF

​

Cette ConfigMap peut être stockée en toute sécurité dans un dépôt git.

Ensuite, nous allons utiliser l'initContainer de la solution Kamus, qui invoquera les services de déchiffrement sur les fichiers montés, et les résultats seront stockés dans un volume emptyDir commun destiné à notre conteneur "applicatif" (notre application est un conteneur endormi que nous consulterons plus tard)

$ cat «EOF >kamus-pod.yaml apiVersion: v1 kind: Pod metadata: namespace: my-namespace name: kamus-zero-trust-example spec: serviceAccountName: dummy-sa automountServiceAccountToken: true initContainers:

  • name: “kamus-init” image: “soluto/kamus-init-container:latest” imagePullPolicy: IfNotPresent env:
    • name: KAMUS_URL value: http://kamus-decryptor.default.svc.cluster.local/ volumeMounts:
    • name: encrypted-secrets mountPath: /encrypted-secrets
    • name: decrypted-secrets mountPath: /decrypted-secrets args: ["-e","/encrypted-secrets","-d","/decrypted-secrets", “-n”, “config.json”] containers:
  • name: app image: busybox imagePullPolicy: IfNotPresent args: [“sleep”, “3600”] volumeMounts:
    • name: decrypted-secrets mountPath: /secrets volumes:
  • name: encrypted-secrets configMap: name: kamus-encrypted-secrets-cm
  • name: decrypted-secrets emptyDir: medium: Memory EOF ​```

Création de notre pod applicatif :

$ kubectl create -f kamus-pod.yaml
pod/kamus-zero-trust-example created

Et vérification du fichier déchiffré /secrets/config.json disponible pour notre conteneur applicatif :

$ kubectl exec -n my-namespace kamus-zero-trust-example app -- more /secrets/config.json
{
    "DBPWD":"highlysecret",
    "DBUSER":"mydbuser"
}

Remarques

À noter que :

  • dans le cas d’un KamusSecret (première méthode), les arguments –service-account et –namespace de la ligne de commande kamus-cli, bien qu’obligatoires, sont purement arbitraires : les données chiffrées d’un KamusSecret ne sont liées à aucun compte de service ni namespace en particulier, et leur existence n’est pas requise au moment du chiffrement.
  • À l’inverse, dans le cas du mode Zero-Trust (seconde méthode), la cohérence des comptes de service et des namespaces entre les étapes de chiffrement et ceux utilisés dans l’initContainer est fondamentale. Un autre compte de service, ou un autre namespace, ne déclenchera pas de déchiffrement correct.

Résumé

➕ Plusieurs modes de fonctionnement

➕ Fonctionne en environnement zero-trust

➕ Peut utiliser un véritable backend KMS

➖ Documentation parfois confuse

➖ Solution encore aux premiers stades

➖ Son client basé sur npm

Conclusion

Si Kamus offre davantage d’options que Bitnami, il nécessite un effort nettement plus important pour les scénarios les plus courants. La documentation fournie avec la solution est également parfois confuse, bien que des efforts significatifs aient été faits sur la clarté depuis les premières versions. Certains clients et publics seront particulièrement sensibles à sa capacité à fonctionner dans un environnement zero-trust, malgré la mise en place conséquente et l’injection d’init containers dans les workloads que cela exige. Elle peut également s’appuyer sur des solutions de coffre-fort cryptographiques sophistiquées et sécurisées comme backend, ce qui constitue également un argument de poids lorsque cela est requis.

Dans les scénarios les plus courants cependant, Bitnami Sealed Secrets demeure particulièrement efficace, compact et simple à exploiter. La solution existe depuis un certain temps déjà, est bien maintenue et s’impose comme une réponse de facto pour la gestion légère des secrets dans les workflows GitOps, et ce pour de bonnes raisons.

Comme toujours, il n’existe pas de meilleure alternative universelle, mais il y a toujours la meilleure réponse à un cas d’usage particulier, avec ses exigences et ses contraintes propres à l’environnement. Nous espérons que cet article vous aidera à choisir la solution de gestion de secrets adaptée à vos scénarios GitOps.

Pour aller plus loin, découvrez notre formation ArgoCD et GitOps et nos consultants DevSecOps.