Skip to content

Sécurisation des secrets

Estimated time to read: 5 minutes

Pour sécuriser nos secrets, nous allons utiliser external-secrets pour simplifier la délégation de la gestion des secrets à un outil tiers tel que GitLab, HashiCorp Vault...

Installation

external-secrets fournit un Helm chart pour une installation simple

On ajoute donc le repo pour external-secrets :

helm repo add external-secrets https://charts.external-secrets.io
helm repo update

et on installe dans un namespace dédié 🚀

helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace --set installCRDs=true

external-secrets est installé

external-secrets has been deployed successfully!

external-secrets instancie plusieurs composants, on peut les lister:

kubectl get all -n external-secrets
NAME                                                   READY   STATUS    RESTARTS   AGE
pod/external-secrets-5f45b6f844-27fmq                  1/1     Running   0          2m9s
pod/external-secrets-cert-controller-9795887f6-8mssr   1/1     Running   0          2m9s
pod/external-secrets-webhook-6f4789ccf-qmpkn           1/1     Running   0          2m9s

NAME                               TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
service/external-secrets-webhook   ClusterIP   10.3.175.44   <none>        443/TCP   2m11s

NAME                                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/external-secrets                   1/1     1            1           2m10s
deployment.apps/external-secrets-cert-controller   1/1     1            1           2m10s
deployment.apps/external-secrets-webhook           1/1     1            1           2m10s

NAME                                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/external-secrets-5f45b6f844                  1         1         1       2m9s
replicaset.apps/external-secrets-cert-controller-9795887f6   1         1         1       2m9s
replicaset.apps/external-secrets-webhook-6f4789ccf           1         1         1       2m9s

Tout est prêt !

Configuration

Pour permettre à external-secrets de gérer les secrets, il est nécessaire de créer un SecretStore. Celui-ci fera le lien entre le cluster et l'outil hébergeant les secrets.

Pour faire simple dans ce workshop, on va faire avec GitLab pour stocker nos données sensibles via l'utilisation des variables de CICD. C'est simple à mettre en oeuvre mais introduit une faiblesse coté sécurité car nécessite quand même d'avoir un secret coté Kubernetes, pour stocker la clé d'API pour se connecter à GitLab.

D'autres solutions sont possibles...

external-secrets fournit plusieurs connecteurs possibles. Le plus complet semble être celui d'HashiCorp Vault car il n'introduit pas de besoin de clé ou de credentials pour s'intégrer dans le cluster.

On a déjà préparé les variables dans le projet GitLab (via le menu Settings -> CICD -> Variables):

img.png

Créons le secret de connexion:

kubectl -n external-secrets create secret generic gitlab-token --from-literal=token=$GITLAB_TOKEN

GITLAB_TOKEN est déjà configuré pour vous

On est sympa, c'est déjà fait grâce au script exécuté au début du workshop. Si vous avez changer de terminal, il faut refaire la commande suivante:

source <(curl -s -u "devoxx2024:MOT_DE_PASSE_QUON_VOUS_DONNERA" https://heracles.yodamad.fr/setup/devoxx-2024/setup.sh)

Maintenant on peut créer un ClusterSecretStore pour faire le lien entre GitLab et le cluster.

apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: gitlab-cluster-secret-store
  namespace: external-secrets
spec:
  provider:
    # provider type: gitlab
    gitlab: # (1)
      url: https://gitlab.com/ # (2)
      auth:
        SecretRef:
          accessToken:
            name: gitlab-token
            key: token
            namespace: external-secrets
      projectID: "53147568" # (3)
  1. 👽 Le type du provider
  2. 🔗 L'URL de votre GitLab, pas forcément gitlab.com
  3. 📦 L'ID du projet qui héberge les données sensibles
Alternative SecretStore

Il est possible d'utiliser un SecretStore pour restreindre à un namespace, si vous voulez limiter l'accessibilité/utilisation de vos secrets.

On peut déployer notre ClusterSecretStore:

kubectl apply -f external-secrets/external-secrets-secret-store.yml -n external-secrets
Pour les curieux, le fichier external-secrets-secret-store.yml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: gitlab-cluster-secret-store
  namespace: external-secrets
spec:
  provider:
    # provider type: gitlab
    gitlab:
      url: https://gitlab.com/
      auth:
        SecretRef:
          accessToken:
            name: gitlab-token
            key: token
            namespace: external-secrets
      projectID: "53147568"

et vérifier que le connexion à GitLab est opérationnelle:

kubectl get clustersecretstores.external-secrets.io -n external-secrets

ClusterSecretStore est valide

NAME                          AGE     STATUS   CAPABILITIES   READY
gitlab-cluster-secret-store   2m50s   Valid    ReadOnly       True

Configuration d'un ExternalSecret

Nos secrets stockés dans GitLab, il faut créer désormais créer un ExternalSecret qui va rappatrier dans notre cluster le secret stocké dans GitLab.

On va faire le test sur external-dns pour externaliser notre clé d'API et username Cloudflare dans GitLab:

kubectl apply -n external-dns -f external-secrets/external-secrets-external-secret.yml
Pour les curieux, le fichier external-secrets-external-secret.yml
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: external-secret-cloudflare-key-credentials
  namespace: external-dns
spec:
  refreshInterval: 1h
  secretStoreRef:
    kind: ClusterSecretStore
    name: gitlab-cluster-secret-store # Must match SecretStore on the cluster
  target:
    name: external-cloudflare-api-token # Name for the secret to be created on the cluster
    creationPolicy: Owner
  data:
    - secretKey: api-key # Key given to the secret to be created on the cluster
      remoteRef:
        key: CF_API_KEY # Key of the variable on Gitlab
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: external-secret-cloudflare-mail-credentials
  namespace: external-dns
spec:
  refreshInterval: 1h
  secretStoreRef:
    kind: ClusterSecretStore
    name: gitlab-cluster-secret-store # Must match SecretStore on the cluster
  target:
    name: external-cloudflare-user-mail # Name for the secret to be created on the cluster
    creationPolicy: Owner
  data:
    - secretKey: user-mail # Key given to the secret to be created on the cluster
      remoteRef:
        key: CF_USER_MAIL # Key of the variable on Gitlab

Les secrets sont créés

externalsecret.external-secrets.io/external-secret-cloudflare-key-credentials created
externalsecret.external-secrets.io/external-secret-cloudflare-mail-credentials created

On peut vérifier que nos secrets sont valides (ie synchronizés à l'état SecretSynced):

kubectl get externalsecrets.external-secrets.io -n external-dns

Success

NAME                                          STORE                         REFRESH INTERVAL   STATUS         READY
external-secret-cloudflare-key-credentials    gitlab-cluster-secret-store   1h                 SecretSynced   True
external-secret-cloudflare-mail-credentials   gitlab-cluster-secret-store   1h                 SecretSynced   True

Automatiquement external-secrets a généré des secrets pour nous:

kubectl get secrets -n external-dns

Les nouveaux secrets sont générés

NAME                            TYPE     DATA   AGE
cloudflare-api-token            Opaque   1      4h53m
cloudflare-user-mail            Opaque   1      15h
external-cloudflare-api-token   Opaque   1      99s
external-cloudflare-user-mail   Opaque   1      98s

Configuration de external-dns pour utiliser les nouveaux secrets

Maintenant que l'on a nos nouveaux secrets, on peut mettre à jour notre Deployment d'external-dns pour qu'il les utilise plutôt que les anciens.

On change donc la référence des secrets dans le Deployment:

- name: CF_API_TOKEN
  valueFrom:
    secretKeyRef:
      name: external-cloudflare-api-token
      key: api-key
- name: CF_API_EMAIL
  valueFrom:
    secretKeyRef:
      name: external-cloudflare-user-mail
      key: user-mail

On aura plus besoin de nos anciens secrets, autant les supprimer:

kubectl delete secret cloudflare-api-token cloudflare-user-mail -n external-dns

Et on met à jour external-dns pour utiliser les nouveaux

kubectl apply -f external-secrets/external-dns-cloudflare.yml -n external-dns
Pour les curieux, le fichier external-dns-cloudflare.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns-cloudflare
  namespace: external-dns
  labels:
    app.kubernetes.io/instance: external-dns-cloudflare
    app.kubernetes.io/name: external-dns
    app.kubernetes.io/version: 0.14.0
spec:
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app.kubernetes.io/instance: external-dns-cloudflare
      app.kubernetes.io/name: external-dns
  template:
    metadata:
      labels:
        app.kubernetes.io/instance: external-dns-cloudflare
        app.kubernetes.io/name: external-dns
    spec:
      serviceAccountName: external-dns
      containers:
        - name: external-dns-cloudflare
          image: registry.k8s.io/external-dns/external-dns:v0.14.0
          args:
            - --source=ingress
# 🛜 On filtre sur le domaine grunty.uk
            - --domain-filter=grunty.uk
# 🚚 Gestionnaire du DNS
            - --provider=cloudflare
# 💡 Prefix pour les enregistrements TXT (et ne pas supprimer ceux du voisin)
# 🫸 C'est un hack pour le TP, cela fonctionne car on à un seul noeud
# 🫸 Dans la vrai vie on utilise un prefix spécifique au cluster
            - --txt-prefix=$(UID)-
            - --txt-owner-id=$(UID)
          env:
            - name: CF_API_TOKEN
              valueFrom:
                secretKeyRef:
                  name: external-cloudflare-api-token
                  key: api-key
            - name: CF_API_EMAIL
              valueFrom:
                secretKeyRef:
                  name: external-cloudflare-user-mail
                  key: user-mail
            - name: UID
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName

Après quelques secondes, on voit qu'un nouveau pod a été créé et fonctionne:

kubectl get po -n external-dns

external-dns est recréé et opérationnel

NAME                                  READY   STATUS    RESTARTS   AGE
external-dns-XXX-7d4cf677f6-nmwn5     1/1     Running   0          25s

et on vérifie que la connexion avec Cloudflare est toujours ok:

kubectl logs -f $(kubectl get pods -l "app.kubernetes.io/name=external-dns" -n external-dns | grep external-dns-cloudflare | cut -d' ' -f1) -n external-dns

external-dns est bien connecté à Cloudflare

time="2023-12-20T13:48:56Z" level=info msg="Instantiating new Kubernetes client"
time="2023-12-20T13:48:56Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2023-12-20T13:48:56Z" level=info msg="Created Kubernetes client https://10.3.0.1:443"

Le chef de brigade sécurité est content, mais il sent qu'on a du répondant 😎, il nous impose alors encore de nouvelles contraintes : il ne vaut que des ingrédients contrôlés en amont et veut favoriser la filière locale plutot que la grande distribution... 🛒

Ok, challenge accepted !! 💪 Montrons-lui comment faire ➡️