## Introduction

Utiliser Docker en production ne se limite pas à construire et expédier des images. La sécurité est une responsabilité opérationnelle continue qui englobe la configuration, la surveillance, la maintenance, les sauvegardes et les mises à niveau. Sans processus reproductible, les équipes s'exposent à des risques d'évasion de conteneur, d'élévation de privilèges, de fuite de secrets et d'attaques de la chaîne d'approvisionnement.

Cet article fournit une liste de contrôle opérationnelle pratique pour la sécurité Docker en production. Chaque section inclut des commandes concrètes, des extraits de configuration, des sorties attendues et des étapes de vérification. L'objectif est de donner aux développeurs, consultants DevOps et équipes techniques de startups une référence adaptable à leurs propres environnements.

Nous couvrons :

- L'inventaire des versions Docker et les prérequis d'environnement
- L'application d'un chemin de configuration sûr avec un durcissement ciblé
- La vérification de la posture de sécurité avec des contrôles observables
- La gestion des modes de défaillance et les procédures de récupération
- Une liste de contrôle opérationnelle reproductible pour une sécurité continue

À la fin, vous disposerez d'un ensemble clair d'actions pour durcir les hôtes Docker, appliquer des politiques d'exécution, détecter les anomalies et récupérer après incident.

## Inventaire des versions et de l'environnement

Avant toute modification de sécurité, documentez l'environnement Docker actuel. Cela inclut les versions du moteur Docker et de la CLI, le runtime de conteneur, le système d'exploitation hôte, les pilotes de stockage et de réseau, ainsi que les paramètres actuels liés à la sécurité.

### Vérifier les versions Docker

Exécutez la commande suivante sur chaque hôte Docker :

```bash
docker version
```

Exemple de sortie (tronquée) :

```
Client: Docker Engine - Community
  Version:           24.0.5
  API version:       1.43
  Go version:        go1.20.4
  Git commit:        ced0996
  Built:             Fri Jul 21 19:01:22 2023
  OS/Arch:           linux/amd64
  Context:           default

Server: Docker Engine - Community
  Engine:
    Version:          24.0.5
    API version:      1.43 (minimum version 1.12)
    Go version:       go1.20.4
    Git commit:       a61e2b4
    Built:            Fri Jul 21 19:01:22 2023
    OS/Arch:          linux/amd64
    Experimental:     false
  containerd:
    Version:          1.6.21
    GitCommit:        3dce8eb055cbb6878b1e53c1b34f8f1b3e3f0c1a
  runc:
    Version:          1.1.7
    GitCommit:        v1.1.7-0-g860f061
  docker-init:
    Version:          0.19.0
    GitCommit:        de40ad0
```

Notez la version du moteur et les composants (containerd, runc). Les correctifs de sécurité ciblent souvent des versions spécifiques, donc maintenir cet inventaire à jour est essentiel.

### Inspecter la configuration de sécurité actuelle

Utilisez `docker info` pour voir les paramètres de sécurité généraux :

```bash
docker info --format '{{json .SecurityOptions}}'
```

Exemple de sortie :

```json
["name=seccomp,profile=builtin","name=cgroupns"]
```

Si vous voyez `name=userns` dans la sortie, le remappage des espaces de noms utilisateur est activé. Sinon, il peut être candidat à un durcissement (discuté plus loin).

Vérifiez également le profil seccomp par défaut et le statut d'AppArmor/SELinux :

```bash
docker info | grep -E 'Security Options|Kernel Version|Operating System'
```

### Identifier les conteneurs privilégiés

Listez les conteneurs en cours d'exécution et vérifiez le mode privilégié :

```bash
docker ps --quiet | xargs docker inspect --format '{{.Name}}: Privileged={{.HostConfig.Privileged}}'
```

Exemple de sortie :

```
/mysql: Privileged=false
/web: Privileged=true
/cache: Privileged=false
```

Le conteneur `/web` s'exécute en mode privilégié, ce qui est risqué. Vous traiterez cela dans le chemin de configuration sûr.

### Prérequis

