❯ infra · Running

Zéro secret en clair dans Git avec Sealed Secrets Zero-Secret-in-Git with Sealed Secrets

Chiffrement des secrets Kubernetes pour un stockage GitOps sécurisé via Bitnami Sealed Secrets — fonctionnement, workflow kubeseal et considérations opérationnelles. Encrypting Kubernetes secrets for safe GitOps storage using Bitnami Sealed Secrets — how it works, the kubeseal workflow, and key operational considerations.

· 8 min de lecture 8 min read ·
k3s-clustersecuritysecretsgitopsk8s
🇬🇧 Read this article in English

Le problème fondamental du GitOps et des secrets

Un cluster reconstruit, une clé privée Sealed Secrets perdue : tous les secrets versionnés dans Git deviennent d’un coup illisibles. C’est le genre de scénario qui force à comprendre vraiment comment fonctionne le chiffrement des secrets en GitOps.

Le GitOps impose que tout l’état du cluster soit versionné dans Git. Mais les secrets Kubernetes (mots de passe, tokens, clés API) ne peuvent évidemment pas être committés en clair. Un Secret Kubernetes encodé en base64 ne vaut rien — le base64 n’est pas du chiffrement.

Les alternatives :

  • Vault (HashiCorp) : excellent mais surdimensionné pour un homelab. Complexité opérationnelle élevée.
  • External Secrets : récupère les secrets depuis des stores externes (AWS SSM, GCP Secret Manager). Nécessite un service externe.
  • Sealed Secrets : chiffrement asymétrique, controller dans le cluster, zéro dépendance externe.

Sealed Secrets est le choix naturel pour un homelab auto-contenu.

Comment ça marche

Sealed Secrets repose sur une paire de clés asymétriques :

  1. Le controller tourne dans le cluster et détient la clé privée. Il peut déchiffrer les SealedSecrets.
  2. kubeseal (CLI) récupère la clé publique du controller pour chiffrer un Secret en SealedSecret.

Le SealedSecret chiffré est parfaitement sûr à committer dans Git — sans la clé privée du controller, il est inexploitable.

Secret YAML (clair)
        │
        ▼  kubeseal --cert <clé publique>
SealedSecret YAML (chiffré) ──► Git ──► ArgoCD ──► Cluster
                                                        │
                                            Controller déchiffre
                                                        │
                                                        ▼
                                               Secret Kubernetes (clair)

Installation

Le controller Sealed Secrets est installé via un manifest statique, dans le namespace kube-system :

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

Il est important de nommer le controller sealed-secrets-controller dans kube-system — c’est le nom par défaut que kubeseal recherche.

Workflow : créer un SealedSecret

1. Préparer le Secret source

kubectl create secret generic gitea-credentials \
  --namespace gitea \
  --from-literal=DB_PASSWORD='$(openssl rand -base64 32)' \
  --dry-run=client \
  -o yaml > /tmp/secret.yaml

L’option --dry-run=client -o yaml génère le YAML sans créer la ressource dans le cluster.

2. Chiffrer avec kubeseal

kubeseal \
  --controller-name sealed-secrets-controller \
  --controller-namespace kube-system \
  --format yaml \
  < /tmp/secret.yaml \
  > gitea/sealedsecret-credentials.yaml

3. Nettoyer le secret en clair

rm /tmp/secret.yaml  # Ne jamais committer ce fichier

4. Committer le SealedSecret

git add gitea/sealedsecret-credentials.yaml
git commit -m "feat(gitea): add sealed credentials"
git push

ArgoCD synchronise le SealedSecret, le controller le déchiffre, et le Secret Kubernetes apparaît dans le namespace gitea.

Structure d’un SealedSecret

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: gitea-credentials
  namespace: gitea
spec:
  encryptedData:
    DB_PASSWORD: AgB3x9K2m...  # Chiffré RSA-OAEP
  template:
    metadata:
      name: gitea-credentials
      namespace: gitea
    type: Opaque

Le champ encryptedData contient la valeur chiffrée pour chaque clé. Le template définit le Secret qui sera créé par le controller.

Scope du chiffrement

Par défaut, kubeseal chiffre avec le scope strict : le SealedSecret ne peut être déchiffré que dans le namespace spécifié, que pour le nom spécifié. Impossible de déchiffrer un SealedSecret gitea/gitea-credentials dans un namespace différent.

