Faire du GitOps avec Flux v2 (Part 3/4) - La gestion des secrets

Introduction

Dans les articles précédents, nous avons vu avec Paul BOISSON comment mettre en place une démarche GitOps avec Flux, en s’appuyant notamment sur des charts helm pour nos déploiements. Si vous ne les avez pas encore lu ou que vous avez besoin d’un petit rappel, voici les liens :

Dans cette troisième partie, nous aborderons la gestion des secrets et nous verrons différentes approches afin de répondre à ce besoin.

Kubernetes et les secrets

Comme présenté dans les précédents articles, Flux est un outil permettant d’implémenter une approche GitOps dans un cluster Kubernetes. Cet article se place donc naturellement dans ce contexte de déploiement d’applications sur un cluster Kubernetes.

Kubernetes nous permet de stocker et de gérer nos informations sensibles, telles que des mots de passe, grâce à des objets de type Secret. La particularité de ces objets, contrairement aux autres types, est que l’information y est stockée encodée en base64 ; plus précisément les valeurs se trouvant sous le champ data, car il est possible d’utiliser le champ stringData si l’on ne veut pas encoder ces informations en base64.

Il est important de noter ici que l’encodage en base64 n’est autre qu’un… encodage, ce qui n’a rien à voir avec du chiffrement ! Par définition, n’importe qui peut, à partir du manifeste d’un secret, accéder aux informations qui y sont stockées.

L’approche GitOps et les secrets

Comme nous l’avons vu précédemment, l’approche GitOps consiste à commit nos manifestes et à les stocker dans un repository Git agissant comme seule source de vérité.

Le problème, vous l’aurez deviné de par le titre et l’introduction de cet article, c’est lorsque l’on souhaite manipuler des secrets. En effet, il est hors de question d’envisager de les stocker tel quel dans notre repository. Deux méthodologies se présentent donc à nous :

  • Stocker les secrets chiffrés dans le repository Git, et l’agent, ici Flux, va déchiffrer ces derniers lors de la synchronisation des ressources et générer des secrets Kubernetes
  • Stocker les secrets dans un gestionnaire de secrets tel que AWS Secret Manager, et stocker une référence à ces derniers dans le repository Git ; l’agent s’occupe alors de récupérer les secrets référencés pour générer des secrets Kubernetes

Dans cet article, nous nous intéresserons à la première méthode, à savoir stocker des secrets chiffrés dans notre repository Git. Pour ce faire, nous avons deux outils open source à notre disposition : Sealed secrets développé par Bitnami, et Sops développé par Mozilla.

Sealed secrets

Pour pouvoir travailler avec Sealed secrets, il est nécessaire d’installer la CLI Kubeseal, disponible sur la page releases du github https://github.com/bitnami-labs/sealed-secrets/releases, ou avec un simple brew install kubeseal.

Le fonctionnement de cet outil consiste à déployer un contrôleur supplémentaire, qui va avoir la charge de déchiffrer les Sealed secrets afin de générer des Secrets Kubernetes. Cela peut se faire à l’aide des deux ressources suivantes :

apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
  name: sealed-secrets
  namespace: flux-system
spec:
  url: https://bitnami-labs.github.io/sealed-secrets
  interval: 1h0m0s
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: sealed-secrets
  namespace: flux-system
spec:
  chart:
    spec:
      chart: sealed-secrets
      reconcileStrategy: ChartVersion
      sourceRef:
        kind: HelmRepository
        name: sealed-secrets
      version: '>=1.15.0-0'
  releaseName: sealed-secrets-controller
  interval: 1h0m0s
  install:
    crds: Create
  upgrade:
    crds: CreateReplace

Au démarrage, le contrôleur va générer une paire de clé RSA qui va être utilisée pour le chiffrement et le déchiffrement des secrets. Cette paire de clé est stockée dans un secret Kubernetes dans le namespace flux-system.

Afin de pouvoir chiffrer nos secrets, nous pouvons récupérer la clé publique à l’aide de la commande suivante :

kubeseal --fetch-cert \
--controller-name=sealed-secrets-controller \
--controller-namespace=flux-system \
> pub-sealed-secrets.pem

Nous sommes donc en capacité de chiffrer nos secrets avant de les commit dans Git.

Prenons le secret suivant :

apiVersion: v1
kind: Secret
metadata:
  name: my-secret
  namespace: my-namespace
data:
  my-secret: V2hhdCBhIHNlY3JldCE=

Il suffit d’exécuter la commande suivante :

kubeseal --format=yaml --cert=pub-sealed-secrets.pem < my-secret.yaml > my-secret-sealed.yaml

Et nous obtenons notre Sealed secret :

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: my-secret
  namespace: my-namespace
spec:
  encryptedData:
    my-secret: AgCgWPYEJjedOZq5TazC162M7Ho8JbjOpHmiiDSl7g5wZpUtv2wTiORsfumJzc0Bp78I7L5bzI68Mw3UW46GnLJvB/mUmr0+VrFpfhmrj6yFWoxYGxA1dUSKST5WfztuWmwEOTCn8zStDkBxCSjpIsyUs6LUktPW29QwlmybIfm3Z+dnfNz/EXtNX+xCGmtJzmiTOwz7klu18gs2OZ8Lx21fYIXdRILPFbXB39wBxCXm+AUMWcr1/omhgUa81/HNDmWmL0NlqafME9RjRFi6364USIfyDiklmPz+cq528Z3GcdbHtKm2Uyx3J0cUh5TJBhn7R66wj7dExhYkDjZKLgPlifji8s3olf1b1/CG2HYcTbShhQBYFxOQwGyeiRiaWJbERaN09cNwqSYTV72pNRlIp1lEAdZ1U4wjSkV0G4deL03th+1ShLi+t9ZtVS3hC8tFPgoaDhwbWWxH/SMNAuxqojxz13Ce1uA8dcN9AE251P8/NO8EB6tDZcTbp8WSzpYTvUVffHNFLnVrGNkJlnpM7Ok6CPCLr3AC1maQeSuVqm45vB4wcNzNYMunKr12ohr5oF3qHgBhSjLTUkSa7Q68Ct2Bz4nKlzOoI8yh7t/+m8nxC/dGdlmnQgVEmdhvg5KhRwVuL+gxThszoZ/bEN27ROFDkVUIXEslb6SEX4//Zt/TyFQT2jyjT26OMDv1zIsnyqvKynensjT1WhdcIQ==
  template:
    metadata:
      creationTimestamp: null
      name: my-secret
      namespace: my-namespace

Vous aurez sûrement remarqué qu’il s’agit d’une CRD (Custom Resource Definition) SealedSecret utilisant l’API bitnami.com/v1alpha1.

Nous pouvons désormais stocker notre sealed secret dans notre repository Git. Lors de la synchronisation, le contrôleur Sealed secret déchiffre le secret et génère un secret Kubernetes équivalent au secret avant chiffrement.

Une précision importante à soulever concernant Sealed secrets est la portée. Il en existe trois :

  • strict (le cas par défaut) : le secret doit être chiffré et déchiffré avec le même nom et le même namespace. En effet, ces données se retrouvent dans le champ encryptedData, et donc modifier le nom ou le namespace du SealedSecret aura pour conséquence d’obtenir une erreur au moment du déchiffrement.
  • namespace-wide : il est possible de renommer le SealedSecret mais pas de modifier son namespace.
  • cluster-wide : il est possible de modifier le nom et le namespace du SealedSecret.

Cette portée peut être spécifiée au moment de chiffrer un secret en passant à notre commande l’option --scope. Il est intéressant de comprendre qu’avec la portée par défaut, un SealedSecret ne peut pas être renommé ou changer de namespace, car cela ne fonctionnerait tout simplement pas : on obtiendrait une erreur lors du déchiffrement par le contrôleur. Le but derrière tout ça, c’est de permettre à plusieurs équipes de travailler et d’avoir accès au même repository Git, sans pour autant qu’elles aient la possibilité de récupérer les secrets des autres équipes. En effet, les équipes ne possédant que la clé publique, la seule façon d’obtenir les secrets déchiffrés est de récupérer les secrets Kubernetes créés par Sealed secret. Or, si l’équipe A n’a accès qu’au namespace A, elle serait tentée de copier coller un SealedSecret de l’équipe B et de modifier son namespace afin de pouvoir accéder aux informations sensibles, ce qui n’est donc pas possible avec les portées strict et namespace-wide.

Sops

Un autre outil permettant de chiffrer nos secrets afin de pouvoir les commit en toute sécurité dans notre repository Git est Sops.

