Aller au contenu
English
Conteneurs & Orchestration medium

Secrets Kubernetes : stocker les données sensibles

50 min de lecture

logo kubernetes

Un Secret Kubernetes permet de stocker des données sensibles comme des mots de passe, des clés API, des tokens ou des certificats. Il fournit un conteneur dédié pour ce type d'information, mais sa sécurité dépend surtout du RBAC, du chiffrement au repos et de la manière dont vous l'injectez dans vos Pods.

Ce guide montre comment créer un Secret, choisir son type, l'injecter dans un Pod et appliquer les bonnes pratiques de sécurité.

Prérequis : un cluster Kubernetes avec kubectl configuré.

  • Comprendre ce qu'un Secret protège (et ce qu'il ne protège pas)
  • Créer des Secrets de différents types (Opaque, TLS, docker-registry)
  • Injecter les Secrets dans vos Pods (env ou volume)
  • Appliquer les bonnes pratiques de sécurité essentielles
  • Décider quand passer à un gestionnaire de secrets externe

Ce qui différencie vraiment un Secret d'un ConfigMap

Section intitulée « Ce qui différencie vraiment un Secret d'un ConfigMap »

Un Secret est pensé pour des données sensibles, mais il ne devient réellement sûr que si vous montez les protections autour : RBAC strict, chiffrement au repos dans etcd, accès à l'API limités, et montage en volume plutôt qu'en variables d'environnement. Aucune de ces quatre protections n'est automatique. Par défaut, un Secret reste lisible par toute identité autorisée à le récupérer via l'API.

Cela posé, le type Secret n'est pas qu'une étiquette : Kubernetes lui réserve trois traitements qu'un ConfigMap n'a pas, tous observables en une commande.

kubectl describe masque les valeurs d'un Secret et affiche seulement leur taille, là où il imprime en clair celles d'un ConfigMap. C'est la protection la plus concrète au quotidien, celle qui évite d'étaler un mot de passe dans un terminal partagé ou une capture d'écran.

Fenêtre de terminal
kubectl describe secret db-credentials
Sortie
Type: Opaque
Data
====
password: 11 bytes
username: 5 bytes

Cette protection ne tient qu'à la commande employée, et elle s'annule trivialement. kubectl get -o yaml ne masque rien : il rend le champ data tel quel, et le base64 se décode en une seconde.

Fenêtre de terminal
kubectl get secret db-credentials -o yaml
Sortie
data:
password: UzNjcjN0IVBAc3M=
username: YWRtaW4=
kind: Secret

Retenez donc ce que describe protège réellement : le coup d'oeil par-dessus l'épaule, la capture d'écran, le copier-coller dans un ticket. Il ne protège de personne ayant le droit de lire le Secret, puisqu'une seule autre commande suffit.

Un Secret monté est écrit en tmpfs, donc en mémoire, et n'atteint jamais le disque du nœud. Un ConfigMap monté, lui, est un fichier ordinaire du système de fichiers du nœud :

Fenêtre de terminal
kubectl exec mon-pod -- stat -f -c '%n : %T' /etc/secrets /etc/conf
Sortie
/etc/secrets : tmpfs
/etc/conf : ext2/ext3

Le chiffrement au repos d'etcd se configure pour la ressource secrets, et c'est le seul de ces trois mécanismes qui ne soit pas actif par défaut : tant que vous ne l'avez pas activé, vos Secrets sont écrits en clair sur le disque du control plane.

Ce que le type Secret n'apporte pas, en revanche : aucune confidentialité vis-à-vis de qui possède le droit de lecture sur l'API. C'est le RBAC qui décide, pas le type de la ressource.

C'est le piège le plus courant. Beaucoup pensent que l'encodage base64 protège leurs données. C'est faux. N'importe qui le décode en une seconde, par exemple avec CyberChef.

Fenêtre de terminal
# Créer un Secret
kubectl create secret generic demo-secret --from-literal=password='S3cr3t!P@ss'
# Voir le Secret "protégé"
kubectl get secret demo-secret -o jsonpath='{.data.password}'
Sortie
UzNjcjN0IVBAc3M=
Fenêtre de terminal
# Décoder en une commande
echo 'UzNjcjN0IVBAc3M=' | base64 -d
Sortie
S3cr3t!P@ss

Le décodage est instantané. Base64 sert uniquement à transporter des données binaires dans du YAML/JSON, pas à les protéger.

La conséquence est directe : quiconque peut exécuter kubectl get secret dans votre namespace lit tous vos mots de passe, sans outil ni privilège particulier. Le RBAC est votre première ligne de défense, et la seule qui vaille tant que le chiffrement au repos n'est pas activé.

Kubernetes propose plusieurs types de Secrets, chacun avec une structure adaptée :

TypeUsageClés créées
OpaqueUsage générique (mots de passe, tokens)Libres
kubernetes.io/tlsCertificats TLStls.crt, tls.key
kubernetes.io/dockerconfigjsonAuthentification registry.dockerconfigjson
kubernetes.io/basic-authAuthentification HTTP basiqueusername, password
kubernetes.io/ssh-authClé SSHssh-privatekey

La colonne de droite n'est pas indicative : ces clés sont obligatoires, et l'API refuse le Secret si elles manquent. Un type kubernetes.io/tls déclaré avec des clés nommées autrement est rejeté avant d'exister.

Sortie
The Secret "tls-faux" is invalid:
* data[tls.crt]: Required value
* data[tls.key]: Required value

La validation s'arrête cependant à la structure, jamais au contenu. Un Secret TLS dont tls.crt contient du texte quelconque est accepté, avec un simple avertissement côté client, Warning: tls: failed to find any PEM data in certificate input. Ne comptez donc pas sur Kubernetes pour vérifier vos certificats.

Dernier point à connaître avant de choisir : le type est immuable. Un Secret créé en Opaque ne deviendra jamais un Secret TLS, il faudra le supprimer et le recréer.

Sortie
* type: Invalid value: "kubernetes.io/tls": field is immutable

Le choix de la méthode a une conséquence de sécurité immédiate. La ligne de commande est la plus rapide, mais elle inscrit le mot de passe dans l'historique de votre shell et le rend visible dans la liste des processus le temps de son exécution. Le manifeste YAML évite cela, au prix d'un fichier qui ne doit jamais atteindre Git.

La méthode la plus simple pour des identifiants :

Fenêtre de terminal
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='S3cr3t!P@ss'

Vérifiez la création :

Fenêtre de terminal
kubectl get secret db-credentials
Sortie
NAME TYPE DATA AGE
db-credentials Opaque 2 5s

Pour stocker un certificat et sa clé privée :

Fenêtre de terminal
kubectl create secret tls my-tls-secret \
--cert=server.crt \
--key=server.key
Fenêtre de terminal
kubectl get secret my-tls-secret
Sortie
NAME TYPE DATA AGE
my-tls-secret kubernetes.io/tls 2 5s

Ce type répond à un besoin précis : c'est le kubelet, pas votre application, qui doit s'authentifier pour tirer l'image. Le Secret n'est donc pas monté dans le conteneur mais déclaré au niveau du Pod, via imagePullSecrets.

Fenêtre de terminal
kubectl create secret docker-registry my-registry \
--docker-server=registry.example.com \
--docker-username=myuser \
--docker-password=mypassword

Utilisez-le dans un Pod avec imagePullSecrets :

spec:
imagePullSecrets:
- name: my-registry
containers:
- name: app
image: registry.example.com/myapp:1.5.0

Pour un contrôle total et la gestion en GitOps :

secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
username: YWRtaW4= # echo -n 'admin' | base64
password: UzNjcjN0IVBAc3M= # echo -n 'S3cr3t!P@ss' | base64

Vous pouvez aussi utiliser stringData pour éviter l'encodage manuel :

secret-stringdata.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
username: admin
password: S3cr3t!P@ss

Kubernetes convertit automatiquement stringData en data encodé.

Attention à ne pas y voir une amélioration de sécurité : stringData simplifie l'écriture du manifeste, il ne rend pas le fichier plus sûr. Le secret y figure en clair avant application, ce qui est même plus exposé qu'un base64 dans un historique de shell ou une pièce jointe.

Deux méthodes principales, chacune avec ses cas d'usage :

MéthodeCas d'usageMise à jour auto
Variable d'environnementIdentifiants simplesJamais
VolumeCertificats, fichiers de configOui, jusqu'à 2 min
deployment-secret-env.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: app
image: nginx:1.29-alpine@sha256:5616878291a2eed594aee8db4dade5878cf7edcb475e59193904b198d9b830de
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password

Vérifiez l'injection :

Fenêtre de terminal
kubectl exec deploy/myapp -- env | grep DB_
Sortie
DB_USERNAME=admin
DB_PASSWORD=S3cr3t!P@ss

Les valeurs sont décodées automatiquement dans le conteneur : vous ne manipulez jamais de base64 côté application.

Pour charger toutes les clés d'un coup, envFrom accepte un secretRef, exactement comme il accepte un configMapRef. Chaque clé du Secret devient une variable portant son nom, ce qui suppose des clés déjà nommées comme des variables.

envFrom:
- secretRef:
name: db-credentials

Pratique, mais à peser : vous perdez la liste explicite de ce que le Pod reçoit. Une clé ajoutée au Secret arrive dans tous les conteneurs qui le chargent ainsi, sans que leur manifeste change.

Recommandé pour les certificats et fichiers de configuration :

deployment-secret-volume.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: app
image: nginx:1.29-alpine@sha256:5616878291a2eed594aee8db4dade5878cf7edcb475e59193904b198d9b830de
volumeMounts:
- name: secret-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: db-credentials
defaultMode: 0400

Vérifiez les fichiers montés :

Fenêtre de terminal
kubectl exec deploy/myapp -- ls -la /etc/secrets
Sortie
total 8
drwxrwxrwt 3 root root 120 Sep 13 16:56 .
drwxr-xr-x 1 root root 4096 Sep 13 16:56 ..
drwxr-xr-x 2 root root 80 Sep 13 16:56 ..2026_09_13_16_56_55.1418541054
lrwxrwxrwx 1 root root 32 Sep 13 16:56 ..data -> ..2026_09_13_16_56_55.1418541054
lrwxrwxrwx 1 root root 15 Sep 13 16:56 password -> ..data/password
lrwxrwxrwx 1 root root 15 Sep 13 16:56 username -> ..data/username

Cette arborescence n'est pas du bruit, elle est le mécanisme de mise à jour. Les vraies données vivent dans le répertoire horodaté. ..data est un lien vers lui, et chaque clé est un lien vers ..data. Pour publier une nouvelle version, le kubelet écrit un répertoire horodaté neuf, puis fait basculer le seul lien ..data : l'opération est atomique, votre application ne lit jamais un jeu de fichiers à moitié mis à jour. Comptez jusqu'à deux minutes avant la bascule, comme pour un ConfigMap, le kubelet additionnant sa période de synchronisation et la durée de vie de son cache. Mesuré ici : 68 secondes.

defaultMode attend un entier, et le zéro de tête est ce qui le rend octal. Les trois écritures ci-dessous n'ont rien d'équivalent, et deux passent sans la moindre erreur :

ÉcritureVerdict de l'APIPermissions réelles du fichier
defaultMode: 0400acceptée-r--------, ce que vous vouliez
defaultMode: 400acceptée aussi-rw--w----, groupe en écriture
defaultMode: "0400"refuséecannot unmarshal string ... of type int32

Le cas du milieu est le dangereux : un mot de passe monté en lecture seule devient inscriptible par le groupe, sans le moindre avertissement. Notez que la règle est ici l'inverse de celle des modules de fichiers Ansible, où le mode se quote toujours : ce champ-ci refuse la chaîne de caractères.

