Aller au contenu
Sécurité medium

Secrets Kubernetes : bonnes pratiques

21 min de lecture

Kubernetes propose un objet natif Secret pour stocker les données sensibles. Par défaut, cet objet est encodé en base64 (pas chiffré), stocké en clair dans etcd, et ne fournit ni rotation ni audit sans configuration explicite.

Cela ne signifie pas que les Secrets natifs sont inutilisables. Avec les bonnes mesures (chiffrement etcd, RBAC strict, audit logs), ils deviennent acceptables pour certains cas. Pour des exigences plus fortes, External Secrets Operator et le Secrets Store CSI Driver apportent chacun des garanties supplémentaires, avec leurs propres compromis.

Ce guide couvre quatre dimensions : le stockage, la distribution, la consommation et la rotation des secrets.

  • Les limites des Kubernetes Secrets natifs, et comment les atténuer
  • External Secrets Operator : synchronisation depuis un coffre-fort externe
  • Secrets Store CSI Driver : montage direct depuis un provider externe
  • Comment choisir selon le mode de consommation de votre application
  • Les risques liés aux nœuds et pods privilégiés

Ces limites ne sont pas des défauts cachés : elles découlent du rôle que Kubernetes s'est donné, celui de distribuer des données sensibles aux pods, pas de les protéger. Les cinq points qui suivent décrivent chacun un comportement par défaut à connaître avant de décider si les Secrets natifs suffisent à votre contexte.

Un Secret ressemble à une ConfigMap avec une différence de traitement : son contenu n'apparaît pas dans les sorties standard de kubectl et il n'est pas écrit sur le disque du nœud, seulement monté en mémoire.

apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
username: YWRtaW4= # "admin" en base64
password: cGFzc3dvcmQ= # "password" en base64

Ce que c'est : un objet qui stocke des données sensibles, accessible par les pods via des volumes ou des variables d'environnement.

Ce que ce n'est pas : un coffre-fort sécurisé.

Le base64 est un encodage, pas un chiffrement : il ne demande aucune clé pour être inversé et sert uniquement à transporter des octets arbitraires dans un champ texte.

Fenêtre de terminal
# Le "secret" est trivial à décoder
echo "cGFzc3dvcmQ=" | base64 -d
# password

Tout utilisateur avec kubectl get secret peut lire le secret en clair.

Limite 2 : stockage en clair dans etcd (par défaut)

Section intitulée « Limite 2 : stockage en clair dans etcd (par défaut) »

Par défaut, les secrets sont stockés en clair dans etcd (la base de données de Kubernetes). Un accès à etcd = accès à tous les secrets.

Le chiffrement at-rest peut être activé (voir la section suivante), mais il n'est pas actif par défaut.

Kubernetes n'a pas de RBAC strict par défaut sur les secrets. Il faut créer des rôles explicites :

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-credentials"] # Secret spécifique
verbs: ["get"]

Kubernetes ne fournit pas nativement de génération ni de rotation métier des credentials (contrairement à Vault ou un secret manager cloud). Il n'y a pas de mécanisme intégré d'expiration ou de versioning.

En revanche, quand un Secret monté en volume est mis à jour, Kubernetes propage la nouvelle valeur dans le pod de façon "eventually consistent". Cette propagation ne fonctionne pas avec subPath.

Kubernetes fournit un mécanisme d'audit via les audit logs, mais il n'est pas activé par défaut. Il faut le configurer explicitement pour tracer qui a lu quel secret et quand.

Avec les mesures ci-dessous, les Secrets natifs deviennent acceptables pour des secrets à faible rotation dans un cluster bien géré.

Activez le chiffrement des secrets dans etcd :

/etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {} # Fallback pour les secrets non chiffrés
Fenêtre de terminal
# Configurer kube-apiserver
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml

Principe : aucun accès par défaut aux secrets.