Là encore, il est nécessaire d’installer la CLI Sops, disponible sur la page releases du github https://github.com/mozilla/sops/releases, ou avec un simple brew install sops.

Contrairement à Sealed secrets, il n’est pas nécessaire d’installer un contrôleur supplémentaire, étant donné que Sops est déjà intégré à Flux. À titre de comparaison, Sops n’utilise pas non plus de CRD mais directement des secrets Kubernetes, et chiffre seulement la partie data ou stringData de ces derniers.

Sops peut être utilisé avec pgp, age, hashicorp vault, aws kms, gcp kms et azure kv. Nous allons nous attarder sur trois d’entre eux, à savoir pgp, hashicorp vault et aws kms.

SOPS - PGP

Sops peut être utilisé avec PGP/GPG afin de chiffrer nos secrets.

Pour ce faire, nous devons tout d’abord générer une paire de clé OpenPGP. Une fois ceci fait, nous pouvons exporter la clé privée dans un secret Kubernetes avec la commande suivante (l’id de ma clé étant D13D0D9916A18B3532DA91805D57348190B1CB86) :

gpg --export-secret-keys --armor D13D0D9916A18B3532DA91805D57348190B1CB86 |
kubectl create secret generic sops-gpg \
  --namespace=flux-system \
  --from-file=sops.asc=/dev/stdin

Une fois la clé privée exportée dans le secret sops-gpg et dans le namespace flux-system, il nous suffit de le préciser à Flux au niveau de notre ressource Kustomization dans la partie decryption :

apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
  name: apps
  namespace: flux-system
spec:
  sourceRef:
    kind: GitRepository
    name: flux-system
  interval: 1m
  path: ./apps/demo
  prune: true
  wait: true
  decryption:
    provider: sops
    secretRef:
      name: sops-gpg

Tous les secrets se trouvant dans ./apps/demo seront désormais déchiffrés par Flux en mode PGP à l’aide de la clé privée se trouvant dans le secret sops-gpg précédemment créé.

Nous pouvons donc le tester. Prenons le secret suivant :

apiVersion: v1
kind: Secret
metadata:
  name: my-secret
data:
  my-secret: V2hhdCBhIHNlY3JldCE=

Il suffit d’exécuter la commande suivante :

sops --pgp D13D0D9916A18B3532DA91805D57348190B1CB86 \
  --encrypt \
  --in-place \
  --encrypted-regex '^(data|stringData)$' \
  my-secret.yaml

Et nous obtenons notre secret chiffré :

apiVersion: v1
kind: Secret
metadata:
  name: my-secret
data:
  my-secret: ENC[AES256_GCM,data:uqraYDJhCiwlZ9xsQLAwKg9Kyl8=,iv:+jkE3tL9tfxBEE5ZXNmm07ejQIHF5Bs7awsQJTIXfVA=,tag:071REkAjhw3n4EmRgqjr2A==,type:str]
sops:
  kms: []
  gcp_kms: []
  azure_kv: []
  hc_vault: []
  age: []
  lastmodified: "2023-01-09T18:29:53Z"
  mac: ENC[AES256_GCM,data:58w9EQe7PZ7Oq7NdwLAXvbOTyo017liHAiwLqgtRIbJIFU+ClrIRH1eQ/Q+XJTEJEUfrcLjFvvPOQWXRA8utExmWAEbIuSDQwsLjtJmRq5g6BngkZ69NTqOhfZJu2DIIG2tbpc/ebr4zh0QmksKBjT8IHaxTDc1t8xRhaOYcUkY=,iv:qJj52Ag5tPS4NJlh7NJgFeEDSEf4gBDuHCa/Y4zSa3I=,tag:6pslKSH/MUrBtByitpxQ8w==,type:str]
  pgp:
  - created_at: "2023-01-09T18:29:52Z"
    enc: |
      -----BEGIN PGP MESSAGE-----


      hF4DQ7fBAFHM9IYSAQdAKHbmxVDUr1WXI+YWaKoM/UEFYVQ2jeXYcvWbYQQiR3ow
      zygak32DIUpd6at0HDA1YZKEUmblsUA+swFcdFcYrX8oIejyc2mTUynXoID//KRB
      0lwBRfnY1xJ/mARYeRPF8LC5Gz9kNG6MHUIDPevG7pNne0cj2RZ+/A9KO5Yj/sNk
      wTmjNTPt/Tl1EbBDxK8gfbvlckIYKmELiLU5flO7EZudKFe4SwpWpgPZ4R5G+w==
      =mqZK
      -----END PGP MESSAGE-----
    fp: D13D0D9916A18B3532DA91805D57348190B1CB86
  encrypted_regex: ^(data|stringData)$
  version: 3.7.3

