Sécurité Kubernetes : 15 points critiques avant la production
Kubernetes est devenu le standard de facto pour orchestrer des conteneurs en production. Mais sa puissance vient avec une complexité importante, et la surface d’attaque est large. Un cluster mal sécurisé, c’est une porte ouverte : fuites de données, cryptomining, compromission complète du cluster.
En 2024, 84% des organisations utilisant Kubernetes ont subi au moins un incident de sécurité (rapport Red Hat State of Kubernetes Security). Les misconfigurations représentent 90% de ces incidents.
La bonne nouvelle ? La majorité de ces problèmes sont évitables avec des pratiques simples et une checklist bien suivie.
Après avoir déployé Kubernetes en production aussi bien chez des CloudProvider que On-Premise, voici les 15 points critiques à valider avant d’aller en production.
🔐 Catégorie 1 : Authentification & Autorisation
Point 1 : RBAC activé et configuré finement
Le problème : Par défaut, Kubernetes peut avoir des permissions trop larges. Un pod compromis avec des permissions excessives peut escalader ses privilèges et compromettre tout le cluster.
La solution :
- Activer RBAC (obligatoire depuis Kubernetes 1.6)
- Appliquer le principe du moindre privilège
- Créer des
Role(namespace-scoped) ouClusterRole(cluster-wide) spécifiques - Assigner des
ServiceAccountdédiés par application
Exemple pratique :
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: my-app
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: my-app
subjects:
- kind: ServiceAccount
name: my-app-sa
namespace: my-app
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
Vérification :
# Tester les permissions d'un ServiceAccount
kubectl auth can-i --list --as=system:serviceaccount:my-app:my-app-sa
Retour d’expérience : Avec une isolation multi-tenancy via VCluster, nous avons configuré RBAC finement par équipe. Chaque équipe ne voit que ses propres ressources, évitant les erreurs de manipulation.
Point 2 : Authentification API Server renforcée
Le problème : Un accès non sécurisé à l’API Server Kubernetes permet à un attaquant de contrôler entièrement le cluster.
La solution :
- Désactiver l’authentification anonyme :
--anonymous-auth=false - Utiliser OIDC pour l’authentification (Keycloak, Okta, Azure AD)
- Client certificates avec expiration automatique
- Activer les audit logs pour tracer toutes les actions
Configuration API Server :
# kube-apiserver flags
--anonymous-auth=false
--oidc-issuer-url=https://idp.example.com
--oidc-client-id=kubernetes
--oidc-username-claim=email
--oidc-groups-claim=groups
--audit-log-path=/var/log/kubernetes/audit.log
--audit-log-maxage=30
Pourquoi OIDC ? Les tokens générés ont une durée de vie limitée et peuvent être révoqués centralement. C’est bien plus sécurisé que des certificats clients permanents.
Point 3 : ServiceAccount tokens avec expiration
Le problème : Historiquement, les tokens ServiceAccount Kubernetes n’expiraient jamais. Un token volé restait valide indéfiniment.
La solution (Kubernetes 1.21+) :
- Utiliser Bound ServiceAccount Token Volumes
- Les tokens expirent automatiquement (1 heure par défaut)
- Rotation automatique par kubelet
Configuration pod :
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
serviceAccountName: my-sa
automountServiceAccountToken: false # Si l'app n'a pas besoin d'accès API K8s
containers:
- name: app
image: my-app:1.0
# Si besoin d'accès API, le token est monté automatiquement avec expiration
Important : Si votre application n’a pas besoin d’interagir avec l’API Kubernetes, mettez automountServiceAccountToken: false. C’est un vecteur d’attaque en moins.
🌐 Catégorie 2 : Réseau & Isolation
Point 4 : Network Policies actives
Le problème : Par défaut dans Kubernetes, tous les pods peuvent communiquer entre eux. Un pod compromis peut attaquer latéralement tout le cluster.
La solution :
- Activer un plugin CNI supportant les Network Policies (Calico, Cilium, Weave Net)
- Créer une politique default deny all
- Whitelister uniquement les flux nécessaires
Exemple : Default Deny All :
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: my-app
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Exemple : Autoriser uniquement frontend → backend :
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: my-app
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
Retour d’expérience : Sur une architecture haute performance (800k points/min), les Network Policies ont été essentielles pour isoler les flux critiques et éviter qu’un service compromis puisse accéder aux bases de données.
Point 5 : Ingress sécurisé (TLS + WAF)
Le problème : Traffic HTTP non chiffré = interception facile. Pas de protection applicative = vulnérabilités exploitables.
La solution :
- TLS obligatoire avec Let’s Encrypt (cert-manager)
- WAF (Web Application Firewall) : ModSecurity, Cloudflare
- Rate limiting pour éviter les attaques DDoS
Configuration avec cert-manager :
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
tls:
- hosts:
- myapp.example.com
secretName: myapp-tls
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80
Bonus : Avec Cloudflare devant votre Ingress, vous bénéficiez de leur WAF et protection DDoS gratuitement (plan gratuit) ou avancée (plan payant).
Point 6 : Egress control (sortie Internet maîtrisée)
Le problème : Par défaut, les pods peuvent appeler n’importe quelle IP externe. Un malware peut exfiltrer des données ou télécharger des outils d’attaque.
La solution :
- Network Policies egress pour whitelister les destinations
- Proxy sortant (Squid, Cloud NAT) avec logs
- Bloquer par défaut, autoriser explicitement
Exemple : Bloquer tout egress sauf DNS et API externe spécifique :
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-and-api
namespace: my-app
spec:
podSelector:
matchLabels:
app: my-app
policyTypes:
- Egress
egress:
# Allow DNS
- to:
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: UDP
port: 53
# Allow specific external API
- to:
- ipBlock:
cidr: 203.0.113.0/24 # IP API externe
ports:
- protocol: TCP
port: 443
Use case : Empêcher un pod compromis d’appeler un serveur C&C (Command & Control) externe.
🛡️ Catégorie 3 : Workloads & Pods
Point 7 : Pod Security Standards (PSS)
Le problème : Un pod avec privileged: true a accès root sur le node. C’est une compromission totale du node.
La solution (Kubernetes 1.25+) :
- Activer Pod Security Admission
- Appliquer le niveau Restricted par défaut (le plus strict)
- Trois niveaux : Privileged (permissif), Baseline (minimal), Restricted (strict)
Configuration namespace :
apiVersion: v1
kind: Namespace
metadata:
name: my-app
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
Que bloque “Restricted” ? :
- Pods privileged
- Root user (UID 0)
- Host network, PID, IPC
- Capabilities non sécurisées
- Volumes host path
Point 8 : Security Context des pods
Le problème : Les conteneurs s’exécutent souvent en root par défaut. Une faille applicative = accès root dans le conteneur.
La solution :
runAsNonRoot: true(bloquer root)readOnlyRootFilesystem: true(filesystem immutable)allowPrivilegeEscalation: false(pas d’escalade)- Dropper toutes les capabilities (
drop: ["ALL"])
Configuration sécurisée :
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: my-app:1.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
Note : readOnlyRootFilesystem: true nécessite de monter un volume emptyDir pour /tmp et autres répertoires où l’app écrit.
Point 9 : Image scanning & trusted registries
Le problème : Les images Docker contiennent souvent des vulnérabilités connues (CVEs). Utiliser une image avec une CVE critique = porte ouverte.
La solution :
- Scan d’images : Trivy, Clair, Snyk, Anchore
- Bloquer les images avec CVEs HIGH ou CRITICAL
- Registry privé : Harbor, Artifactory, AWS ECR
- Image signing : Cosign, Notary (supply chain security)
Exemple avec Trivy (CI/CD) :
# Scanner une image
trivy image myapp:latest \
--severity HIGH,CRITICAL \
--exit-code 1 # Fail CI si vulnérabilités trouvées
# Intégration GitHub Actions
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:latest
severity: 'HIGH,CRITICAL'
exit-code: '1'
Retour d’expérience : Nous avons intégré Trivy dans GitLab CI. Toute image avec une CVE CRITICAL bloque le pipeline. Cela a détecté plusieurs vulnérabilités critiques avant qu’elles n’atteignent la production.
Point 10 : Resource limits & quotas
Le problème : Un pod sans limits peut consommer toutes les ressources du node (CPU, RAM), causant un déni de service.
La solution :
- Requests & Limits sur tous les pods
- ResourceQuotas par namespace
- LimitRanges pour imposer des valeurs par défaut
Configuration pod :
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
image: my-app:1.0
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
ResourceQuota namespace :
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-quota
namespace: my-app
spec:
hard:
requests.cpu: "10"
requests.memory: "20Gi"
limits.cpu: "20"
limits.memory: "40Gi"
pods: "50"
Important : Sans limits, un bug applicatif (memory leak) peut crasher tout le node.
🔑 Catégorie 4 : Secrets & Données sensibles
Point 11 : Secrets encryption at rest
Le problème : Par défaut, les secrets Kubernetes sont stockés en clair dans etcd (base de données du cluster). Un accès à etcd = tous les secrets exposés.
La solution :
- Activer l’encryption at rest
- Utiliser un KMS provider (AWS KMS, Azure Key Vault, HashiCorp Vault)
Configuration EncryptionConfiguration :
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {} # Fallback (si clé indisponible, lecture possible mais pas création)
API Server flag :
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml
Avec AWS KMS (plus sécurisé) :
providers:
- kms:
name: aws-kms
endpoint: unix:///var/run/kmsplugin/socket.sock
cachesize: 1000
Point 12 : External Secrets Operator ou Sealed Secrets
Le problème : Stocker des secrets dans Git (même chiffrés) = risque de compromission. Les secrets doivent rester hors de Git.
La solution :
Option A : External Secrets Operator (recommandé)
- Secrets stockés dans Vault, AWS Secrets Manager, Azure Key Vault
- ESO synchronise automatiquement vers Kubernetes
- Rotation automatique possible
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: my-app
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: db-secret
creationPolicy: Owner
data:
- secretKey: password
remoteRef:
key: secret/data/database
property: password
Option B : Sealed Secrets (plus simple)
- Chiffre les secrets avec une clé publique
- Peut être commité dans Git
- Sealed Secrets controller déchiffre dans le cluster
# Créer un SealedSecret
kubeseal --format yaml < secret.yaml > sealed-secret.yaml
# Commiter sealed-secret.yaml (sécurisé)
git add sealed-secret.yaml
git commit -m "Add database credentials (sealed)"
Retour d’expérience : Nous avons utilisé Sealed Secrets pour GitOps (ArgoCD). Les secrets sont versionnés et déployés automatiquement, sans jamais être en clair dans Git.
Point 13 : Pas de secrets en variables d’environnement
Le problème : Les secrets passés en variables d’environnement sont visibles dans kubectl describe pod et dans les logs. Ils peuvent fuiter facilement.
La solution :
- Monter les secrets comme volumes (fichiers)
- Lecture depuis le filesystem
- Pas d’exposition dans
describeou logs
Mauvais (ENV vars) ❌ :
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
Bon (Volume mount) ✅ :
containers:
- name: app
image: my-app:1.0
volumeMounts:
- name: db-credentials
mountPath: /etc/secrets
readOnly: true
volumes:
- name: db-credentials
secret:
secretName: db-secret
Application lit :
# /etc/secrets/password contient le mot de passe
password=$(cat /etc/secrets/password)
Bonus : Rotation automatique possible (ESO met à jour le secret, kubelet monte automatiquement la nouvelle version).
📊 Catégorie 5 : Observabilité & Compliance
Point 14 : Audit logs activés et centralisés
Le problème : Sans audit logs, impossible de savoir qui a fait quoi sur le cluster. En cas d’incident, pas de traçabilité.
La solution :
- Activer les audit logs de l’API Server
- Centraliser les logs (ELK, Loki, SIEM)
- Alerter sur actions sensibles (delete, exec, secrets)
Configuration Audit Policy :
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Log toutes les actions sur secrets
- level: RequestResponse
verbs: ["create", "update", "patch", "delete"]
resources:
- group: ""
resources: ["secrets"]
# Log tous les exec dans pods
- level: RequestResponse
verbs: ["create"]
resources:
- group: ""
resources: ["pods/exec"]
# Log modifications RBAC
- level: RequestResponse
verbs: ["create", "update", "patch", "delete"]
resources:
- group: "rbac.authorization.k8s.io"
resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]
# Metadata seulement pour les autres actions (moins verbeux)
- level: Metadata
API Server flags :
--audit-policy-file=/etc/kubernetes/audit-policy.yaml
--audit-log-path=/var/log/kubernetes/audit.log
--audit-log-maxage=30
--audit-log-maxbackup=10
--audit-log-maxsize=100
Centralisation avec Loki :
# Promtail daemonset pour collecter audit logs
- job_name: kubernetes-audit
static_configs:
- targets:
- localhost
labels:
job: kubernetes-audit
__path__: /var/log/kubernetes/audit.log
Point 15 : Scanning continu avec outils automatisés
Le problème : La configuration d’un cluster dérive dans le temps (changements manuels, erreurs). Un audit ponctuel ne suffit pas.
La solution : Outils de scanning continu
1. Kube-bench (CIS Kubernetes Benchmark)
# Lancer un audit CIS
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
# Voir les résultats
kubectl logs job/kube-bench
Exemple output :
[INFO] 1 Master Node Security Configuration
[PASS] 1.1.1 Ensure that the API server pod specification file permissions are set to 644 or more restrictive
[FAIL] 1.2.5 Ensure that the --audit-log-path argument is set
[WARN] 1.2.12 Ensure that the admission control plugin AlwaysPullImages is set
2. Kube-hunter (Penetration testing)
# Scanner un cluster (depuis l'extérieur)
docker run -it --rm aquasec/kube-hunter --remote <cluster-ip>
# Scanner depuis l'intérieur (pod)
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-hunter/main/job.yaml
3. Falco (Runtime security)
- Détecte comportements anormaux en temps réel
- Alertes sur actions suspectes
Exemple règle Falco :
- rule: Shell spawned in container
desc: Détecte l'exécution d'un shell dans un conteneur
condition: spawned_process and container and proc.name in (shell_binaries)
output: "Shell spawned in container (user=%user.name container=%container.name image=%container.image.repository)"
priority: WARNING
4. OPA/Gatekeeper (Policy as Code)
- Valide les manifests avant déploiement
- Bloque les configurations non conformes
Exemple policy Gatekeeper :
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-app-label
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
labels:
- key: "app"
Retour d’expérience : Chez un client, nous avons déployé Falco pour détecter du cryptomining (charge CPU anormale). Nous avons détecté et bloqué une tentative d’exploitation de vulnérabilité Log4Shell en quelques minutes.
✅ Checklist complète (à imprimer)
## 🔐 Authentification & Autorisation
- [ ] RBAC activé et configuré finement (principe moindre privilège)
- [ ] Authentification API Server renforcée (OIDC, pas d'anonymous auth)
- [ ] ServiceAccount tokens avec expiration (Bound SA Tokens)
## 🌐 Réseau & Isolation
- [ ] Network Policies actives (default deny, whitelist explicite)
- [ ] Ingress sécurisé (TLS obligatoire, WAF, rate limiting)
- [ ] Egress control (sortie Internet maîtrisée)
## 🛡️ Workloads & Pods
- [ ] Pod Security Standards (niveau Restricted appliqué)
- [ ] Security Context (runAsNonRoot, readOnlyRootFS, drop capabilities)
- [ ] Image scanning (bloquer CVE HIGH/CRITICAL en CI/CD)
- [ ] Resource limits & quotas (tous les pods ont requests/limits)
## 🔑 Secrets & Données sensibles
- [ ] Secrets encryption at rest (KMS provider configuré)
- [ ] External Secrets Operator ou Sealed Secrets (secrets hors Git)
- [ ] Pas de secrets en variables d'environnement (volume mounts)
## 📊 Observabilité & Compliance
- [ ] Audit logs activés et centralisés (actions tracées)
- [ ] Scanning continu (Kube-bench, Falco, OPA/Gatekeeper)
Priorisation si ressources limitées
Vous n’avez pas le temps de tout faire ? Voici les must-have absolus (P0) :
Must-have (P0)
- RBAC configuré (Point 1)
- Network Policies actives (Point 4)
- Pod Security Standards (Point 7)
- Image scanning (Point 9)
Ces 4 points couvrent 80% des risques critiques.
Should-have (P1)
- Secrets encryption at rest (Point 11)
- Audit logs (Point 14)
- Security Context (Point 8)
- TLS Ingress (Point 5)
Nice-to-have (P2)
- OIDC authentication (Point 2)
- Falco runtime security (Point 15)
- OPA/Gatekeeper policies (Point 15)
- External Secrets Operator (Point 12)
Conclusion
Sécuriser Kubernetes n’est pas sorcier, mais cela demande rigueur et méthode. Ces 15 points ne sont pas exhaustifs, mais ils couvrent les vulnérabilités les plus fréquentes et les plus critiques.
Chez PeriScop, nous avons appliqué ces pratiques sur nos missions Kubernetes. Résultat : zéro incident de sécurité majeur, conformité audits, et équipes sereines.
La sécurité Kubernetes est un processus itératif :
- Commencez par les must-have (P0)
- Ajoutez progressivement les autres points
- Automatisez les scans (CI/CD, scanning continu)
- Formez vos équipes
N’attendez pas un incident pour agir. La plupart des compromissions Kubernetes sont dues à des misconfigurations basiques, facilement évitables.
Besoin d’aide ?
Vous souhaitez un audit de sécurité de votre cluster Kubernetes ? PeriScop vous accompagne :
- Audit production-readiness (checklist complète)
- Recommandations priorisées
- Accompagnement implémentation
- Formation équipes DevSecOps