Encore faut-il pouvoir le vérifier, et c'est là que le montage piège une seconde fois. La commande réflexe, ls -l, montre le lien symbolique, dont les permissions sont toujours lrwxrwxrwx et ne veulent rien dire :

Fenêtre de terminal
kubectl exec mon-pod -- ls -l /etc/secrets/password # le lien
kubectl exec mon-pod -- ls -lL /etc/secrets/password # le fichier derrière
Sortie
lrwxrwxrwx 1 root root 15 /etc/secrets/password -> ..data/password
-rw--w---- 1 root root 11 /etc/secrets/password

C'est ls -lL, qui suit le lien, ou stat sur le fichier réel, qui donnent la vérité. Plus simple encore, contrôlez avant même de déployer : l'API convertit le mode octal en décimal, et la valeur stockée trahit immédiatement l'erreur.

Fenêtre de terminal
kubectl get pod mon-pod -o jsonpath='{.spec.volumes[0].secret.defaultMode}'
Ce que vous avez écritValeur stockéePermissions obtenues
0400256-r--------
400400-rw--w----
0644420-rw-r--r--

Une valeur stockée identique à ce que vous avez tapé est le signe qu'il manque le zéro de tête.

volumes:
- name: secret-volume
secret:
secretName: db-credentials
items:
- key: password
path: db-password

Le fichier /etc/secrets/db-password contient uniquement le mot de passe.

Ces cinq pratiques se renforcent mutuellement et aucune ne suffit seule. Le RBAC décide qui peut lire, le chiffrement au repos protège le disque du control plane, le montage en volume limite l'exposition dans le Pod, la rotation borne la durée de vie d'une fuite, et l'immutabilité empêche une modification accidentelle.

Par défaut, tout utilisateur avec get sur les Secrets peut les lire. Créez des Roles restrictifs :

role-secrets-readonly.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-credentials"] # Limiter aux Secrets nécessaires
verbs: ["get"]

Le verbe list mérite une attention particulière : accordé largement, il permet d'énumérer tous les noms de Secrets du namespace. Un nom comme stripe-prod-api-key renseigne déjà un attaquant sur ce qui existe et sur ce qu'il vaut la peine d'attaquer. Préférez get avec resourceNames, comme ci-dessus.

Par défaut, Kubernetes stocke les Secrets en clair dans etcd. Activez le chiffrement au repos :

encryption-config.yaml (sur le control plane)
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {}

Sur un cluster managé, vous n'avez pas la main sur ce fichier : le chiffrement au repos est un réglage du fournisseur. Ne le supposez pas actif, allez lire sa documentation et vérifiez comment il est configuré, la réponse varie d'un fournisseur à l'autre et parfois d'une option de facturation à l'autre.

3. Préférer le montage volume aux variables d'environnement

Section intitulée « 3. Préférer le montage volume aux variables d'environnement »
AspectVariable envVolume
Visible dans docker inspectOuiNon
Visible dans /proc/<pid>/environOuiNon
Mise à jour automatiqueNonOui

Le montage en volume est préférable : il limite les expositions accidentelles et bénéficie des mises à jour automatiques. Mais il ne remplace ni un cloisonnement des accès ni un durcissement du runtime : si le conteneur est compromis, les fichiers montés restent lisibles par le processus.

Kubernetes n'offre aucune rotation automatique : c'est à vous de remplacer la valeur, puis de forcer les Pods à la reprendre. Le --dry-run=client suivi d'un apply sert à écraser un Secret existant, là où un create seul échouerait sur un objet déjà présent.

Fenêtre de terminal
# Mettre à jour le Secret
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='N0uv3@uP@ss!' \
--dry-run=client -o yaml | kubectl apply -f -
# Redémarrer les Pods pour appliquer
kubectl rollout restart deployment myapp

Comme un ConfigMap, un Secret accepte le champ immutable: true. L'API refuse alors toute modification de son contenu, avec le même message :

Sortie
The Secret "fige" is invalid: data: Forbidden: field is immutable when `immutable` is set

Le bénéfice n'est pas que défensif. Un Secret immutable n'est plus surveillé par les kubelets : sur un gros cluster, cela retire un flux d'observation permanent par Secret et par nœud. Le coût est le versionnement, qui devient obligatoire, db-credentials-v2 remplaçant db-credentials.

La contrepartie mérite d'être pesée pour un Secret précisément : une rotation d'urgence, après une fuite, demande alors de créer un nouvel objet et de modifier tous les Pods qui le référencent. Réservez l'immutabilité aux secrets dont la rotation est planifiée, pas à ceux que vous pourriez devoir révoquer dans la minute.

Les Secrets Kubernetes natifs ont des limites :

LimiteImpact
Pas de rotation automatiqueVous devez mettre à jour manuellement
Pas d'audit détailléQui a accédé à quel Secret ?
Chiffrement etcd à configurerPas activé par défaut
Secrets visibles en YAMLRisque de commit accidentel

Utilisez un gestionnaire externe si vous avez besoin de :

  • Rotation automatique des secrets
  • Audit détaillé des accès
  • Intégration avec des secrets existants (AWS, Azure, GCP)
  • Secrets dynamiques (credentials de BDD à durée limitée)

Solutions recommandées :

  • HashiCorp Vault avec External Secrets ou Vault Sidecar
  • AWS Secrets Manager / Azure Key Vault / GCP Secret Manager
  • Sealed Secrets pour chiffrer les Secrets dans Git
  1. Le Secret existe-t-il ?

    Fenêtre de terminal
    kubectl get secret db-credentials

    Si "NotFound", créez-le d'abord.

  2. La clé existe-t-elle dans le Secret ?

    Fenêtre de terminal
    kubectl get secret db-credentials -o jsonpath='{.data}'

    Vérifiez l'orthographe exacte de la clé.

  3. Le Pod référence-t-il le bon Secret ?

    Fenêtre de terminal
    kubectl describe pod myapp-xxx | grep -A5 "Environment\|Mounts"
  4. Erreur CreateContainerConfigError ?

    Secret ou clé introuvable. Vérifiez que le Secret existe dans le même namespace que le Pod.

ErreurCauseSolution
CreateContainerConfigErrorSecret ou clé introuvableVérifier nom et namespace
Variable videClé mal orthographiéekubectl get secret -o yaml
unauthorizedRBAC insuffisantVérifier les permissions

La première ne se devine pas, Kubernetes nomme la clé qu'il a cherchée et le Secret où il ne l'a pas trouvée :

Fenêtre de terminal
kubectl get pod mon-pod \
-o jsonpath='{.status.containerStatuses[0].state.waiting.message}'
Sortie
couldn't find key CLE-ABSENTE in Secret production/db-credentials
  1. base64 n'est pas du chiffrement, c'est un encodage de transport qui se décode en une commande
  2. Ce que le type Secret apporte réellement : describe masque les valeurs, le volume est monté en tmpfs, et le chiffrement au repos d'etcd ne vise que lui
  3. Le masquage de describe ne tient qu'à la commande : get -o yaml rend le data en clair
  4. Les types de Secrets (Opaque, TLS, docker-registry) structurent vos données
  5. Variables d'environnement : simples mais jamais rafraîchies ; volumes : mise à jour atomique via ..data, en jusqu'à 2 minutes
  6. defaultMode: 0400 et defaultMode: 400 sont tous deux acceptés et n'ont pas le même sens ; ls -l montre le lien, ls -lL montre le fichier
  7. RBAC est votre première ligne de défense, et le droit de créer un Pod vaut droit de lecture
  8. Chiffrement etcd : activez-le pour protéger les Secrets au repos
  9. immutable: true fige un Secret et le retire de la surveillance des kubelets, au prix d'une rotation d'urgence plus lourde
  10. Pour la rotation automatique ou l'audit, passez à un gestionnaire externe

Sept questions sur les points qui coûtent cher : ce que base64 ne fait pas, ce qui se rafraîchit, et ce qu'un droit de création de Pod permet vraiment.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

7 questions
5 min.
80% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

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