Nous pouvons constater que seule la partie data a été chiffrée, conformément à la regex de chiffrement spécifiée. De plus, nous constatons dans la partie sops plusieurs tableaux, tels que pgp, kms ou encore hc_vault. C’est parce qu’il est tout à fait possible de chiffrer un même secret avec plusieurs clés PGP et même plusieurs méthodes différentes. Cela peut se faire assez simplement en ajoutant des arguments à la commande :

sops --pgp D13D0D9916A18B3532DA91805D57348190B1CB86 \
  --kms "arn:aws:kms:us-west-2:927034868273:key/fe86dd69-4132-404c-ab86-4269956b4500" \
  --encrypt \
  --in-place \
  --encrypted-regex '^(data|stringData)$' \
  my-secret.yaml

Nous pouvons désormais commit notre secret chiffré dans notre repository Git, et constater la présence de notre secret Kubernetes my-secret après la synchronisation de Flux.

Sops - HashiCorp Vault

Sops peut également être utilisé avec HashiCorp Vault.

Pour ce faire, il nous faut d’abord stocker le jeton Vault dans un secret :

echo $VAULT_TOKEN |
kubectl create secret generic sops-hcvault \
  --namespace=flux-system \
  --from-file=sops.vault-token=/dev/stdin

Une fois le jeton stocké dans le secret sops-hcvault et dans le namespace flux-system,  il nous suffit de le préciser à Flux au niveau de notre ressource Kustomization dans la partie decryption comme réalisé précédemment, en précisant le nom du secret :

decryption:
    provider: sops
    secretRef:
      name: sops-hcvault

Tous les secrets se trouvant dans ./apps/demo seront désormais déchiffrés par Flux en mode HashiCorp Vault à l’aide du token se trouvant dans le secret sops-hcvault précédemment créé.

Le chiffrement d’un secret est légèrement différent qu’avec PGP :

export VAULT_ADDR=https://vault.example.com:8200
export VAULT_TOKEN=my-token
sops --hc-vault-transit $VAULT_ADDR/v1/sops/keys/my-encryption-key \
  --encrypt \
  --in-place \
  --encrypted-regex '^(data|stringData)$' \
  my-secret.yaml

Le secret chiffré ainsi obtenu possède toutes les informations nécessaires à la récupération de la clé de chiffrement, et Flux utilise pour cela le jeton Vault stocké dans le secret sops-hcvault.

Sops - AWS KMS

Sops peut aussi être utilisé avec des cloud providers, à l’aide d’un service comme KMS pour AWS par exemple.

Pour ce faire, nous allons utiliser le provider sops dans nos ressources Kustomization, tout comme précédemment, mais nous ne précisons pas de secret. La partie decryption ressemble donc à ça :

decryption:
    provider: sops

Pour la suite, il est important de comprendre le fonctionnement de IRSA (IAM Roles for Service Accounts). Je ne peux donc que vous recommander l’article de Timothée AUFORT à ce sujet : https://blog.ippon.fr/2022/12/19/securiser-lacces-a-aws-depuis-vos-pods-kubernetes-avec-irsa/.

Le principe va être de lier un rôle IAM ayant accès à KMS au service account du contrôleur kustomize.

Il nous faut donc créer en premier lieu un rôle IAM ayant une policy similaire à :

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "kms:Decrypt",
                "kms:DescribeKey"
            ],
            "Effect": "Allow",
           	"Resource": "arn:aws:kms:eu-west-1:XXXXX209540:key/4f581f5b-7f78-45e9-a543-83a7022e8105"
        }
    ]
}

Ensuite, nous allons venir annoter le service account du contrôleur kustomize avec l’ARN du rôle créé :

kubectl -n flux-system annotate serviceaccount kustomize-controller \
--field-manager=flux-client-side-apply \
eks.amazonaws.com/role-arn='arn:aws:iam::<ACCOUNT_ID>:role/<KMS-ROLE-NAME>'

Enfin, il nous suffit de redémarrer le contrôleur kustomize.

Cette partie de la configuration peut également être réalisée lors de l’installation de Flux, dans la partie patches du fichier flux-system/kustomization.yaml :

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - gotk-components.yaml
  - gotk-sync.yaml
patches:
  - patch: |
      apiVersion: v1
      kind: ServiceAccount
      metadata:
        name: kustomize-controller
        annotations:
          eks.amazonaws.com/role-arn: arn:aws:iam::<ACCOUNT_ID>:role/<KMS-ROLE-NAME>      
    target:
      kind: ServiceAccount
      name: kustomize-controller

La commande permettant de chiffrer nos secrets est alors la suivante :

sops --kms "arn:aws:kms:eu-west-1:XXXXX209540:key/4f581f5b-7f78-45e9-a543-83a7022e8105" \
  --encrypt \
  --in-place \
  --encrypted-regex '^(data|stringData)$' \
  my-secret.yaml

Sops - L’intégration avec les releases Helm

Si vous utilisez des objets de type HelmRelease, comme nous l’avons vu dans le second article de cette série, il est tout à fait possible de chiffrer un fichier de values, notamment grâce au générateur de secrets de Kustomize.

Pour ce faire, nous devons d’abord créer le fichier kustomizeconfig.yaml dans le même répertoire que notre ressource HelmRelease :

nameReference:
- version: v1
  kind: Secret
  fieldSpecs:
  - kind: HelmRelease
    path: spec/valuesFrom/name

Nous pouvons ensuite ajouter une partie valuesFrom dans notre HelmRelease :

apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: podinfo
spec:
  chart:
    spec:
      chart: podinfo
      sourceRef:
        kind: HelmRepository
        name: podinfo
  releaseName: podinfo
  interval: 5m
  valuesFrom:
  - kind: Secret
    name: podinfo-values

Puis mettre à jour notre fichier kustomization.yaml en ajoutant les parties secretGenerator et configurations :

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: podinfo
resources:
- helmrelease.yaml
secretGenerator:
- name: podinfo-values
  files:
  - values.yaml=values.enc.yaml
configurations:
- kustomizeconfig.yaml

Il ne nous reste plus qu’à chiffrer notre fichier values.yaml :

sops --pgp D13D0D9916A18B3532DA91805D57348190B1CB86 \
  --encrypt \
  --input-type=yaml \
  --output-type=yaml \
  values.yaml > values.enc.yaml

Nous pouvons alors commit le fichier values.enc.yaml (et non le values.yaml !).

Lors de la synchronisation, nous pouvons constater la création d’un secret Kubernetes ressemblant à podinfo-values-kt5g6d4c62. Flux ajoute en effet un identifiant unique afin de pouvoir mettre à jour le déploiement automatiquement lors de la mise à jour du fichier values.enc.yaml. Lors d’une telle action, un nouveau secret est créé avec un nouvel identifiant, et le déploiement est mis à jour afin d’utiliser ce nouveau secret, l’ancien étant supprimé.

Conclusion

Dans ce troisième article de cette série sur le GitOps avec Flux, nous avons pu voir différentes approches afin de gérer nos secrets.

Quelques différences sont à noter entre les deux outils présentés, à savoir Sealed secrets et Sops.

Comme nous l’avons vu, Sealed secrets nécessite de déployer un contrôleur supplémentaire et ajoute des CRD notamment le type SealedSecret. À l’inverse, Sops est déjà intégré à Flux et ne nécessite aucune CRD particulière autre que celles installées par Flux.

D’autre part, Sealed secrets génère lui-même une paire de clé pour le chiffrement et le déchiffrement, et offre donc par définition moins de fonctionnalités par rapport à Sops qui en propose plusieurs : PGP, HashiCorp Vault, AWS KMS, GCP KMS, etc.

C’est pour ces deux principales raisons que je recommande l’utilisation de Sops si vous faites le choix de Flux pour gérer votre workflow GitOps.

Si ce sujet vous intéresse vous pouvez regarder du côté de 1Password Operator et de External Secrets Operator, deux outils permettant de synchroniser des secrets stockés dans des gestionnaires de secrets externes à votre cluster, comme par exemple AWS Secret Manager, plutôt que de stocker vos secrets chiffrés dans vos repository Git comme présenté dans cet article.

Sources