Assurez-vous d'avoir :

- Accès SSH aux hôtes Docker avec sudo (ou équivalent)
- Un environnement de test ou de préproduction pour valider les changements avant la production
- Sauvegarde de la configuration du démon Docker (`/etc/docker/daemon.json`)
- Sauvegarde des fichiers d'unité systemd ou scripts d'init pour Docker
- Capacité à redémarrer le démon Docker (nécessite une fenêtre de maintenance planifiée)
- Outils d'agrégation de journaux et de surveillance (par exemple, ELK, Prometheus, Datadog)

Documentez l'inventaire dans un tableur ou une base de données de gestion de configuration. Cette base sera utilisée pour la vérification ultérieure.

## Chemin de configuration sûr

Appliquez le durcissement de sécurité de manière contrôlée et incrémentale. Commencez par les changements à plus faible risque et plus fort impact : configuration du démon, paramètres d'exécution des conteneurs et politiques réseau. Testez toujours sur un hôte hors production d'abord.

### 1. Durcir le démon Docker

Le démon Docker s'exécute avec les privilèges root et contrôle tous les conteneurs. Un démon mal configuré peut exposer l'hôte. Modifiez `/etc/docker/daemon.json` (créez-le s'il n'existe pas) et ajoutez ou modifiez ce qui suit :

```json
{
  "icc": false,
  "log-level": "info",
  "userland-proxy": false,
  "no-new-privileges": true,
  "live-restore": true,
  "iptables": true,
  "ip6tables": true,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 64000,
      "Soft": 64000
    }
  }
}
```

Paramètres clés :

- `"icc": false` : Désactive la communication inter-conteneurs par défaut. Les conteneurs doivent être explicitement liés ou sur le même réseau défini par l'utilisateur pour communiquer.
- `"no-new-privileges": true` : Empêche les conteneurs d'obtenir des privilèges supplémentaires via les binaires setuid/setgid ou d'autres mécanismes.
- `"live-restore": true` : Maintient les conteneurs en cours d'exécution lorsque le démon Docker est arrêté, réduisant les temps d'arrêt lors des mises à niveau.

Après modification, redémarrez le démon Docker :

```bash
sudo systemctl restart docker
```

Vérifiez les paramètres :

```bash
docker info --format '{{json .SecurityOptions}}'
```

La sortie attendue devrait inclure `no-new-privileges`. En fait, `no-new-privileges` est un drapeau par conteneur ; le paramètre du démon n'y est pas affiché. Vérifiez plutôt `docker info` pour les paramètres au niveau du démon :

```bash
sudo docker info | grep -i 'Default Runtime\|Cgroup Driver\|Security Options'
```

Mais pour vérifier `icc: false`, exécutez :

```bash
docker network inspect bridge | grep -i 'com.docker.network.bridge.enable_icc'
```

Sortie attendue :

```
"com.docker.network.bridge.enable_icc": "false"
```

### 2. Activer le remappage des espaces de noms utilisateur

Les espaces de noms utilisateur mappent l'utilisateur root du conteneur vers un utilisateur non root sur l'hôte, limitant l'impact des évasions de conteneur. C'est un contrôle de sécurité puissant mais peut casser certaines charges de travail (par exemple, celles nécessitant des permissions de fichiers spécifiques). Testez minutieusement.

Pour l'activer, ajoutez à `/etc/docker/daemon.json` :

```json
{
  "userns-remap": "default"
}
```

Puis redémarrez Docker :

```bash
sudo systemctl restart docker
```

Vérifiez que l'utilisateur remappé existe :

```bash
id dockremap
```

Exemple de sortie :

```
uid=140000(dockremap) gid=140000(dockremap) groups=140000(dockremap)
```

Remarque : Si vous avez des conteneurs ou volumes existants, ils peuvent avoir des problèmes de permission après activation. Prévoyez de recréer les conteneurs et d'ajuster la propriété des volumes. Référez-vous à la documentation Docker pour les étapes de migration.

### 3. Restreindre les capacités des conteneurs

Par défaut, les conteneurs Docker reçoivent un sous-ensemble de capacités Linux. Vous pouvez réduire davantage les privilèges en supprimant toutes les capacités et en n'ajoutant que celles nécessaires. Utilisez les drapeaux `--cap-drop` et `--cap-add`.

Exemple : Exécuter un conteneur Nginx avec des capacités minimales :

```bash
docker run -d --name web \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  nginx:alpine
```

Ici, nous supprimons toutes les capacités, puis ajoutons uniquement `NET_BIND_SERVICE` (pour se lier au port 80). Nous définissons également `no-new-privileges` pour empêcher l'élévation de privilèges.

Vérifiez les capacités d'un conteneur en cours d'exécution :

```bash
docker inspect --format '{{.HostConfig.CapAdd}} {{.HostConfig.CapDrop}}' web
```

Sortie attendue :

```
[NET_BIND_SERVICE] [ALL]
```

### 4. Appliquer des limites de ressources

Une utilisation illimitée des ressources peut conduire à un déni de service. Définissez des limites de mémoire et de CPU sur les conteneurs :

```bash
docker run -d --name api \
  --memory 512m \
  --cpus 1.5 \
  --pids-limit 100 \
  myapp:1.0
```

Ces drapeaux garantissent que le conteneur utilise au maximum 512 Mo de RAM, 1,5 cœur CPU et 100 processus/threads. `--pids-limit` empêche les bombes fork.

Vérifiez les limites :

```bash
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.NanoCpus}} {{.HostConfig.PidsLimit}}' api
```

Sortie attendue :

```
536870912 1500000000 100
```

### 5. Utiliser un système de fichiers racine en lecture seule

De nombreuses applications peuvent fonctionner avec un système de fichiers racine en lecture seule, empêchant les attaquants d'écrire des fichiers malveillants. Utilisez `--read-only` et montez des volumes inscriptibles pour des chemins spécifiques (par exemple, `/tmp`, `/var/run`).

```bash
docker run -d --name web \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /var/run \
  nginx:alpine
```

Vérifiez :

```bash
docker inspect --format '{{.HostConfig.ReadonlyRootfs}}' web
```

Sortie attendue :

```
true
```

### 6. Sécuriser la socket Docker

La socket Docker (`/var/run/docker.sock`) accorde un contrôle total sur le démon. Ne l'exposez jamais aux conteneurs sauf nécessité absolue. Si un conteneur doit gérer Docker (par exemple, Portainer), utilisez un proxy comme `docker-socket-proxy` avec des permissions restreintes.

Vérifiez si un conteneur monte la socket :

```bash
docker ps --quiet | xargs docker inspect --format '{{.Name}}: {{range .Mounts}}{{if eq .Source "/var/run/docker.sock"}}YES{{end}}{{end}}'
```

Si un conteneur affiche `YES`, évaluez s'il a réellement besoin de la socket et restreignez avec un proxy.

### 7. Segmentation du réseau

Utilisez des réseaux bridge définis par l'utilisateur pour isoler les conteneurs. Évitez d'utiliser le réseau bridge par défaut en production.

Créez un réseau :

```bash
docker network create --driver bridge frontend
```

Exécutez des conteneurs attachés à ce réseau :

```bash
docker run -d --name web --network frontend nginx:alpine
```

La communication entre conteneurs sur différents réseaux est bloquée sauf autorisation explicite.

### 8. Configurer la journalisation et la surveillance

Assurez-vous que les journaux sont capturés et rotés. La configuration du démon ci-dessus définit la journalisation en fichier JSON avec rotation. Pour la production, envisagez d'envoyer les journaux vers un système central en utilisant un pilote de journalisation comme `fluentd` ou `splunk`.

Exemple d'ajout à `daemon.json` :

```json
{
  "log-driver": "fluentd",
  "log-opts": {
    "fluentd-address": "localhost:24224",
    "tag": "docker.{{.Name}}"
  }
}
```

Puis redémarrez Docker. Testez en exécutant un conteneur et en vérifiant la destination des journaux.

## Vérification et diagnostic

Après avoir appliqué les changements, vérifiez que les contrôles de sécurité sont efficaces. Utilisez des outils automatisés et des vérifications manuelles.

### 1. Exécuter Docker Bench for Security

Docker Bench for Security est un script qui vérifie les meilleures pratiques courantes. Exécutez-le sur l'hôte :

```bash
git clone https://github.com/docker/docker-bench-security.git
cd docker-bench-security
sudo ./docker-bench-security.sh
```

La sortie est un long rapport avec des éléments `[WARN]`, `[INFO]` et `[PASS]`. Exemple d'extrait :

```
[INFO] 1 - Host Configuration
[WARN] 1.1 - Ensure a separate partition for containers has been created
[PASS] 1.2 - Ensure the container host has been Hardened
[PASS] 1.3 - Ensure Docker is up to date
[INFO] 2 - Docker daemon configuration
[WARN] 2.1 - Ensure network traffic is restricted between containers on the default bridge
[PASS] 2.2 - Ensure the logging level is set to 'info'
...
```

Traitez les avertissements selon votre environnement. Notez que certains avertissements peuvent être acceptables en raison de contraintes opérationnelles ; documentez les exceptions.

### 2. Scanner les images pour les vulnérabilités

Utilisez des outils de scan d'images comme Trivy, Clair ou Docker Scout. Exemple avec Trivy :

```bash
trivy image nginx:alpine
```

Exemple de sortie (tronquée) :

```
nginx:alpine (alpine 3.18.3)
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
```

Si des vulnérabilités sont trouvées, mettez à jour l'image de base ou appliquez des correctifs. Intégrez le scan dans votre pipeline CI/CD pour détecter les problèmes tôt.

### 3. Vérifier les profils Seccomp et AppArmor

Vérifiez que les conteneurs utilisent le profil seccomp par défaut (ou un profil personnalisé).

```bash
docker inspect --format '{{.HostConfig.SecurityOpt}}' web
```

La sortie attendue inclut `seccomp=...` et éventuellement `apparmor=...` si AppArmor est activé.

Si vous avez un profil seccomp personnalisé, testez-le en tentant un appel système interdit à l'intérieur du conteneur. Par exemple, avec un profil qui bloque `mkdir`, exécuter `mkdir /tmp/test` devrait échouer avec `Operation not permitted`.

### 4. Tester le remappage des espaces de noms utilisateur

Après activation de `userns-remap`, exécutez un conteneur et vérifiez la propriété des fichiers créés dans un volume du point de vue de l'hôte.

```bash
docker run -d --name test -v /tmp/testvol:/data alpine sleep 300
docker exec test touch /data/testfile
ls -l /tmp/testvol
```

La sortie attendue montre `testfile` appartenant à `dockremap` (par exemple, `140000`) plutôt qu'à root.

### 5. Confirmer l'absence de conteneurs privilégiés

Relancez la vérification des conteneurs privilégiés :

```bash
docker ps --quiet | xargs docker inspect --format '{{.Name}}: Privileged={{.HostConfig.Privileged}}'
```

La sortie doit afficher `Privileged=false` pour tous les conteneurs. Si certains sont `true`, remédiez immédiatement.

### 6. Vérifier l'isolation réseau

Testez la communication inter-conteneurs. Avec `icc: false`, les conteneurs sur le bridge par défaut ne peuvent pas communiquer. Créez deux conteneurs de test sur le bridge par défaut :

```bash
docker run -d --name test1 alpine sleep 300
docker run -d --name test2 alpine sleep 300
```

Obtenez l'IP de `test1` :

```bash
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' test1
```

Exemple de sortie : `172.17.0.2`

Depuis `test2`, essayez de pinger `test1` :

```bash
docker exec test2 ping -c 2 172.17.0.2
```

Résultat attendu : le ping échoue (100% de perte de paquets) car `icc` est désactivé. S'ils doivent communiquer, placez-les sur un réseau défini par l'utilisateur.

### 7. Surveiller les anomalies d'exécution