# Rôle pour un namespace spécifique
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-secrets-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["app-db-credentials", "app-api-key"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-secrets-reader-binding
namespace: production
subjects:
- kind: ServiceAccount
name: my-app
namespace: production
roleRef:
kind: Role
name: app-secrets-reader
apiGroup: rbac.authorization.k8s.io

Activez les audit logs pour tracer les accès aux secrets :

audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
resources:
- group: ""
resources: ["secrets"]

Utiliser des volumes plutôt que des variables d'environnement

Section intitulée « Utiliser des volumes plutôt que des variables d'environnement »

Une variable d'environnement se retrouve dans /proc/<pid>/environ, dans les sorties de débogage et souvent dans les traces d'erreur remontées par les frameworks. Un fichier monté en volume reste lisible uniquement par l'utilisateur autorisé, grâce à defaultMode, et n'est jamais dupliqué dans l'environnement du processus.

# ❌ Variables d'environnement : visibles dans /proc, logs
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
# ✅ Volume : fichier avec permissions strictes
volumes:
- name: db-creds
secret:
secretName: db-credentials
defaultMode: 0400 # Lecture seule pour le owner

Les volumes permettent en plus la propagation automatique des mises à jour du Secret (sauf avec subPath), ce qui n'est pas le cas des variables d'environnement.

ESO synchronise automatiquement les secrets depuis un coffre-fort externe vers des Kubernetes Secrets. Il ne génère pas de secrets : il récupère ce qui existe dans le provider externe et le copie dans un objet Secret Kubernetes.

Flux ESO : le controller récupère les secrets depuis Vault ou AWS et les synchronise en Secrets Kubernetes

Le chart déploie un controller et les définitions de ressources personnalisées (SecretStore, ExternalSecret) qui étendent l'API du cluster.

Fenêtre de terminal
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
-n external-secrets --create-namespace

La configuration se fait en deux objets distincts : le SecretStore décrit et comment se connecter, l'ExternalSecret décrit quoi récupérer et sous quel nom créer le Secret Kubernetes. Le SecretStore est limité à son namespace, l'authentification passant ici par le ServiceAccount du pod plutôt que par un token stocké.

# 1. SecretStore : connexion à Vault
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: production
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "my-app"
serviceAccountRef:
name: my-app
---
# 2. ExternalSecret : quels secrets synchroniser
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 1h # Synchronisation périodique
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: db-credentials # Nom du Secret K8s créé
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: secret/data/production/db
property: username
- secretKey: password
remoteRef:
key: secret/data/production/db
property: password

Deux différences avec l'exemple Vault. Le ClusterSecretStore est une ressource non namespacée, utilisable depuis tous les namespaces du cluster. Et dataFrom.extract récupère toutes les clés du secret distant d'un coup, au lieu de les énumérer une par une.

apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: eu-west-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
namespace: external-secrets
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: db-credentials
dataFrom:
- extract:
key: production/db-credentials

L'intérêt principal d'ESO est de rendre les secrets compatibles avec le GitOps : le dépôt ne contient que la description de ce qu'il faut aller chercher, jamais la valeur. Le reste des bénéfices découle de ce choix.

AvantageDescription
Source unique de véritéLe coffre-fort externe reste la référence
Synchronisation automatiquerefreshInterval rafraîchit le Secret K8s périodiquement
Multi-providersVault, AWS, GCP, Azure, etc.
GitOps compatibleL'ExternalSecret est versionné dans Git, pas le secret lui-même

Le CSI Driver monte les secrets directement depuis le provider externe dans le pod, sous forme de fichiers en tmpfs.

Flux CSI Driver : le DaemonSet monte les secrets depuis Vault ou AWS directement dans un volume tmpfs du pod

Le CSI Driver peut fonctionner sans créer de Secret Kubernetes (le contenu reste uniquement dans le volume tmpfs du pod). Mais il peut aussi synchroniser optionnellement un Secret Kubernetes, nécessaire notamment si l'application consomme le secret via une variable d'environnement.

Le chart installe le driver sous forme de DaemonSet sur chaque nœud. Attention, il ne suffit pas : chaque backend (Vault, AWS, Azure) exige en plus l'installation de son provider, un composant distinct que le driver appelle pour aller chercher les valeurs.

Fenêtre de terminal
helm repo add secrets-store-csi-driver \
https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi-secrets-store \
secrets-store-csi-driver/secrets-store-csi-driver \
-n kube-system

La SecretProviderClass décrit la source et la liste des objets à monter ; le pod la référence ensuite dans son volume. Rien n'est récupéré tant qu'aucun pod ne monte le volume : le déclencheur est le montage, pas la création de la ressource.

# 1. SecretProviderClass : définit la source
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-db-creds
namespace: production
spec:
provider: vault
parameters:
vaultAddress: "https://vault.example.com"
roleName: "my-app"
objects: |
- objectName: "db-username"
secretPath: "secret/data/production/db"
secretKey: "username"
- objectName: "db-password"
secretPath: "secret/data/production/db"
secretKey: "password"
---
# 2. Pod : monte le volume CSI
apiVersion: v1
kind: Pod
metadata:
name: my-app
namespace: production
spec:
serviceAccountName: my-app
containers:
- name: app
image: my-app:latest
volumeMounts:
- name: secrets
mountPath: "/mnt/secrets"
readOnly: true
volumes:
- name: secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: vault-db-creds

Tous ces bénéfices tiennent au même mécanisme : le secret ne transite pas par l'API Kubernetes. Il n'est donc pas exposé aux comptes disposant de kubectl get secret, et il disparaît du nœud dès la fin du pod.

AvantageDescription
Pas de stockage etcdLe secret n'est pas dans etcd (sauf sync optionnelle)
Montage tmpfsLe contenu est en mémoire, pas sur disque
ÉphémèreLe secret disparaît avec le pod

La contrepartie est lourde et concerne surtout les applications existantes : si la vôtre lit ses identifiants dans une variable d'environnement au démarrage et ne les relit jamais, le CSI Driver seul ne lui conviendra pas.

LimiteImpact
Pas d'env vars directesNécessite la sync vers un Secret K8s pour les env vars
L'app doit recharger le fichierSi le secret change, l'app doit relire le fichier
Pas de restart auto des podsLe driver met à jour le mount, mais ne redémarre pas l'app
ComplexitéPlus difficile à débugger
DaemonSet requisOverhead sur chaque nœud

HashiCorp propose aussi le Vault Agent Injector, un mutation webhook qui injecte un sidecar dans les pods. Ce sidecar récupère les secrets depuis Vault et les écrit dans un volume partagé.

Cette approche est distincte d'ESO et du CSI Driver :

CritèreESOCSI DriverVault Agent Injector
Crée un Secret K8sOuiOptionnelNon
MécanismeControllerDaemonSet + volumeSidecar par pod
DépendanceMulti-providersMulti-providersVault uniquement
AuthConfigurableConfigurableAuth Kubernetes Vault

Le Vault Agent Injector est pertinent si vous utilisez déjà Vault et que vous voulez une injection par pod sans passer par un objet Secret Kubernetes. Pour un guide détaillé, consultez la documentation Vault.

Le choix entre les approches ne dépend pas uniquement de la sécurité du stockage. Le critère le plus important en production est souvent : comment l'application consomme-t-elle le secret ?

L'application...Approche recommandée
Attend une variable d'environnementESO ou CSI avec sync vers Secret K8s
Sait lire un fichier et le recharger dynamiquementCSI Driver (sans sync)
Est legacy et ne supporte aucun reloadSecret natif ou ESO (redéploiement au changement)
Est cloud-native et très sensibleCSI Driver ou Vault Agent Injector
A besoin de secrets dynamiques (credentials éphémères)Vault (secrets engine)

Aucune des trois approches n'est supérieure sur tous les critères : elles placent le curseur différemment entre simplicité d'exploitation et réduction de l'exposition. Deux lignes du tableau tranchent la plupart des débats, la présence du secret dans etcd et la capacité à alimenter une variable d'environnement.

CritèreK8s Secrets natifsExternal SecretsCSI Driver
ComplexitéFaibleMoyenneÉlevée
SécuritéAcceptable (avec durcissement)Haute (source externe)Haute (pas de stockage etcd par défaut)
RotationManuelleSynchronisation automatiqueMise à jour du mount
GitOpsSecret en clair dans Git ❌ExternalSecret dans Git ✅SecretProviderClass dans Git ✅
Secret dans etcdOuiOuiNon (sauf sync optionnelle)
Variables d'envOuiOuiVia sync vers Secret K8s
Reload applicatifVolume : oui (sauf subPath)Dépend du montageOui (si l'app relit le fichier)

Voici les situations les plus fréquentes et la réponse qui leur correspond. Une même organisation combine souvent plusieurs lignes : le durcissement des Secrets natifs reste le socle, même quand ESO ou le CSI Driver sont déployés par-dessus.

SituationRecommandation
Démarrage rapideSecrets natifs + chiffrement etcd + RBAC + audit
Production avec source externeESO avec refreshInterval
Réduire l'exposition etcdCSI Driver sans sync vers Secret K8s
Multi-clusterESO avec ClusterSecretStore
Secrets dynamiquesVault (secrets engine) + ESO ou CSI
GitOps avec secrets versionnésSealed Secrets ou SOPS
App legacy (env var, pas de reload)ESO (redéploiement au changement)

Une bascule en une fois expose à un risque simple : une application qui ne reçoit plus ses identifiants et ne redémarre plus. L'ordre ci-dessous sécurise d'abord l'existant, puis déplace les secrets un par un, en gardant à chaque étape la possibilité de revenir en arrière.

  1. Inventorier l'existant

    Avant toute migration, identifiez :

    • Quels workloads consomment quels secrets
    • Quelles applications supportent le reload de fichier
    • Quels secrets doivent rester en variable d'environnement
    • Quels accès cloud ou Vault sont nécessaires
  2. Sécuriser les Secrets natifs

    Chiffrement etcd + RBAC strict + audit logs. C'est le socle minimum, même si vous prévoyez de migrer vers ESO ou CSI.

  3. Déployer ESO

    Commencez par synchroniser quelques secrets non critiques et vérifiez que le refreshInterval fonctionne correctement.

  4. Migrer progressivement

    Remplacez les Secrets natifs par des ExternalSecrets. Validez le comportement applicatif à chaque étape (l'app reçoit-elle bien le secret ? le recharge-t-elle ?).

  5. Supprimer les Secrets manuels

    Une fois la migration validée, supprimez les définitions manuelles.

  6. Évaluer CSI Driver

    Pour les workloads les plus sensibles dont l'application sait relire un fichier, envisagez le CSI Driver.

  1. Les Secrets natifs sont acceptables avec durcissement, chiffrement etcd, RBAC strict, audit logs et montage en volume
  2. La sécurité dépend aussi du nœud, un pod privilégié accède à tous les secrets du nœud
  3. ESO synchronise, il ne génère pas, la rotation réelle se fait côté provider (Vault, AWS, etc.)
  4. Le CSI Driver peut synchroniser un Secret K8s, c'est optionnel, mais nécessaire pour les variables d'environnement
  5. Le critère de choix principal est la consommation, env var, fichier rechargeable, sidecar, secret dynamique
  6. Volumes plutôt que variables d'environnement, moins d'exposition, propagation automatique

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn