La conteneurisation a profondément transformé le développement et le déploiement logiciel, avec Docker comptant aujourd'hui plus de 13 millions d'utilisateurs actifs et Kubernetes devenu le standard de facto pour l'orchestration de conteneurs en production. Cette adoption massive s'accompagne d'une surface d'attaque considérablement élargie : images vulnérables provenant de registres publics non maîtrisés, mauvaises configurations de runtime, privilèges excessifs accordés aux conteneurs, chaînes d'approvisionnement logicielle compromises, et prolifération de secrets dans les layers d'images. Les incidents de sécurité liés aux conteneurs se multiplient : en 2023, l'attaque SolarWinds-like ciblant des images Docker Hub malveillantes a touché plus de 17 000 projets, et les ransomwares ciblant Kubernetes se sont multipliés avec des vecteurs d'entrée exploitant des APIs non authentifiées (CVE-2023-2728, CVE-2024-21626). Ce guide technique couvre l'intégralité du cycle de vie de la sécurité des conteneurs : du scanning d'images à la sécurité runtime, en passant par le hardening des configurations, la gestion de la supply chain et la conformité Kubernetes Pod Security Standards.
1. Anatomie d'une image Docker et surface d'attaque
Une image Docker est constituée de couches (layers) immuables empilées les unes sur les autres. Chaque instruction Dockerfile crée un nouveau layer. Cette architecture a des implications de sécurité directes : les secrets accidentellement inclus dans un layer intermédiaire restent accessibles même s'ils sont supprimés dans un layer ultérieur.
# Inspecter les layers d'une image avec dive
# https://github.com/wagoodman/dive
dive nginx:latest
# Alternative : docker history
docker history --no-trunc nginx:latest
# Extraire le contenu de chaque layer
docker save nginx:latest | tar xv -C /tmp/nginx_layers/
# Chercher des secrets dans les layers
grep -r "password\|secret\|api_key\|token\|credential" /tmp/nginx_layers/ 2>/dev/null
# Analyser les métadonnées de l'image
docker inspect nginx:latest | jq '.[0] | {
Os: .Os,
Architecture: .Architecture,
Created: .Created,
RootFS: .RootFS,
Config: {
User: .Config.User,
ExposedPorts: .Config.ExposedPorts,
Env: .Config.Env,
Entrypoint: .Config.Entrypoint,
Cmd: .Config.Cmd
}
}'
2. Trivy : scanner de vulnérabilités de référence
Trivy est le scanner de sécurité open-source le plus complet pour les conteneurs, développé par Aqua Security. Il analyse les vulnérabilités OS, les dépendances applicatives, les mauvaises configurations et les secrets.
# Installation Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.50.0
# Scan basique d'une image
trivy image nginx:latest
# Scan avec filtre sur la sévérité (seulement CRITICAL et HIGH)
trivy image --severity CRITICAL,HIGH nginx:latest
# Scan d'une image en sortie JSON pour intégration CI/CD
trivy image --format json --output trivy-results.json nginx:latest
# Scan avec politique de seuil (exit code 1 si CRITICAL trouvé)
trivy image --exit-code 1 --severity CRITICAL nginx:latest
# Scan d'un Dockerfile (analyse statique avant build)
trivy config ./Dockerfile
# Scan complet : image + config + secrets
trivy image \
--scanners vuln, config, secret \
--severity CRITICAL,HIGH,MEDIUM \
--format table \
nginx:latest
# Scan d'un registre privé
trivy image \
--username myuser \
--password mypassword \
registry.company.com/myapp:v1.2.3
# Scan des dépendances Python dans une image
trivy image \
--scanners vuln \
--vuln-type library \
python:3.11-slim
# Résultat type :
# Total: 45 (CRITICAL: 3, HIGH: 12, MEDIUM: 18, LOW: 12)
# ┌─────────────────┬────────────────┬──────────┬───────────────────┬────────────────────────┐
# │ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │
# ├─────────────────┼────────────────┼──────────┼───────────────────┼────────────────────────┤
# │ openssl │ CVE-2024-0727 │ CRITICAL │ 3.0.2-0ubuntu1.13 │ 3.0.2-0ubuntu1.15 │
# └─────────────────┴────────────────┴──────────┴───────────────────┴────────────────────────┘
3. Grype : alternative performante pour le scanning
Grype, développé par Anchore, offre une base de données de vulnérabilités différente de Trivy (Grype utilise principalement NVD + GitHub Advisory Database + plusieurs sources spécifiques aux écosystèmes). L'utilisation combinée des deux outils améliore la couverture.
# Installation Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Scan basique
grype nginx:latest
# Scan avec SBOM en entrée (workflow recommandé)
# 1. Générer un SBOM avec Syft
syft nginx:latest -o spdx-json > sbom.spdx.json
# 2. Scanner le SBOM avec Grype
grype sbom:./sbom.spdx.json
# Comparaison Trivy vs Grype
echo "=== Trivy ===" && trivy image --quiet --severity CRITICAL nginx:latest | tail -5
echo "=== Grype ===" && grype nginx:latest --only-fixed | tail -5
# Grype en mode CI avec seuil
grype nginx:latest --fail-on high
# Exit code 1 si vulnerabilité HIGH ou CRITICAL trouvée
4. Snyk Container : scanning SaaS avec remédiation
Snyk Container se distingue par sa capacité à suggérer des images de base alternatives avec moins de vulnérabilités et à prioriser les CVEs exploitables en contexte réel.
# Installation Snyk CLI
npm install -g snyk
# Authentification
snyk auth
# Scan d'image avec suggestions d'image de base alternative
snyk container test nginx:latest --file=Dockerfile
# Résultat avec suggestion :
# Base Image Vulnerabilities Severity
# nginx:latest 45 3 critical, 12 high
# nginx:1.25-alpine 8 0 critical, 2 high
# ✓ Recommended: nginx:1.25-alpine (37 fewer vulnerabilities)
# Monitoring continu (alertes lors de nouvelles CVEs)
snyk container monitor nginx:latest --project-name=prod-nginx
# Intégration avec Docker Scout (Docker natif depuis 2023)
docker scout cves nginx:latest
docker scout recommendations nginx:latest
# Scout compare avec les images recommandées
docker scout compare nginx:latest --to nginx:alpine
5. Docker Bench for Security : hardening de l'hôte
Docker Bench for Security est un script d'audit automatique qui vérifie des dizaines de bonnes pratiques de sécurité Docker selon les recommandations CIS Docker Benchmark.
# Exécution de Docker Bench
git clone https://github.com/docker/docker-bench-security.git
cd docker-bench-security
sudo bash docker-bench-security.sh
# Résultat type (extrait) :
# [INFO] 1 - Host Configuration
# [WARN] 1.1 - Ensure a separate partition for containers has been created
# [PASS] 1.2 - Ensure only trusted users are allowed to control Docker daemon
# [WARN] 1.3 - Ensure auditing is configured for the Docker daemon
# [INFO] 2 - Docker daemon configuration
# [WARN] 2.1 - Ensure network traffic is restricted between containers on default bridge
# [PASS] 2.2 - Ensure logging level is set to 'info'
# [WARN] 2.14 - Ensure Userland Proxy is disabled
# [INFO] 4 - Container Images and Build Files
# [WARN] 4.1 - Ensure a user for the container has been created (root user détecté)
# [WARN] 4.5 - Ensure Content trust for Docker is enabled
# Score: 34/93 checks passed
# Vérification manuelle des points critiques CIS
# CIS 2.1 — Désactiver le réseau inter-conteneurs par défaut
docker network ls
# Désactiver icc dans /etc/docker/daemon.json :
cat /etc/docker/daemon.json | python3 -m json.tool
# CIS 2.14 — Désactiver le userland proxy
cat > /etc/docker/daemon.json << 'EOF'
{
"icc": false,
"userland-proxy": false,
"no-new-privileges": true,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOF
6. Images distroless : réduire la surface d'attaque
Les images distroless, maintenues par Google, ne contiennent que le runtime de l'application et ses dépendances directes — sans shell, sans gestionnaire de paquets, sans binaires système. Cette approche réduit drastiquement la surface d'attaque.
# Dockerfile multi-stage avec image distroless finale
# Stage 1 : Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-w -s" -o /app/myapp ./cmd/server/
# Stage 2 : Runtime distroless (SANS shell, SANS outils système)
FROM gcr.io/distroless/static-debian12:nonroot
# Copier uniquement le binaire compilé
COPY --from=builder /app/myapp /app/myapp
# Utilisateur non-root (uid:gid 65532:65532 dans distroless/nonroot)
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/myapp"]
# Comparaison de taille et de surface d'attaque :
# ubuntu:22.04 — 77 MB, ~300+ packages, bash, curl, wget...
# debian:slim — 74 MB, shell disponible
# alpine:3.19 — 7 MB, shell ash, busybox
# distroless/static — 2 MB, AUCUN shell, AUCUN package manager
# scratch — 0 MB, juste le binaire (pour Go/Rust statiques)
7. Conteneurs rootless : isolation renforcée
Les conteneurs rootless permettent d'exécuter le daemon Docker ou Podman sans privilèges root sur l'hôte. En cas d'évasion du conteneur (container escape), l'attaquant obtient les droits d'un utilisateur non-privilégié sur l'hôte, et non root.
# Configuration de Docker en mode rootless
# Prérequis : uid_map et gid_map dans /proc/self/uid_map
dockerd-rootless-setuptool.sh install
# Vérification
docker info | grep "rootless"
# Security Options: rootless
# Podman rootless (alternative native rootless à Docker)
# Installation
sudo apt install -y podman
# Podman ne nécessite pas de daemon — rootless par défaut
podman run --rm nginx:latest
podman info | grep "rootless"
# Comparaison privilèges
# Docker classique : conteneur root = root hôte potentiel en cas d'escape
# Docker rootless : conteneur root = uid 100000 sur l'hôte (user namespace)
# Vérification user namespace mapping
cat /proc/$(pgrep dockerd)/uid_map
# 0 100000 65536
# uid 0 (root) dans le conteneur = uid 100000 sur l'hôte
# Test d'évasion de conteneur (educational — environnement de lab uniquement)
# CVE-2024-21626 (runc WORKDIR escape) — corrigé dans runc 1.1.12
# runc --version doit être >= 1.1.12
runc --version
8. Seccomp : filtrage des appels système
Seccomp (Secure Computing Mode) permet de restreindre les appels système (syscalls) disponibles pour un conteneur. Un conteneur standard Docker dispose de ~300 syscalls sur les ~400 disponibles dans le noyau Linux — seccomp permet de réduire ce nombre aux seuls syscalls nécessaires à l'application.
// Profil seccomp personnalisé pour un serveur web (nginx)
// /etc/docker/seccomp/nginx-profile.json
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_AARCH64"],
"syscalls": [
{
"names": [
"accept", "accept4", "access", "bind", "brk",
"capget", "capset", "chdir", "chmod", "chown",
"clock_gettime", "clone", "close", "connect",
"dup", "dup2", "epoll_create", "epoll_create1",
"epoll_ctl", "epoll_wait", "eventfd2",
"execve", "exit", "exit_group",
"fcntl", "fstat", "ftruncate", "futex",
"getcwd", "getdents64", "getegid", "geteuid",
"getgid", "getpid", "getppid", "getuid",
"ioctl", "listen", "lseek", "madvise",
"mmap", "mprotect", "munmap",
"nanosleep", "open", "openat",
"pipe", "pipe2", "poll", "prctl",
"read", "readlink", "recvfrom", "recvmsg",
"rt_sigaction", "rt_sigprocmask", "rt_sigreturn",
"select", "sendfile", "sendmsg", "sendto",
"setgid", "setgroups", "setuid",
"socket", "socketpair", "stat", "statfs",
"sysinfo", "umask", "uname",
"wait4", "write", "writev"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
# Appliquer le profil seccomp
docker run --security-opt seccomp=/etc/docker/seccomp/nginx-profile.json nginx:latest
# Identifier les syscalls nécessaires avec strace
strace -c -f nginx -g 2>&1 | head -30
# Outil automatique : seccomp-profile-generator
# https://github.com/containers/oci-seccomp-bpf-hook
# Lance le conteneur, capture tous les syscalls, génère un profil minimal
# Vérifier les syscalls bloqués dans un conteneur
docker run --security-opt seccomp=/etc/docker/seccomp/nginx-profile.json \
nginx:latest sh -c "hostname" # sh est bloqué dans un profil strict
# OCI runtime exec failed: exec failed: container_linux.go: operation not permitted
9. AppArmor : profils MAC pour conteneurs
AppArmor (Application Armor) est un module de sécurité Linux (LSM) qui implémente le Mandatory Access Control (MAC) via des profils définissant les ressources accessibles à chaque programme.
# Vérifier qu'AppArmor est actif
aa-status | head -10
# Profil AppArmor pour un conteneur Docker nginx
cat > /etc/apparmor.d/docker-nginx << 'EOF'
#include
profile docker-nginx flags=(attach_disconnected, mediate_deleted) {
#include
# Permettre les capacités réseau
network inet tcp,
network inet udp,
# Fichiers nginx — lecture seule
/etc/nginx/** r,
/usr/share/nginx/** r,
/var/log/nginx/** w,
# Répertoire racine web — lecture seule
/var/www/html/** r,
# PID et socket nginx
/run/nginx.pid rw,
/tmp/** rw,
# Bloquer explicitement les shells
deny /bin/sh x,
deny /bin/bash x,
deny /usr/bin/python* x,
# Bloquer l'accès aux fichiers système sensibles
deny /etc/shadow r,
deny /etc/passwd w,
deny /proc/sysrq-trigger w,
# Capacités autorisées
capability net_bind_service,
capability setuid,
capability setgid,
capability dac_override,
}
EOF
# Charger le profil
apparmor_parser -r /etc/apparmor.d/docker-nginx
# Appliquer au conteneur
docker run --security-opt apparmor=docker-nginx nginx:latest
# Vérifier l'application du profil
docker inspect | jq '.[0].HostConfig.SecurityOpt'
10. Falco : détection d'anomalies runtime
Falco est le standard de facto pour la détection d'anomalies runtime dans les environnements Kubernetes et Docker. Développé par Sysdig et maintenu par la CNCF, il utilise eBPF (ou des modules noyau) pour intercepter les appels système et les événements Kubernetes.
# Installation Falco avec Helm sur Kubernetes
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
helm install falco falcosecurity/falco \
--namespace falco \
--create-namespace \
--set driver.kind=ebpf \
--set falcosidekick.enabled=true \
--set falcosidekick.config.slack.webhookurl="https://hooks.slack.com/..."
# Règles Falco personnalisées pour ICS/production
# /etc/falco/rules.d/custom-rules.yaml
# /etc/falco/rules.d/custom-rules.yaml
# Règle 1 : Détecter un shell dans un conteneur (conteneurs distroless ne devraient jamais avoir de shell)
- rule: Shell spawned in container
desc: Un shell a été lancé dans un conteneur — possible intrusion
condition: >
spawned_process and container and
shell_procs and
not proc.name in (known_shell_procs)
output: >
Shell détecté dans conteneur (user=%user.name container=%container.name
image=%container.image.repository:%container.image.tag
shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline)
priority: CRITICAL
tags: [container, shell, mitre_execution]
# Règle 2 : Lecture de fichiers sensibles depuis un conteneur
- rule: Read sensitive file from container
desc: Lecture de fichiers système sensibles depuis un conteneur
condition: >
open_read and container and
(fd.name startswith /etc/shadow or
fd.name startswith /etc/passwd or
fd.name startswith /root/.ssh or
fd.name startswith /proc/1/environ)
output: >
Fichier sensible lu depuis conteneur (user=%user.name
container=%container.name file=%fd.name
image=%container.image.repository)
priority: HIGH
# Règle 3 : Connexion réseau inattendue depuis un conteneur
- rule: Unexpected outbound connection from container
desc: Connexion sortante vers un port/IP non autorisé
condition: >
outbound and container and
not fd.sip in (authorized_ips) and
not fd.sport in (authorized_ports)
output: >
Connexion réseau inattendue (container=%container.name
src=%fd.sip dst=%fd.cip:%fd.cport
image=%container.image.repository)
priority: WARNING
# Règle 4 : Montage de volume sensible
- rule: Sensitive host directory mounted in container
desc: Montage de /etc, /proc/sys, /var/run/docker.sock détecté
condition: >
container and
(container.mount.dest contains "/etc" or
container.mount.dest contains "/var/run/docker.sock" or
container.mount.dest = "/")
output: >
Montage dangereux détecté (container=%container.name
mount=%container.mount.dest:%container.mount.source)
priority: CRITICAL
11. OPA/Gatekeeper : politiques Kubernetes as Code
OPA Gatekeeper (Open Policy Agent) est un contrôleur d'admission Kubernetes qui évalue les politiques Rego avant de permettre la création ou la modification de ressources. Il constitue la couche de prévention, tandis que Falco est la couche de détection.
# Installation OPA Gatekeeper
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.14/deploy/gatekeeper.yaml
# Attendre que Gatekeeper soit prêt
kubectl wait --for=condition=Ready pods --all -n gatekeeper-system --timeout=120s
---
# ConstraintTemplate : interdire les conteneurs root
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8snorunasroot
spec:
crd:
spec:
names:
kind: K8sNoRunAsRoot
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8snorunasroot
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.securityContext.runAsNonRoot == true
not container.securityContext.runAsUser > 0
msg := sprintf("Le conteneur '%v' doit s'exécuter en tant qu'utilisateur non-root", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
container.securityContext.runAsUser == 0
msg := sprintf("Le conteneur '%v' ne peut pas s'exécuter en tant que root (uid=0)", [container.name])
}
---
# Constraint : appliquer la politique sur tous les namespaces sauf kube-system
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sNoRunAsRoot
metadata:
name: no-root-containers
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces: ["kube-system", "gatekeeper-system"]
enforcementAction: deny
---
# ConstraintTemplate : forcer les resource limits
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredresources
spec:
crd:
spec:
names:
kind: K8sRequiredResources
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredresources
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Conteneur '%v': memory limit obligatoire", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.cpu
msg := sprintf("Conteneur '%v': CPU limit obligatoire", [container.name])
}
12. Kubernetes Pod Security Standards
Les Pod Security Standards (PSS) ont remplacé Pod Security Policy (PSP, déprécié en 1.21, supprimé en 1.25) dans Kubernetes. Ils définissent trois niveaux de sécurité appliqués via l'admission controller Pod Security Admission.
| Niveau | Description | Usage typique | Restrictions clés |
|---|---|---|---|
| Privileged | Aucune restriction | Outils système (CNI, CSI) | Aucune |
| Baseline | Restrictions minimales | Applications génériques | Pas de privileged, pas de hostPath sensible |
| Restricted | Hardening maximal | Applications critiques | Non-root, no capabilities, seccomp RuntimeDefault |
# Appliquer le niveau Restricted sur un namespace
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=v1.28 \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/warn-version=v1.28 \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/audit-version=v1.28
# Tester si un Pod viole les PSS
kubectl --dry-run=server apply -f - << 'EOF'
apiVersion: v1
kind: Pod
metadata:
name: test-pod
namespace: production
spec:
containers:
- name: app
image: nginx:latest
securityContext:
runAsRoot: true # Violation du niveau Restricted
EOF
# Error: pods "test-pod" is forbidden: violates PodSecurity "restricted:v1.28"
# Pod Security Context complet — niveau Restricted compatible
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
namespace: production
spec:
# Pod-level security context
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
sysctls: []
containers:
- name: app
image: myapp:v1.0.0
# Container-level security context
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL # Supprimer TOUTES les capabilities Linux
# N'ajouter que si absolument nécessaire :
# add: ["NET_BIND_SERVICE"]
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "500m"
# Volumes temporaires en mémoire pour les fichiers temporaires
volumeMounts:
- name: tmp-dir
mountPath: /tmp
- name: cache-dir
mountPath: /app/cache
volumes:
- name: tmp-dir
emptyDir:
medium: Memory
sizeLimit: 50Mi
- name: cache-dir
emptyDir:
sizeLimit: 100Mi
# Pas de montage de hostPath, pas de hostNetwork, pas de hostPID
hostNetwork: false
hostPID: false
hostIPC: false
automountServiceAccountToken: false
13. Image signing avec Cosign et Notary
Cosign, développé par Sigstore/Linux Foundation, est devenu le standard pour la signature cryptographique d'images de conteneurs. Il s'intègre nativement avec les registres OCI et permet une vérification transparente via le journal de transparence Rekor.
# Installation Cosign
curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"
sudo install cosign-linux-amd64 /usr/local/bin/cosign
# Génération d'une paire de clés pour la signature
cosign generate-key-pair
# Génère cosign.key (privée, protégée par passphrase) et cosign.pub (publique)
# Signer une image après le push
docker push myregistry.com/myapp:v1.0.0
cosign sign --key cosign.key myregistry.com/myapp:v1.0.0@sha256:
# Vérifier la signature avant le déploiement
cosign verify --key cosign.pub myregistry.com/myapp:v1.0.0
# Résultat de vérification :
# Verification for myregistry.com/myapp:v1.0.0 --
# The following checks were performed on each of these signatures:
# - The cosign claims were validated
# - Existence of the claims in the transparency log was verified offline
# - The signatures were verified against the specified public key
# Signature keyless (via OIDC — GitHub Actions, GitLab CI)
# Dans GitHub Actions :
# - uses: sigstore/cosign-installer@v3
# - run: cosign sign --yes ${{ env.IMAGE }}
# env:
# COSIGN_EXPERIMENTAL: "1" # Signature keyless via OIDC GitHub
# Intégration avec Kyverno pour enforcement dans Kubernetes
cat > verify-image-policy.yaml << 'EOF'
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
rules:
- name: verify-myapp-signature
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences:
- "myregistry.com/myapp:*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----
EOF
kubectl apply -f verify-image-policy.yaml
14. SBOM : Software Bill of Materials pour les conteneurs
Un SBOM (Software Bill of Materials) est un inventaire exhaustif de tous les composants logiciels d'une image de conteneur, incluant leurs versions, licences et vulnérabilités connues. Il est devenu obligatoire pour les logiciels vendus au gouvernement américain (Executive Order 14028, mai 2021) et recommandé par l'ANSSI.
# Générer un SBOM avec Syft (par Anchore)
pip install syft # ou via package manager
# Format SPDX (standard ISO/IEC 5962:2021)
syft nginx:latest -o spdx-json > nginx-sbom.spdx.json
# Format CycloneDX (standard OWASP)
syft nginx:latest -o cyclonedx-json > nginx-sbom.cdx.json
# Format table lisible
syft nginx:latest -o table
# Résultat type :
# NAME VERSION TYPE
# adduser 3.134ubuntu3 deb
# apt 2.7.14build2 deb
# bash 5.2.21-2ubuntu4 deb
# libssl3 3.0.13-0ubuntu3.4 deb ← CVE cibles potentielles
# nginx 1.24.0-2ubuntu7 deb
# openssl 3.0.13-0ubuntu3.4 deb
# Attacher le SBOM à l'image (OCI artifact)
syft attest --output cyclonedx-json --key cosign.key myregistry.com/myapp:v1.0.0
# Vérifier et récupérer le SBOM attesté
cosign verify-attestation --key cosign.pub \
--type cyclonedx myregistry.com/myapp:v1.0.0
# Pipeline CI/CD complet (GitHub Actions)
cat > .github/workflows/container-security.yml << 'YAML'
name: Container Security Pipeline
on:
push:
branches: [main]
jobs:
build-scan-sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: sarif
exit-code: 1
severity: CRITICAL,HIGH
- name: Generate SBOM
run: syft myapp:${{ github.sha }} -o spdx-json > sbom.spdx.json
- name: Push to registry
run: |
docker tag myapp:${{ github.sha }} myregistry.com/myapp:${{ github.sha }}
docker push myregistry.com/myapp:${{ github.sha }}
- name: Sign image
uses: sigstore/cosign-installer@v3
with:
cosign-release: v2.2.2
- run: cosign sign --yes myregistry.com/myapp:${{ github.sha }}
YAML
15. Registres privés : sécurisation et contrôle d'accès
# Configuration Harbor (registre privé enterprise)
# Harbor = registre OCI avec scanning Trivy intégré, RBAC, audit logs
# Installation avec Helm
helm repo add harbor https://helm.goharbor.io
helm install harbor harbor/harbor \
--namespace harbor \
--create-namespace \
--set expose.type=ingress \
--set expose.tls.enabled=true \
--set trivy.enabled=true \
--set trivy.ignoreUnfixed=true \
--set notary.enabled=true
# Politique de scanning automatique Harbor :
# Paramètres > CVE allowlist : configurer les CVE ignorées
# Paramètres > Scan automatique des images push : Activé
# Paramètres > Bloquer les images avec vulnérabilités : CRITICAL
# Configurer la politique de prévention des pulls d'images non scannées
# Projects > myproject > Configuration > Prevent vulnerable images from running
# Severity threshold: High
# Authentification Docker avec token Harbor
docker login myregistry.harbor.company.com \
--username myuser \
--password $(cat /tmp/harbor_token)
# Configurer image pull secret Kubernetes pour Harbor
kubectl create secret docker-registry harbor-registry-secret \
--docker-server=myregistry.harbor.company.com \
--docker-username=robot\$myproject \
--docker-password= \
--namespace=production
16. Audit de la supply chain : détecter les images malveillantes
# Détecter les backdoors dans les images Docker Hub
# Analyser les layers avec whaler
go install github.com/P3GLEG/Whaler@latest
whaler nginx:latest
# Rechercher des miners de cryptomonnaie, reverse shells
trivy image --scanners vuln, malware nginx:latest 2>/dev/null
# Vérifier l'intégrité avec docker trust
docker trust inspect nginx:latest
# Vérifier les checksums des binaires dans l'image
docker run --rm --entrypoint sh nginx:latest -c \
"sha256sum /usr/sbin/nginx" | \
tee /tmp/nginx_binary_hash.txt
# Comparer avec le hash officiel Nginx
# https://nginx.org/en/download.html
# Outil : syft + grype pour audit supply chain complet
syft nginx:latest | grype -
# SLSA (Supply chain Levels for Software Artifacts) — vérification de provenance
# Vérifier qu'une image a été construite dans un environnement CI/CD traçable
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp "https://github.com/nginx/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
nginx:latest
nginx@sha256:abc123... au lieu de nginx:latest.
17. Kubernetes RBAC : principe du moindre privilège
# RBAC minimal pour un service applicatif
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp-sa
namespace: production
automountServiceAccountToken: false # Désactiver par défaut
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: myapp-role
namespace: production
rules:
# Seules les permissions strictement nécessaires
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["myapp-config"] # Restreindre à UN configmap spécifique
verbs: ["get", "watch"]
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["myapp-secrets"]
verbs: ["get"]
# NE PAS accorder :
# - verbs: ["list"] sur secrets (permet d'énumérer tous les secrets)
# - resources: ["pods"] avec verbs: ["exec"] (permet l'exécution de commandes)
# - resources: ["*"] (wildcard dangereux)
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: myapp-rolebinding
namespace: production
subjects:
- kind: ServiceAccount
name: myapp-sa
namespace: production
roleRef:
kind: Role
apiRef: myapp-role
apiGroup: rbac.authorization.k8s.io
# Audit des permissions Kubernetes avec kubectl-who-can
kubectl-who-can create pods --namespace production
kubectl-who-can exec pods --namespace production
# Outil rakkess : matrice de permissions RBAC
kubectl rakkess --namespace production
# Vérifier les droits du service account courant
kubectl auth can-i --list --namespace production
# Détecter les permissions dangereuses (RBAC misconfiguration)
# Service accounts avec accès aux secrets au niveau cluster
kubectl get clusterrolebindings -o json | jq '
.items[] |
select(.roleRef.name == "cluster-admin" or
.roleRef.name == "admin") |
{name: .metadata.name, subjects: .subjects}'
18. Network Policies Kubernetes : microsegmentation
# Network Policy : deny-all par défaut, puis autorisation sélective
---
# Bloquer TOUT le trafic entrant et sortant par défaut
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # S'applique à tous les pods
policyTypes:
- Ingress
- Egress
---
# Autoriser uniquement le trafic nécessaire pour myapp
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: myapp-network-policy
namespace: production
spec:
podSelector:
matchLabels:
app: myapp
policyTypes:
- Ingress
- Egress
ingress:
# Accepter uniquement depuis l'ingress controller
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 8080
egress:
# Autoriser uniquement vers la base de données
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
# Autoriser DNS (CoreDNS)
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
19. Monitoring et observabilité de la sécurité
# Stack de monitoring sécurité pour conteneurs
# Falco + Falcosidekick + Prometheus + Grafana
# Métriques Falco exposées pour Prometheus
# falco_events_total{priority="CRITICAL", rule="Shell spawned in container"}
# Dashboard Grafana pour la sécurité des conteneurs
# Importer le dashboard ID 11914 (Falco Security Events)
# Alertes critiques à configurer dans AlertManager :
cat > falco-alerts.yaml << 'EOF'
groups:
- name: container-security
rules:
- alert: CriticalFalcoEvent
expr: increase(falco_events_total{priority="CRITICAL"}[5m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "Événement Falco CRITICAL détecté"
description: "Règle: {{ $labels.rule }} | Container: {{ $labels.container_name }}"
- alert: ContainerPrivilegeEscalation
expr: increase(falco_events_total{rule=~".*privilege.*|.*root.*"}[5m]) > 0
for: 0m
labels:
severity: critical
EOF
20. Comparaison des outils de sécurité conteneurs
| Outil | Type | Licence | Intégration CI/CD | Runtime | Kubernetes |
|---|---|---|---|---|---|
| Trivy | Scanner image | Apache 2.0 | Excellente | Non | Oui (plugin) |
| Grype | Scanner image | Apache 2.0 | Bonne | Non | Non |
| Snyk Container | Scanner SaaS | Freemium | Excellente | Non | Oui |
| Falco | Détection runtime | Apache 2.0 | Non | Oui (eBPF) | Oui (natif) |
| OPA/Gatekeeper | Admission control | Apache 2.0 | Non | Non | Oui (natif) |
| Kyverno | Admission control | Apache 2.0 | Non | Non | Oui (natif) |
| Cosign | Image signing | Apache 2.0 | Excellente | Non | Via Kyverno |
| Docker Bench | Audit hôte | Apache 2.0 | Bonne | Non | Non |
| Aqua Security | Suite complète | Commerciale | Excellente | Oui | Oui |
| Sysdig Secure | Suite complète | Commerciale | Excellente | Oui | Oui |
FAQ — Questions fréquentes sur la sécurité des conteneurs
Quelle est la différence entre un scan de vulnérabilités statique et la sécurité runtime ?
Le scan statique analyse l'image avant exécution pour détecter les packages vulnérables, les mauvaises configurations et les secrets. La sécurité runtime surveille le comportement réel du conteneur en production via eBPF (Falco) pour détecter des activités anormales comme l'exécution d'un shell, des connexions réseau inattendues ou des accès à des fichiers sensibles. Ces deux approches sont complémentaires : le scan statique est la prévention, le runtime est la détection.
Les images distroless sont-elles vraiment impossibles à déboguer en production ?
Le débogage des conteneurs distroless requiert des approches alternatives. Kubernetes dispose de la fonctionnalité ephemeral containers (stable depuis 1.25) : kubectl debug -it pod/mypod --image=busybox --target=myapp injecte un conteneur de débogage temporaire dans le même namespace de processus, permettant l'inspection sans modifier l'image de production. kubectl exec reste possible sur un pod distroless si le processus principal accepte les signaux.
Comment gérer les secrets dans les conteneurs Kubernetes sans les exposer dans les variables d'environnement ?
Les variables d'environnement Kubernetes Secret sont visibles via kubectl exec — env et dans les logs d'événements. Les alternatives plus sécurisées sont : (1) HashiCorp Vault avec l'agent Vault sidecar qui injecte les secrets en fichiers montés, (2) AWS Secrets Manager / Azure Key Vault via le CSI Secrets Store Driver, (3) Sealed Secrets (Bitnami) pour chiffrer les secrets dans Git, et (4) External Secrets Operator pour synchroniser depuis des coffres-forts externes. Le montage en fichier (volume) est toujours préférable aux variables d'environnement pour les secrets sensibles.
Quelle est la différence entre Kyverno et OPA Gatekeeper pour l'admission control Kubernetes ?
OPA Gatekeeper utilise le langage Rego (puissant mais avec une courbe d'apprentissage élevée) et offre une grande flexibilité pour les politiques complexes. Kyverno est spécifiquement conçu pour Kubernetes et utilise des manifests YAML natifs (plus accessible), avec des fonctionnalités supplémentaires comme la génération automatique de ressources et la mutation de manifests. Pour les équipes débutant avec les politiques K8s, Kyverno est plus accessible. Pour des politiques très complexes avec logique conditionnelle avancée, OPA/Gatekeeper est plus puissant.
Comment sécuriser les images construites dans les pipelines CI/CD contre les attaques sur la chaîne d'approvisionnement ?
La protection de la supply chain repose sur : (1) épingler TOUTES les images de base par digest SHA256 dans les Dockerfiles, (2) vérifier les signatures des images de base avec Cosign avant le build, (3) générer et attester un SBOM pour chaque image produite, (4) signer les images finales avec des clés liées à l'identité OIDC du pipeline CI/CD (Sigstore keyless signing), (5) utiliser SLSA Build Level 3 (builds hermétiques, provenance vérifiable), et (6) scanner les dépendances applicatives (npm, pip, go modules) avec Dependabot ou Renovate.
Falco peut-il détecter une évasion de conteneur (container escape) en temps réel ?
Falco peut détecter les indicateurs d'une évasion de conteneur, notamment : l'écriture dans des répertoires hôte montés anormalement, des accès à /proc/1/ ou /proc/*/root depuis un conteneur, l'exécution de binaires hôte depuis un namespace de conteneur, et des changements dans les capabilities Linux. Cependant, une évasion réussie et silencieuse (sans syscalls inhabituels) peut passer inaperçue. Les règles Falco doivent être complétées par un monitoring des kernel audit logs et une surveillance réseau au niveau hôte.
Quels sont les prérequis techniques pour activer les conteneurs rootless en production ?
Les conteneurs rootless nécessitent : (1) noyau Linux >= 5.11 (pour eBPF rootless complet) avec CONFIG_CGROUPS_V2 activé, (2) newuidmap et newgidmap installés (package uidmap), (3) plages d'UIDs/GIDs configurées dans /etc/subuid et /etc/subgid, (4) net.ipv4.ip_unprivileged_port_start=0 si des ports < 1024 sont nécessaires. Les limitations incluent : certains plugins CNI non supportés, performances légèrement inférieures pour les I/O réseau, et incompatibilité avec certains drivers de stockage.
Comment implémenter le scanning de conteneurs dans une pipeline GitLab CI ?
GitLab intègre nativement Container Scanning (basé sur Trivy) depuis la version 15.0. Il suffit d'inclure le template dans le .gitlab-ci.yml : include: template: Security/Container-Scanning.gitlab-ci.yml et de définir CONTAINER_SCANNING_DISABLED: "false". Les résultats apparaissent dans l'onglet Security du pipeline et dans le Vulnerability Report du projet. Pour un contrôle plus fin, l'implémentation manuelle avec trivy image --format template --template @contrib/gitlab.tpl permet d'exporter en format DAST compatible GitLab.
Pour aller plus loin sur la sécurité Kubernetes, consultez notre article sur le hardening RBAC Kubernetes. La sécurité des conteneurs s'inscrit dans une démarche DevSecOps plus large couverte dans notre guide sur l'intégration de la sécurité dans les pipelines CI/CD. Pour la surveillance des menaces runtime, notre analyse de eBPF pour la sécurité Linux approfondit les mécanismes sous-jacents de Falco. La gestion des secrets en production est détaillée dans notre article sur HashiCorp Vault avec Kubernetes. Enfin, pour la conformité réglementaire des environnements conteneurisés, voir la conformité cloud ISO 27001.
Références externes : OWASP Docker Top 10 et NIST SP 800-190 Application Container Security Guide.
21. cgroups v2 et resource limits pour la sécurité
Les cgroups (Control Groups) version 2, devenus le standard depuis Linux 5.2 et adoptés par Docker et Kubernetes, permettent non seulement de limiter les ressources mais aussi de renforcer la sécurité des conteneurs en empêchant les attaques par déni de service interne et l'évasion par fork bomb.
# Vérifier que cgroups v2 est utilisé
mount | grep cgroup
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw, nosuid, nodev, noexec, relatime)
# Docker : activer cgroups v2 (Linux 5.2+)
cat /etc/docker/daemon.json | python3 -c "
import json, sys
d = json.load(sys.stdin)
d['default-cgroupns-mode'] = 'private' # Namespace cgroup isolé
d['no-new-privileges'] = True
print(json.dumps(d, indent=2))
"
# Limites de ressources par conteneur pour prévenir les DoS
docker run -d \
--name secure-app \
--memory="512m" \
--memory-swap="512m" \ # Désactiver le swap (=memory)
--memory-reservation="256m" \ # Soft limit
--cpus="0.5" \ # 0.5 CPU max
--cpu-shares=512 \ # Priorité relative
--pids-limit=100 \ # Max 100 processus (contre fork bombs)
--ulimit nofile=1024:2048 \ # Limite file descriptors
--read-only \ # Filesystem racine en lecture seule
--tmpfs /tmp:size=100m, mode=1777 \
nginx:latest
# Kubernetes : Resource Quotas par namespace
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: ResourceQuota
metadata:
name: production-quota
namespace: production
spec:
hard:
# Limites de compute
requests.cpu: "10"
limits.cpu: "20"
requests.memory: 20Gi
limits.memory: 40Gi
# Limites de pods et services
pods: "50"
services: "10"
services.loadbalancers: "2"
services.nodeports: "0" # Désactiver les NodePorts
# Limites de stockage
persistentvolumeclaims: "20"
requests.storage: 100Gi
# Limites de secrets et configmaps
secrets: "50"
configmaps: "50"
EOF
# LimitRange : limites par défaut si non spécifiées dans les pods
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: production
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: "256Mi"
defaultRequest:
cpu: "100m"
memory: "64Mi"
max:
cpu: "4"
memory: "4Gi"
min:
cpu: "50m"
memory: "32Mi"
EOF
22. Kubernetes Admission Controllers personnalisés
Les Admission Controllers Kubernetes s'intercalent dans le chemin d'authentification pour valider ou muter les ressources avant leur persistance dans etcd. Au-delà d'OPA Gatekeeper et Kyverno, il est possible de développer des webhooks d'admission personnalisés.
// Webhook d'admission personnalisé en Go
// Valide que toutes les images proviennent d'un registre approuvé
package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Registres approuvés
var approvedRegistries = []string{
"myregistry.harbor.company.com",
"gcr.io/distroless",
"registry.k8s.io",
}
func isApprovedImage(image string) bool {
for _, registry := range approvedRegistries {
if strings.HasPrefix(image, registry) {
return true
}
}
return false
}
func admitPod(pod corev1.Pod) *admissionv1.AdmissionResponse {
var violations []string
// Vérifier tous les conteneurs (init + main)
allContainers := append(pod.Spec.InitContainers, pod.Spec.Containers...)
for _, container := range allContainers {
if !isApprovedImage(container.Image) {
violations = append(violations, fmt.Sprintf(
"Conteneur '%s': image '%s' provient d'un registre non approuvé",
container.Name, container.Image))
}
// Vérifier que l'image est épinglée par digest (pas de tag mutable)
if !strings.Contains(container.Image, "@sha256:") {
violations = append(violations, fmt.Sprintf(
"Conteneur '%s': l'image doit être épinglée par digest SHA256 (pas de tag)",
container.Name))
}
}
if len(violations) > 0 {
return &admissionv1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Code: 403,
Message: strings.Join(violations, "; "),
},
}
}
return &admissionv1.AdmissionResponse{Allowed: true}
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
var admissionReview admissionv1.AdmissionReview
if err := json.NewDecoder(r.Body).Decode(&admissionReview); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var pod corev1.Pod
if err := json.Unmarshal(admissionReview.Request.Object.Raw, &pod); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
admissionReview.Response = admitPod(pod)
admissionReview.Response.UID = admissionReview.Request.UID
json.NewEncoder(w).Encode(admissionReview)
}
func main() {
http.HandleFunc("/validate-pod", webhookHandler)
// TLS obligatoire pour les webhooks Kubernetes
http.ListenAndServeTLS(":8443", "/certs/tls.crt", "/certs/tls.key", nil)
}
23. Analyse forensique d'un conteneur compromis
# Procédure forensique pour un conteneur suspect en production
# 1. Ne PAS supprimer le conteneur — conserver les preuves
# Identifier le conteneur suspect
docker ps -a | grep suspicious-pod
CONTAINER_ID="abc123def456"
# 2. Capturer l'état actuel
# Dump mémoire du conteneur
docker stats --no-stream $CONTAINER_ID
docker exec $CONTAINER_ID ps aux
docker exec $CONTAINER_ID ss -tulpn
docker exec $CONTAINER_ID cat /proc/net/tcp6
# 3. Inspecter les connexions réseau actives
docker exec $CONTAINER_ID netstat -tulpn 2>/dev/null || \
docker exec $CONTAINER_ID ss -tulpn
# 4. Capturer le trafic réseau du conteneur
CONTAINER_PID=$(docker inspect --format '{{.State.Pid}}' $CONTAINER_ID)
nsenter -t $CONTAINER_PID -n tcpdump -i any -w /tmp/container_traffic.pcap &
TCPDUMP_PID=$!
sleep 60
kill $TCPDUMP_PID
# 5. Exporter le système de fichiers pour analyse statique
docker export $CONTAINER_ID | gzip > /forensics/container_fs_$(date +%Y%m%d).tar.gz
sha256sum /forensics/container_fs_$(date +%Y%m%d).tar.gz > /forensics/hash.txt
# 6. Analyser les modifications du filesystem
# Comparer avec l'image originale
docker diff $CONTAINER_ID
# A = Ajouté, C = Modifié, D = Supprimé
# 7. Extraire les logs du conteneur
docker logs $CONTAINER_ID --since "24h" > /forensics/container_logs.txt
# 8. Analyser l'image avec Trivy pour les CVEs exploitées
IMAGE_ID=$(docker inspect $CONTAINER_ID --format '{{.Image}}')
trivy image --format json $IMAGE_ID > /forensics/image_vulns.json
# 9. Chercher des indicateurs de compromission dans le FS exporté
mkdir /tmp/container_analysis
tar xzf /forensics/container_fs_*.tar.gz -C /tmp/container_analysis/
# Chercher des webshells
find /tmp/container_analysis/ -name "*.php" -o -name "*.py" -o -name "*.sh" | \
xargs grep -l "exec\|system\|passthru\|shell_exec" 2>/dev/null
# Chercher des miners
find /tmp/container_analysis/tmp/ /tmp/container_analysis/var/ \
-type f -executable -newer /tmp/container_analysis/etc/passwd 2>/dev/null
# 10. Analyse YARA sur l'image exportée
yara -r /path/to/rules/ /tmp/container_analysis/
24. Sécurisation d'un cluster Kubernetes en production
# Audit de sécurité Kubernetes avec kube-bench (CIS Benchmark)
docker run --rm --pid=host --userns=host --net=host \
-v /etc:/etc:ro \
-v /var:/var:ro \
-v /usr/lib:/usr/lib:ro \
-v /usr/local/mount-from-host/bin:/usr/local/mount-from-host/bin:ro \
--env KUBECTL_VERSION=1.28 \
docker.io/aquasec/kube-bench:latest
# Résultat type :
# [PASS] 1.2.1 Ensure that the --anonymous-auth argument is set to false
# [FAIL] 1.2.5 Ensure that the --kubelet-certificate-authority argument is set
# [WARN] 1.3.1 Ensure that the --terminated-pod-gc-threshold argument is set
# Configuration sécurisée du kube-apiserver
# /etc/kubernetes/manifests/kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
namespace: kube-system
spec:
containers:
- name: kube-apiserver
image: registry.k8s.io/kube-apiserver:v1.29.0
command:
- kube-apiserver
# Authentification et autorisation
- --anonymous-auth=false
- --authorization-mode=Node,RBAC
- --enable-admission-plugins=NodeRestriction,PodSecurity,EventRateLimit
# Audit logging obligatoire
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-log-maxage=30
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
# TLS
- --tls-min-version=VersionTLS13
- --tls-cipher-suites=TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384
# Chiffrement des secrets etcd
- --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
# Limites de requêtes
- --max-requests-inflight=400
- --max-mutating-requests-inflight=200
# /etc/kubernetes/audit-policy.yaml — Politique d'audit Kubernetes
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Loguer toutes les opérations sur les secrets (niveau Metadata = pas de contenu)
- level: Metadata
resources:
- group: ""
resources: ["secrets", "configmaps"]
# Loguer les accès exec et attach (niveau Request = avec corps de requête)
- level: Request
verbs: ["create"]
resources:
- group: ""
resources: ["pods/exec", "pods/attach", "pods/portforward"]
# Loguer les créations de pods (peut contenir des images malveillantes)
- level: RequestResponse
verbs: ["create", "update", "patch"]
resources:
- group: ""
resources: ["pods"]
# Loguer les changements RBAC
- level: RequestResponse
resources:
- group: "rbac.authorization.k8s.io"
resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
# Tout le reste : niveau minimal
- level: None
users: ["system:kube-proxy"]
verbs: ["watch"]
resources:
- group: ""
resources: ["endpoints", "services", "services/status"]
- level: Metadata
omitStages: ["RequestReceived"]
25. Stratégie de mise à jour des images en production
#!/bin/bash
# Script de mise à jour sécurisée des images de conteneurs en production
# Intègre : scan Trivy, vérification de signature Cosign, déploiement progressif
IMAGE_NAME="$1"
NEW_TAG="$2"
NAMESPACE="${3:-production}"
DEPLOYMENT="${4:-myapp}"
if [ -z "$IMAGE_NAME" ] || [ -z "$NEW_TAG" ]; then
echo "Usage: $0 [namespace] [deployment]"
exit 1
fi
FULL_IMAGE="${IMAGE_NAME}:${NEW_TAG}"
echo "=== Mise à jour sécurisée: $FULL_IMAGE ==="
# Étape 1 : Scanner la nouvelle image
echo "[1/5] Scan Trivy..."
trivy image --exit-code 1 --severity CRITICAL "$FULL_IMAGE"
if [ $? -ne 0 ]; then
echo "[-] ARRÊT : Vulnérabilités CRITIQUES détectées dans $FULL_IMAGE"
exit 1
fi
echo "[OK] Aucune vulnérabilité CRITICAL"
# Étape 2 : Vérifier la signature Cosign
echo "[2/5] Vérification signature Cosign..."
cosign verify --key /etc/cosign/cosign.pub "$FULL_IMAGE" 2>/dev/null
if [ $? -ne 0 ]; then
echo "[-] ARRÊT : Signature Cosign invalide ou absente pour $FULL_IMAGE"
exit 1
fi
echo "[OK] Signature vérifiée"
# Étape 3 : Résoudre le digest exact
echo "[3/5] Résolution du digest SHA256..."
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "$FULL_IMAGE" 2>/dev/null | grep -o 'sha256:[a-f0-9]*')
if [ -z "$DIGEST" ]; then
DIGEST=$(crane digest "$FULL_IMAGE")
fi
PINNED_IMAGE="${IMAGE_NAME}@${DIGEST}"
echo "[OK] Image épinglée: $PINNED_IMAGE"
# Étape 4 : Déploiement progressif (canary 10% → 50% → 100%)
echo "[4/5] Déploiement progressif..."
for WEIGHT in 10 50 100; do
echo " Déploiement à $WEIGHT%..."
kubectl set image deployment/$DEPLOYMENT app="$PINNED_IMAGE" \
-n "$NAMESPACE"
# Attendre que le déploiement soit stable
kubectl rollout status deployment/$DEPLOYMENT -n "$NAMESPACE" --timeout=120s
if [ $? -ne 0 ]; then
echo "[-] Échec du déploiement à $WEIGHT% — Rollback automatique"
kubectl rollout undo deployment/$DEPLOYMENT -n "$NAMESPACE"
exit 1
fi
# Vérifier les métriques d'erreur (nécessite metrics-server)
ERROR_RATE=$(kubectl exec -n monitoring prometheus-pod -- \
curl -s "http://localhost:9090/api/v1/query?query=rate(http_requests_total{status='5xx'}[2m])" \
2>/dev/null | python3 -c "import sys, json; d=json.load(sys.stdin); print(d['data']['result'][0]['value'][1] if d['data']['result'] else '0')")
if [ "$(echo "$ERROR_RATE > 0.05" | bc -l)" = "1" ]; then
echo "[-] Taux d'erreur élevé ($ERROR_RATE) à $WEIGHT% — Rollback"
kubectl rollout undo deployment/$DEPLOYMENT -n "$NAMESPACE"
exit 1
fi
echo " [OK] Stable à $WEIGHT%"
[ $WEIGHT -lt 100 ] && sleep 30
done
echo "[5/5] Déploiement terminé avec succès !"
echo "[+] Image en production: $PINNED_IMAGE"
26. Comparaison des runtimes de conteneurs
| Runtime | Isolation | Performance | Sécurité | Support K8s | Use case |
|---|---|---|---|---|---|
| runc | Namespaces Linux | Excellente | Standard | Natif (OCI) | Production générale |
| gVisor (runsc) | Sandbox userspace | Bonne | Très haute | Via RuntimeClass | Code non fiable |
| Kata Containers | VM légère (KVM/QEMU) | Moyenne | Maximale | Via RuntimeClass | Multi-tenant strict |
| Firecracker | microVM | Très bonne | Maximale | Via Flintlock | FaaS, Lambda |
| Podman | Namespaces Linux | Excellente | Haute (rootless) | Via CRI-O | Workstations, CI/CD |
# Kubernetes RuntimeClass pour gVisor
# Prérequis : gVisor installé sur les nodes (runsc)
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
---
# Pod utilisant gVisor pour l'isolation renforcée
apiVersion: v1
kind: Pod
metadata:
name: sandboxed-app
spec:
runtimeClassName: gvisor # Isolation sandbox userspace
containers:
- name: app
image: myapp:latest@sha256:abc123...
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
27. Surveillance des dépendances et mise à jour automatique
# Renovate Bot — Mise à jour automatique des dépendances et images Docker
# .github/renovate.json
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", "security:openssf-scorecard"],
"vulnerabilityAlerts": {
"enabled": true,
"labels": ["security"],
"assignees": ["@security-team"]
},
"packageRules": [
{
"matchManagers": ["dockerfile"],
"matchUpdateTypes": ["major", "minor", "patch"],
"addLabels": ["container-update"],
"automerge": false,
"reviewers": ["@infrastructure-team"]
},
{
"matchManagers": ["dockerfile"],
"matchPackagePatterns": ["*"],
"pinDigests": true
},
{
"matchManagers": ["helm-values"],
"matchUpdateTypes": ["patch"],
"automerge": true,
"automergeType": "pr",
"requiredStatusChecks": ["trivy-scan", "cosign-verify"]
}
],
"customManagers": [
{
"customType": "regex",
"fileMatch": [".gitlab-ci.yml", ".github/workflows/*.yml"],
"matchStrings": [
"image: (?[^:@\\s]+):(?[^@\\s]+)(?:@(?sha256:[a-f0-9]+))?"
],
"datasourceTemplate": "docker"
}
]
}
28. Sécurité des images multi-architecture
Avec la généralisation des architectures ARM dans les environnements cloud (AWS Graviton, Apple Silicon pour le développement) et IoT, la sécurité des images multi-architecture est devenue une préoccupation concrète. Les images multi-architecture, distribuées via des Docker manifests lists, peuvent présenter des profils de vulnérabilités différents selon l'architecture cible, ce qui complique les workflows de scanning.
Un aspect souvent négligé est que le scanning d'une image multi-architecture se fait par défaut sur l'architecture de la machine qui effectue le scan. Si les pipelines CI/CD tournent sur des machines x86_64 mais que les images sont déployées sur des nodes ARM64 en production (Amazon Graviton, par exemple), les vulnérabilités spécifiques aux binaires ARM peuvent passer inaperçues. La solution consiste à forcer explicitement l'architecture lors du scanning avec Trivy via l'option --platform linux/arm64, ou à utiliser des runners CI/CD natifs ARM pour les builds et scans destinés aux nodes ARM.
Les images distroless elles-mêmes existent en variantes multi-architecture pour toutes les plateformes majeures (amd64, arm64, arm/v7, s390x, ppc64le), ce qui facilite l'adoption sans compromettre la couverture architecturale. Google maintient des SBOMs distincts pour chaque architecture dans le cadre de son programme de transparence des artefacts logiciels (SLSA Level 3 pour les images distroless officielles).
La construction d'images multi-architecture sécurisées avec Docker Buildx nécessite une stratégie de build cohérente. Le cross-compilation est préférable à l'émulation QEMU pour les performances, mais requiert que les dépendances soient disponibles pour l'architecture cible. Dans un contexte de sécurité de la supply chain, il est important de s'assurer que les images de base utilisées pour chaque architecture proviennent du même éditeur de confiance et sont épinglées par digest spécifique à l'architecture (chaque architecture a son propre digest dans un manifest list).
La gestion des secrets spécifiques aux architectures est un autre point d'attention. Certaines implémentations cryptographiques sont optimisées par architecture (AES-NI sur x86, AES extensions sur ARM). Les conteneurs doivent utiliser les bibliothèques adaptées à leur architecture cible pour bénéficier des accélérations matérielles, notamment pour les charges de travail cryptographiques intensives comme les terminaisons TLS ou les opérations de hachage en masse.
29. Service Mesh et sécurité mTLS entre microservices
Dans les architectures microservices Kubernetes, les communications entre services constituent une surface d'attaque significative souvent sous-estimée. Le chiffrement et l'authentification mutuels entre services via mTLS (mutual TLS) est la solution recommandée, et les service meshes comme Istio ou Linkerd automatisent ce processus de manière transparente pour les applications.
Un service mesh implémente mTLS en injectant un proxy sidecar (Envoy pour Istio, linkerd-proxy pour Linkerd) dans chaque pod. Ces proxies interceptent tout le trafic réseau entrant et sortant du pod et établissent des connexions TLS mutuellement authentifiées avec les autres sidecars, en utilisant des certificats de courte durée émis automatiquement par un Certificate Authority interne au mesh. Cette approche présente l'avantage de ne nécessiter aucune modification du code applicatif et de fournir une authentification service-to-service basée sur les identités SPIFFE (Secure Production Identity Framework For Everyone) plutôt que sur des adresses IP potentiellement usurpées.
La configuration de politiques d'autorisation dans Istio permet de définir précisément quels services peuvent communiquer avec quels autres services, et avec quelles méthodes HTTP et chemins. Cette granularité va bien au-delà des NetworkPolicies Kubernetes qui opèrent au niveau IP/port, en ajoutant une couche d'autorisation au niveau L7. Par exemple, une politique Istio peut autoriser le service payment-service à appeler uniquement le chemin POST /api/payments du service bank-connector, bloquant tout autre appel entre ces deux services même s'ils sont dans le même namespace.
L'observabilité fournie par les service meshes est également précieuse pour la sécurité. Istio génère automatiquement des métriques (Prometheus), des logs d'accès et des traces distribuées (Jaeger) pour toutes les communications inter-services, créant un journal d'audit exhaustif de toutes les interactions entre microservices. Cette visibilité est fondamentale pour la détection d'anomalies comportementales (un service qui commence soudainement à appeler des services qu'il n'appelait pas habituellement peut indiquer une compromission) et pour la réponse aux incidents (reconstituer le chemin d'une attaque de lateral movement à travers les microservices).
30. Sécurité des registres de conteneurs en entreprise
La sécurisation du registre de conteneurs est un maillon souvent négligé de la chaîne de sécurité. Un registre compromis peut permettre à un attaquant d'injecter du code malveillant dans toutes les images distribuées aux équipes de développement et aux environnements de production. Les bonnes pratiques de sécurité pour les registres d'entreprise couvrent plusieurs dimensions.
Le contrôle d'accès au registre doit suivre le principe du moindre privilège. Les pipelines CI/CD n'ont besoin que de droits de push (écriture) sur des espaces de noms spécifiques correspondant aux projets qu'ils gèrent. Les développeurs individuels ne devraient avoir que des droits de lecture en production. Seuls les comptes de service dédiés aux pipelines de déploiement (ArgoCD, Flux) ont besoin de droits de pull. Les comptes administrateurs doivent être protégés par MFA et leur utilisation auditée.
La politique de rétention des images est une considération à la fois de sécurité et de gouvernance. Les images vulnérables ou obsolètes stockées dans le registre représentent un risque si elles sont accidentellement redéployées. Une politique automatisée doit supprimer les images non taguées après un délai court (7 jours), archiver ou supprimer les versions plus anciennes que la politique de rétention (par exemple, garder les 5 dernières versions de chaque tag stable), et bloquer les pulls d'images ayant des vulnérabilités CRITICAL non résolues depuis plus de 30 jours.
La séparation des registres par environnement est une autre bonne pratique. Les images en développement (non validées) ne doivent pas être accessibles depuis les environnements de production. Un pipeline de promotion formalisé (développement → staging → production) avec un registre distinct pour chaque environnement, une signature Cosign requise pour passer du staging à la production, et une validation manuelle optionnelle pour les changements majeurs, garantit qu'aucune image non validée n'atteint la production.
La surveillance continue des images en production via des scanners comme Trivy Operator (qui scanne les images déployées dans Kubernetes et crée des ressources VulnerabilityReport) ou les solutions commerciales comme Aqua Security ou Sysdig permet de détecter de nouvelles CVEs dans des images déjà déployées, sans attendre le prochain build. Quand une CVE critique est publiée et affecte une image en production, le système d'alerte doit notifier l'équipe responsable en quelques heures pour permettre une remédiation rapide.
31. Sécurité des Helm Charts et des packages Kubernetes
Les Helm Charts sont devenus le standard de packaging des applications Kubernetes, mais leur sécurité est souvent négligée. Un Chart mal configuré peut déployer des workloads avec des privilèges excessifs, des images non signées, ou des configurations réseau permissives qui annulent tous les efforts de hardening réalisés au niveau des pods individuels.
La sécurisation des Helm Charts commence par l'audit des valeurs par défaut (values.yaml). Les Charts publics (Helm Hub, Artifact Hub) sont créés pour la facilité d'utilisation plutôt que pour la sécurité maximale, ce qui se traduit par des valeurs par défaut permissives : securityContext absent, hostNetwork: false non enforced, resources sans limites. Avant de déployer un Chart externe en production, un audit systématique de ses valeurs et templates est indispensable.
L'outil helm-audit et le plugin helm-checkov permettent d'analyser automatiquement les Charts pour détecter les mauvaises configurations. Trivy supporte également le scanning des Charts via trivy config ./mychart/. Pour les organisations utilisant Helm comme standard de déploiement, la mise en place d'une bibliothèque de Charts internes basés sur des standards de sécurité définis (securityContext obligatoire, resource limits par défaut, networkPolicy générée automatiquement) est une approche plus efficace que l'audit au cas par cas.
La signature des Charts avec le mécanisme de provenance Helm (helm package --sign avec une clé GPG) permet de vérifier l'intégrité et la provenance d'un Chart avant son déploiement. Cette fonctionnalité, disponible depuis Helm 2 mais peu utilisée en pratique, prend de l'importance dans le contexte de la supply chain sécurité où la compromission d'un repository Helm pourrait permettre l'injection de Charts malveillants.
La gestion des secrets dans les Helm Charts est un autre point critique. La pratique de stocker des secrets en clair dans les values.yaml ou les templates Helm est malheureusement courante. Les alternatives sécurisées incluent : l'utilisation du plugin helm-secrets (qui intègre SOPS pour le chiffrement des fichiers de valeurs sensibles dans Git), l'injection des secrets via External Secrets Operator depuis un vault externe au moment du déploiement, ou la référence à des Kubernetes Secrets existants (créés par un pipeline séparé) plutôt que leur création directe dans les templates Helm.
32. Conteneurs et conformité PCI-DSS, HIPAA, SOC 2
Les environnements conteneurisés soumis à des obligations réglementaires spécifiques (PCI-DSS pour les traitements de paiement, HIPAA pour les données de santé américaines, SOC 2 pour les prestataires de services cloud) nécessitent des contrôles supplémentaires au-delà du hardening technique général. La mise en conformité dans ces contextes demande une compréhension fine des exigences réglementaires et de leur traduction dans les environnements Kubernetes.
PCI-DSS v4.0 (applicable depuis mars 2024) impose, pour les environnements de traitement des données de paiement conteneurisés : l'isolation réseau stricte entre les pods du CDE (Cardholder Data Environment) et les autres pods via des NetworkPolicies (satisfait aux exigences de segmentation 1.3.x), le logging et la surveillance de toutes les activités dans le CDE avec rétention de 12 mois (Falco + Wazuh couvrent les exigences 10.x), la gestion des patches dans les 30 jours pour les vulnérabilités critiques (workflow Trivy + Renovate), et la gestion des identités et des accès avec MFA pour tous les accès aux systèmes CDE (satisfait par l'RBAC Kubernetes + Entra ID/OIDC avec MFA).
HIPAA (Health Insurance Portability and Accountability Act) pour les données de santé américaines exige des contrôles d'accès (AC), d'audit (AU) et d'intégrité (SI) qui se traduisent concrètement dans les environnements Kubernetes par : le chiffrement au repos des Persistent Volumes contenant des PHI (Protected Health Information) via les fonctionnalités de chiffrement des PVC AWS EBS/GCP PD, le chiffrement en transit via mTLS (Istio/Linkerd), la gestion des clés de chiffrement via un KMS external (AWS KMS, HashiCorp Vault), et la piste d'audit complète de tous les accès aux données PHI (Kubernetes Audit Logs + Falco).
SOC 2 Type II, la certification de sécurité la plus demandée pour les prestataires SaaS, évalue la conception ET l'efficacité opérationnelle des contrôles sur une période de 6 à 12 mois. Pour les environnements conteneurisés, les contrôles SOC 2 les plus souvent évalués sont : la gestion du changement (git flow, code review, déploiement automatisé via CI/CD avec approbations), la surveillance continue (dashboards Grafana + alertes PagerDuty pour les métriques de sécurité), la gestion des accès (revue trimestrielle des accès Kubernetes RBAC), et la réponse aux incidents (runbooks documentés, exercices de tabletop annuels). La collecte de preuves pour SOC 2 est facilitée par les logs immuables des pipelines CI/CD et des systèmes d'audit Kubernetes.
33. Benchmarking de performance : impact des contrôles de sécurité
L'application de contrôles de sécurité sur les conteneurs a un impact mesurable sur les performances, qu'il est important de quantifier pour dimensionner correctement les ressources et arbitrer les choix de sécurité en connaissance de cause. L'impact est souvent surestimé par les équipes de développement et sous-estimé par les équipes de sécurité, d'où l'importance de mesures objectives.
Le profil seccomp avec l'option RuntimeDefault (le profil par défaut de Docker/Kubernetes) introduit une latence inférieure à 2% sur la plupart des charges de travail applicatives selon les benchmarks Aqua Security 2024, car il ne filtre que les syscalls rarement utilisés. Un profil seccomp personnalisé très restrictif peut réduire cette latence à zéro ou même légèrement l'améliorer en réduisant les vérifications kernel. L'activation d'AppArmor avec un profil adapté au workload introduit une latence similaire, inférieure à 3% selon les tests Ubuntu 22.04.
Les conteneurs rootless avec Docker présentent une légère pénalité de performance (5-10%) pour les opérations d'I/O réseau intensives en raison de la couche supplémentaire de traduction des espaces de noms d'utilisateurs. Pour les applications web standard, cet impact est négligeable. Pour les applications à très haute performance réseau (streaming, base de données), l'évaluation cas par cas est recommandée. Podman rootless présente généralement de meilleures performances que Docker rootless pour les opérations réseau grâce à son implémentation différente des user namespaces.
Les runtimes d'isolation renforcée ont un impact plus significatif. gVisor (runsc) introduit une pénalité de 10-30% sur les opérations I/O système en raison de son interception de tous les syscalls dans son sandbox userspace. Kata Containers avec une microVM QEMU présente une pénalité similaire (10-25%) avec un surcoût de démarrage de 0.5-1 seconde par pod. Ces pénalités sont acceptables pour les workloads traitant du code non fiable (exécution de code utilisateur, microservices exposés directement à l'internet) mais trop importantes pour les workloads latency-sensitive. Le choix du runtime doit donc être guidé par le profil de risque de chaque type de workload.
Conclusion et recommandations
La maîtrise de ces techniques et outils est indispensable pour tout professionnel de la cybersécurité en 2026. L'évolution constante des menaces exige une veille permanente et une mise à jour régulière des compétences. Pour aller plus loin, consultez nos articles techniques ou contactez notre équipe pour un accompagnement sur mesure adapté à votre contexte.
À retenir : La sécurité est un processus continu, pas un état. Chaque audit, chaque test et chaque analyse contribue à renforcer la posture de défense de l'organisation face aux menaces actuelles et futures.