Utilisez des outils comme Falco ou Sysdig pour détecter les comportements suspects. Installez Falco sur l'hôte selon sa documentation. Ensuite, testez en exécutant un conteneur qui tente d'écrire dans `/etc` à l'intérieur du conteneur :

```bash
docker run --rm -it alpine sh -c "echo bad > /etc/test"
```

Falco devrait générer une alerte. Exemple de ligne de journal Falco :

```
12:34:56.789012345: Warning Container with writable /etc directory detected (user=root command=sh container_id=abc123)
```

Configurez les alertes pour qu'elles aillent vers votre canal de réponse aux incidents.

## Modes de défaillance et récupération

Les changements de sécurité peuvent casser les applications. Planifiez les échecs et sachez comment revenir en arrière rapidement.

### Modes de défaillance courants

- **L'application nécessite des privilèges** : Certaines applications ont légitimement besoin de certaines capacités ou même du mode privilégié (par exemple, démons système, outils réseau). Si vous supprimez des capacités et que l'application échoue, réajoutez uniquement les capacités nécessaires.
- **Le remappage des espaces de noms utilisateur casse les permissions de fichiers** : Les volumes existants peuvent devenir inaccessibles en raison du mappage UID/GID. Solutions : recréez les volumes avec la bonne propriété, ou désactivez `userns-remap`.
- **Le système de fichiers racine en lecture seule cause des erreurs d'écriture** : Les applications peuvent tenter d'écrire à des emplacements arbitraires. Utilisez `--tmpfs` ou des montages de volume pour les chemins inscriptibles requis.
- **L'isolation réseau bloque le trafic légitime** : Les services internes peuvent avoir besoin de communiquer sur le bridge par défaut. Utilisez des réseaux définis par l'utilisateur avec une connectivité explicite.
- **Les limites de ressources causent des kills OOM ou une limitation** : Des limites trop basses peuvent faire planter les conteneurs. Ajustez en fonction de l'utilisation observée.

### Procédures de retour en arrière

Pour tout changement de configuration du démon, gardez une sauvegarde de `/etc/docker/daemon.json`. Pour revenir en arrière :

```bash
sudo cp /etc/docker/daemon.json.backup /etc/docker/daemon.json
sudo systemctl restart docker
```

Pour les changements au niveau du conteneur, recréez simplement le conteneur avec les drapeaux précédents.

Documentez les étapes exactes pour annuler chaque changement dans votre runbook.

### Vérifications de récupération

Après le retour en arrière, vérifiez que l'environnement fonctionne :

- Exécutez `docker ps` pour vous assurer que les conteneurs sont en cours d'exécution.
- Vérifiez les journaux d'application pour les erreurs.
- Testez la connectivité.
- Relancez les commandes de vérification pertinentes de la section précédente.

## Liste de contrôle des opérations

Utilisez cette liste de contrôle pour les revues de sécurité régulières et dans le cadre de votre routine opérationnelle.

### Opérations quotidiennes

- [ ] Surveiller l'utilisation des ressources de l'hôte Docker (CPU, mémoire, disque) pour les anomalies. Responsable : ingénieur DevOps de garde. Vérifiez via les alertes Prometheus.
- [ ] Vérifier les journaux du démon Docker (`journalctl -u docker`) pour les erreurs ou événements de sécurité.
- [ ] Examiner les journaux des conteneurs pour les activités suspectes.
- [ ] Vérifier que la surveillance/alerte (par exemple, Falco) fonctionne.

### Opérations hebdomadaires

- [ ] Exécuter Docker Bench for Security et examiner les nouveaux avertissements. Responsable : responsable sécurité.
- [ ] Scanner les images pour les nouvelles vulnérabilités (si non intégré au CI/CD).
- [ ] Examiner l'accès des utilisateurs aux hôtes Docker et les permissions de la socket. Responsable : responsable infrastructure.
- [ ] Vérifier les conteneurs, images, volumes et réseaux orphelins ; nettoyer :

```bash
docker system prune -f --volumes
```

