Aller au contenu
Sécurité medium

Secrets Kubernetes : bonnes pratiques

18 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
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é.

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 »
# ❌ 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

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
# 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
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
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.

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
# 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
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
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)
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)
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)
  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 tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn