Sécurité Kubernetes : 15 points critiques avant la production

Publié le 13 novembre 2025 par Mathieu ROGER

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) ou ClusterRole (cluster-wide) spécifiques
  • Assigner des ServiceAccount dé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 describe ou 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)

  1. RBAC configuré (Point 1)
  2. Network Policies actives (Point 4)
  3. Pod Security Standards (Point 7)
  4. Image scanning (Point 9)

Ces 4 points couvrent 80% des risques critiques.

Should-have (P1)

  1. Secrets encryption at rest (Point 11)
  2. Audit logs (Point 14)
  3. Security Context (Point 8)
  4. TLS Ingress (Point 5)

Nice-to-have (P2)

  1. OIDC authentication (Point 2)
  2. Falco runtime security (Point 15)
  3. OPA/Gatekeeper policies (Point 15)
  4. 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 :

  1. Commencez par les must-have (P0)
  2. Ajoutez progressivement les autres points
  3. Automatisez les scans (CI/CD, scanning continu)
  4. 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