Attention avec `--volumes` car cela supprime les volumes inutilisés.

### Opérations mensuelles

- [ ] Mettre à jour le moteur Docker et la CLI vers la dernière version stable. Responsable : responsable infrastructure, planifié le premier mardi de chaque mois.
- [ ] Examiner et mettre à jour les profils seccomp/AppArmor si nécessaire.
- [ ] Faire tourner les secrets et identifiants utilisés par les conteneurs (par exemple, identifiants de registre, clés API).
- [ ] Tester les procédures de sauvegarde et de restauration.
- [ ] Examiner les limites de ressources et ajuster en fonction des tendances de performance.
- [ ] Évaluer les nouvelles fonctionnalités de sécurité dans les versions Docker et planifier leur adoption.

### Réponse aux incidents

En cas d'incident de sécurité :

- Isolez l'hôte ou le conteneur affecté.
- Capturez les données forensiques : journaux de conteneur, métadonnées d'image, journaux de l'hôte.
- Identifiez la vulnérabilité (par exemple, port exposé, dépendance vulnérable).
- Corrigez et redéployez.
- Documentez l'incident et mettez à jour la liste de contrôle pour éviter la récurrence.

## Pièges courants et comment les éviter

1. **Ignorer l'exposition de la socket Docker** : De nombreux développeurs montent `/var/run/docker.sock` dans des conteneurs par commodité, donnant à ces conteneurs un accès équivalent à root sur l'hôte. Utilisez un proxy comme `docker-socket-proxy` avec un accès API en lecture seule ou restreint, et uniquement pour les conteneurs qui en ont réellement besoin.

2. **Exécuter des conteneurs en tant que root** : Par défaut, les conteneurs Docker s'exécutent en root. Si un attaquant s'échappe du conteneur, il obtient root sur l'hôte. Utilisez la directive `USER` dans les Dockerfiles pour exécuter en tant qu'utilisateur non root, et évitez `--privileged` sauf nécessité absolue.

3. **Ne pas définir de limites de ressources** : Sans limites de mémoire ou CPU, un seul conteneur peut épuiser les ressources de l'hôte, causant un déni de service. Définissez toujours `--memory` et `--cpus` en production, et utilisez des orchestrateurs comme Kubernetes pour appliquer des limites.

4. **Utiliser des images de base obsolètes** : Les images de base contiennent souvent des vulnérabilités connues. Mettez régulièrement à jour les images et scannez-les avec des outils comme Trivy dans le CI/CD. Épinglez les versions d'image pour éviter les changements inattendus.

5. **Désactiver les fonctionnalités de sécurité pour simplifier** : Désactiver seccomp, AppArmor ou SELinux pour faire fonctionner une application expose l'hôte. Créez plutôt des profils personnalisés qui n'accordent que les appels système ou permissions requis.

6. **Stocker des secrets dans les images** : Coder en dur des mots de passe ou clés API dans les Dockerfiles ou variables d'environnement est une erreur courante. Utilisez les secrets Docker, les secrets Kubernetes ou des gestionnaires de secrets externes comme Vault, et ne jamais intégrer de secrets dans les couches d'image.

7. **Négliger la segmentation réseau** : S'appuyer sur le réseau bridge par défaut permet à tous les conteneurs de communiquer. Utilisez des réseaux définis par l'utilisateur avec une connectivité explicite et envisagez des politiques réseau (par exemple, Calico, Cilium) dans Kubernetes.

## Conclusion

La sécurité Docker n'est pas une configuration unique ; elle exige un effort opérationnel continu. Cette liste de contrôle fournit une base pour durcir votre environnement Docker, vérifier les contrôles de sécurité et récupérer après échec. Commencez par l'inventaire, appliquez les changements de configuration sûrs de manière incrémentale et établissez des cycles de revue réguliers. Adaptez la liste de contrôle à vos charges de travail et profil de risque spécifiques. En suivant ces pratiques, vous pouvez réduire considérablement la surface d'attaque de vos applications conteneurisées et maintenir une posture de sécurité robuste en production.