C’est le comportement souhaité — isolation forte entre namespaces.

Sceller hors-ligne avec la clé publique

Le workflow kubeseal ci-dessus suppose que la machine qui scelle a accès au cluster (pour récupérer la clé publique via kubectl). Or il y a plein de cas où on veut sceller sans cet accès — depuis un runner GitHub Actions, depuis un environnement de dev qui ne porte pas la config kubectl du cluster, ou simplement en pair-programming avec un collègue qui n’est pas admin.

La parade : committer la clé publique du controller dans le repo GitOps (docs/sealed-secrets/sealed-secrets-pub.pem chez moi). C’est une clé publique — la divulguer est sans risque, par construction. Avec elle, n’importe qui peut chiffrer un secret pour ce cluster, mais personne ne peut le déchiffrer hormis le controller qui détient la clé privée.

Extraction de la clé publique (à faire une fois, depuis une machine avec kubectl) :

kubeseal --fetch-cert \
  --controller-name sealed-secrets-controller \
  --controller-namespace kube-system \
  > docs/sealed-secrets/sealed-secrets-pub.pem
git add docs/sealed-secrets/sealed-secrets-pub.pem
git commit -m "docs: pin sealed-secrets public cert for offline sealing"

Utilisation depuis n’importe où, avec le repo cloné :

kubectl create secret docker-registry ghcr-pull \
  --docker-server=ghcr.io \
  --docker-username=YellowBeanie \
  --docker-password=<PAT> \
  --namespace=tapaz \
  --dry-run=client -o yaml \
| kubeseal --cert docs/sealed-secrets/sealed-secrets-pub.pem \
           --format yaml \
           --scope strict \
  > tapaz/sealedsecret-ghcr-pull.yaml

Pas un seul kubectl qui touche le cluster. Pas besoin que le poste ait un kubeconfig valide. Le SealedSecret produit est strictement équivalent à celui qu’aurait scellé un workflow en ligne — même cert, même algorithme, même protection de scope.

Rotation: si le controller régénère sa clé (rotation manuelle ou réinstallation), il faut refresh le .pem committé. Sinon, les nouveaux SealedSecret continueront à fonctionner (le controller garde les anciennes clés), mais on chiffre encore avec une clé plus ancienne que nécessaire — sans danger, juste pas optimal.

Backup de la clé privée

La clé privée du controller est stockée dans un Secret dans kube-system :

kubectl get secret -n kube-system \
  -l sealedsecrets.bitnami.com/sealed-secrets-key \
  -o yaml > sealed-secrets-master-key-BACKUP.yaml

Ce backup est CRITIQUE. Si le cluster est reconstruit et que la clé privée est perdue, tous les SealedSecrets existants sont indéchiffrables. Le backup doit être stocké hors du cluster (coffre-fort de mots de passe, stockage chiffré).

Rotation des clés

Sealed Secrets génère automatiquement de nouvelles clés de chiffrement régulièrement (par défaut tous les 30 jours). Les anciens SealedSecrets restent déchiffrables (les anciennes clés sont conservées), mais les nouveaux sont chiffrés avec la clé la plus récente.

Pour forcer le rechiffrement d’un SealedSecret avec la clé courante :

kubeseal --re-encrypt < ancien-sealedsecret.yaml > nouveau-sealedsecret.yaml

Leçons apprises

Toujours spécifier --controller-namespace : si kubeseal ne trouve pas le controller, il échoue silencieusement ou utilise un controller incorrect. Être explicite évite les surprises.

Versionner les SealedSecrets avec les manifests : stocker sealedsecret-credentials.yaml dans le même répertoire que les Deployment qui les consomment. L’organisation logique simplifie les opérations.

Ne jamais regenerer un secret existant sans mise à jour de l’application : si le DB_PASSWORD d’une base de données est régénéré, la base de données doit être mise à jour simultanément. Un SealedSecret changé sans mise à jour correspondante dans la BDD entraîne une erreur d’authentification au prochain redémarrage du pod.

Une fois les secrets chiffrés et versionnés dans Git, l’étape suivante est d’automatiser leur déploiement avec ArgoCD.

k3s-clustersecuritysecretsgitopsk8s