TP

Le but de ce TP est de deployer docker puis de durcir au maximum les conteneurs déployés.

Prérequis : https://doc.oxyhack.com/books/ynov/page/lab-docker-default

Docker Bench - Premier test

Docker Bench est un outil que j’utilise pour évaluer la sécurité de ma configuration Docker. Il exécute une série de tests automatisés basés sur les bonnes pratiques de sécurité, m'aidant à identifier et corriger les failles potentielles dans mon environnement Docker.

  • Mise en Place :

    • Clonez Docker Bench :

      git clone https://github.com/docker/docker-bench-security.git
    • Exécutez l’outil :

      cd docker-bench-security
      sudo ./docker-bench-security.sh

Résultat du premier test :

Mise en place des actions de hardenninng (DockerFile)

1. Exécution de Conteneurs avec des Privilèges Réduits

Risques de Sécurité :

Exécuter des conteneurs avec des privilèges root expose l'hôte à des risques élevés en cas de compromission du conteneur.

Solution :

Pour limiter les risques, je vais exécuter mes conteneurs avec un utilisateur non-root. Docker permet de configurer un mappage des utilisateurs entre l'hôte et le conteneur grâce aux user namespaces. Cela permet de mapper l'utilisateur root du conteneur à un utilisateur non-privilégié sur l'hôte.

Comment ça fonctionne ?

Le mappage des utilisateurs permet de faire en sorte que le root dans le conteneur ne soit pas réellement un utilisateur root sur l'hôte. Par exemple, l'utilisateur root du conteneur peut être mappé à un utilisateur non privilégié sur l'hôte. Cela permet de restreindre les permissions d’un conteneur compromis.

Mise en Place :

Activer le mappage des utilisateurs (userns-remap) :

  • Pour cela, je vais modifier la configuration de Docker en ajoutant l'option userns-remap dans le fichier /etc/docker/daemon.json. Cela va forcer Docker à utiliser des espaces de noms utilisateurs pour mapper les utilisateurs du conteneur à ceux de l'hôte.

{
  "userns-remap": "default"
}
sudo systemctl restart docker

Modifier le Dockerfile : Dans mon Dockerfile, je vais créer un utilisateur non-root qui sera utilisé pour exécuter l'application à l'intérieur du conteneur.

# Je commence avec l'image de base python:3.9-slim
FROM python:3.9-slim

# Je crée un utilisateur non-root nommé "appuser"
RUN useradd -m appuser

# Je définis l'utilisateur non-root pour exécuter les commandes suivantes dans le conteneur
USER appuser

# Je définis le répertoire de travail pour l'application dans le conteneur
WORKDIR /home/appuser

# Je copie le fichier requirements.txt en s'assurant que l'utilisateur "appuser" a la propriété du fichier
COPY --chown=appuser:appuser requirements.txt /home/appuser/

# J'installe les dépendances Python sans cache pour alléger l'image
RUN pip install --no-cache-dir -r requirements.txt

# Je copie l'application Python dans le conteneur
COPY --chown=appuser:appuser app.py /home/appuser/

# Je définis la commande à exécuter lors du démarrage du conteneur
CMD ["python", "app.py"]

Vérification : Une fois le conteneur lancé, je peux vérifier que l'application tourne sous un utilisateur non-root avec la commande suivante :

docker exec -it <nom_du_conteneur> whoami


2. Hardenning du compose .yaml

Voici le docker-compose.yml

services:
  caddy:
    image: caddy:latest  # Image Docker pour le serveur Caddy
    container_name: caddy  # Nom du conteneur
    ports:
      - "80:80"  # Redirection du port 80
    volumes:
      - "./Caddyfile:/etc/caddy/Caddyfile:ro"  # Volume pour la configuration
    networks:
      - app_network  # Réseau du conteneur
    user: "1000:1000"  # Utilisateur du conteneur
    read_only: true  # Conteneur en lecture seule
    security_opt:
      - seccomp:default.json  # Sécurisation avec seccomp
      - apparmor:docker-default  # Profil AppArmor
    cap_drop:
      - ALL  # Suppression de toutes les capacités du conteneur
    deploy:
      resources:
        limits:
          memory: 512M  # Limite mémoire
          cpus: "0.5"  # Limite CPU
      restart_policy:
        condition: on-failure  # Redémarrage en cas d'échec
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://localhost/"]  # Vérification de l'état du conteneur

  backend:
    build:
      context: ./backend  # Dossier du backend
      dockerfile: Dockerfile  # Fichier Dockerfile pour construire l'image
    container_name: flask_app  # Nom du conteneur
    restart: unless-stopped  # Redémarrage sauf si stoppé manuellement
    networks:
      - app_network  # Réseau du conteneur
    user: "1000:1000"  # Utilisateur du conteneur
    read_only: true  # Conteneur en lecture seule
    security_opt:
      - seccomp:default.json  # Sécurisation avec seccomp
      - apparmor:docker-default  # Profil AppArmor
    cap_drop:
      - ALL  # Suppression de toutes les capacités du conteneur
    deploy:
      resources:
        limits:
          memory: 512M  # Limite mémoire
          cpus: "0.5"  # Limite CPU
          pids: 100  # Limite des processus
      restart_policy:
        condition: on-failure  # Redémarrage en cas d'échec
    environment:
      - FLASK_ENV=production  # Variable d'environnement Flask
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://localhost/"]  # Vérification de l'état du conteneur

  redis:
    image: redis:alpine  # Image Docker pour Redis
    container_name: redis  # Nom du conteneur
    restart: unless-stopped  # Redémarrage sauf si stoppé manuellement
    networks:
      - app_network  # Réseau du conteneur
    user: "1000:1000"  # Utilisateur du conteneur
    read_only: true  # Conteneur en lecture seule
    security_opt:
      - seccomp:default.json  # Sécurisation avec seccomp
      - apparmor:docker-default  # Profil AppArmor
    cap_drop:
      - ALL  # Suppression de toutes les capacités du conteneur
    deploy:
      resources:
        limits:
          memory: 256M  # Limite mémoire
          cpus: "0.2"  # Limite CPU
          pids: 50  # Limite des processus
      restart_policy:
        condition: on-failure  # Redémarrage en cas d'échec
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]  # Vérification de l'état de Redis

networks:
  app_network:
    driver: bridge  # Utilisation du driver bridge pour le réseau

Explication des mesures de sécurité appliquées :

  1. Exécution sous un utilisateur non-root (user: "1000:1000") :

    • Risque : Si un conteneur s'exécute en tant que root, un attaquant ayant compromis le conteneur pourrait potentiellement obtenir un accès complet au système hôte.

    • Solution : En exécutant les conteneurs sous un utilisateur non-root (ici l'UID 1000 et le GID 1000), on limite les privilèges d'un conteneur, réduisant ainsi le risque de prise de contrôle complète.

  2. Système de fichiers en lecture seule (read_only: true) :

    • Risque : Un système de fichiers en écriture peut être manipulé par un attaquant pour injecter du code malveillant ou modifier des fichiers sensibles.

    • Solution : En rendant le système de fichiers en lecture seule, on empêche toute modification par des processus malveillants, ce qui est particulièrement utile pour les services qui n'ont pas besoin d'écrire sur le disque.

  3. Options de sécurité (security_opt) :

    • seccomp:unconfined : Restreint les appels système que les conteneurs peuvent effectuer. Cela permet de réduire les risques d'exploitation des vulnérabilités dans le noyau.

    • no-new-privileges : Empêche l'élévation de privilèges. Même si un processus est compromis, il ne pourra pas obtenir des privilèges plus élevés que ceux accordés initialement au conteneur.

  4. Retrait des capacités inutiles (cap_drop: - ALL) :

    • Risque : Certains conteneurs peuvent avoir des capacités qui leur permettent d'effectuer des actions non sécurisées, comme manipuler les ressources du noyau ou interagir avec d'autres conteneurs de manière dangereuse.

    • Solution : En retirant toutes les capacités inutiles, on minimise la surface d'attaque. Cela empêche des actions malveillantes comme l'accès non autorisé au système hôte ou la manipulation de la configuration du conteneur.

  5. Limitation des ressources (CPU et mémoire) (deploy.resources.limits) :

    • Risque : Si un conteneur consomme trop de ressources (CPU, mémoire), il peut entraîner un déni de service (DoS) sur le système hôte ou affecter les autres conteneurs en partageant les ressources.

    • Solution : En limitant la mémoire et l'utilisation du CPU de chaque conteneur, on garantit que chaque service reste dans des limites contrôlées, prévenant ainsi l'épuisement des ressources du système.

  6. Contrôle de santé du conteneur (healthcheck) :

    • Risque : Si un service est défaillant et n'est pas détecté, il peut entraîner une perte de service sans alerte.

    • Solution : Un contrôle de santé vérifie régulièrement si le service fonctionne correctement. Si le service est en panne, Docker peut redémarrer automatiquement le conteneur pour maintenir la disponibilité du service.


3. Isolation Réseau et Restriction du Trafic

  • Risques de Sécurité : La communication réseau libre entre conteneurs expose le système à des attaques réseau internes.

  • Solution : Configurez des réseaux Docker et des règles nftables pour restreindre le trafic.

  • Mise en Place sur Fedora :

    • Créez un réseau Docker isolé :

      docker network create --driver bridge isolated_network
    • Associez les conteneurs au réseau isolé :

      docker run --network=isolated_network my-container-image
    • Utilisez nftables pour restreindre le trafic :

      sudo nft add table inet docker
      sudo nft add chain inet docker input { type filter hook input priority 0 \; }
      sudo nft add rule inet docker input iifname "docker0" drop
  • Vérification :

    docker network inspect isolated_network

4. Configurer l'Audit des Fichiers Docker

  • Risque : L'absence d'audit des fichiers Docker peut permettre des modifications malveillantes non détectées, affectant la sécurité du système.

  • Solution : J'ai configuré auditd pour suivre les accès et modifications aux fichiers critiques de Docker (comme /var/lib/docker, /etc/docker, etc.).

Commandes pour configurer les règles d'audit :

# Les mettres dans ce fichiers pour les rendres permanantes 
sudo nano /etc/audit/rules.d/audit.rules

# Vérifier les règles appliquées
sudo auditctl -l

# Recharger les règles
sudo auditctl -R /etc/audit/rules.d/docker.rules

# Génerer un rapport 
sudo aureport -f

Voici ma config :

# Surveiller l'exécution de Docker et ses composants principaux :
-w /usr/bin/dockerd -k docker
-w /usr/bin/docker -k docker
-w /usr/bin/docker-containerd -k docker
-w /usr/bin/docker-runc -k docker
-w /usr/bin/containerd -k docker
-w /usr/bin/containerd-shim -k docker
-w /usr/bin/runc -k docker

# Surveiller les fichiers de configuration de Docker et containerd :
-w /etc/docker -k docker
-w /etc/docker/daemon.json -k docker
-w /etc/default/docker -k docker
-w /etc/containerd/config.toml -k docker

# Surveiller les répertoires et fichiers système importants pour Docker :
-w /var/lib/docker -k docker
-w /run/containerd -k docker
-w /run/containerd/containerd.sock -k docker

# Surveiller les services systemd pour Docker et containerd :
-w /usr/lib/systemd/system/docker.service -k docker
-w /usr/lib/systemd/system/docker.socket -k docker
-w /usr/lib/systemd/system/containerd.service -k docker

# Surveiller les répertoires de Docker avec les permissions d'accès :
-w /var/lib/docker -p wa
-w /etc/docker -p wa

Test d'une règle :


5. Autoriser Uniquement les Utilisateurs de Confiance à Contrôler Docker

  • Risque : Si des utilisateurs non autorisés ont accès à Docker, ils peuvent exécuter des actions malveillantes ou causer des erreurs humaines affectant la sécurité du système.

  • Solution : Je vais créer un groupe dédié aux administrateurs Docker et m'assurer que seuls les utilisateurs de confiance sont membres de ce groupe.

    Commande :

    # 1. Créer un groupe 'docker-admins' pour les administrateurs Docker
    sudo groupadd docker-admins
    
    # 2. Ajouter l'utilisateur 'alex' au groupe 'docker-admins'
    sudo usermod -aG docker-admins alex
    
    # 3. Vérifier que 'alex' fait bien partie du groupe
    groups alex
    
    # 4. Vérifier les permissions actuelles du socket Docker
    ls -l /var/run/docker.sock
    
    # 5. Modifier les permissions et le groupe propriétaire du socket Docker
    sudo chmod 660 /var/run/docker.sock
    sudo chown root:docker-admins /var/run/docker.sock
    
    # 6. Vérifier que les permissions sont appliquées correctement
    ls -l /var/run/docker.sock
    
    # 7. Appliquer immédiatement le changement de groupe sans se déconnecter
    newgrp docker-admins
    

6. Activer TLS + les logs

  • Risque : Si Docker écoute sans cryptage (HTTP non sécurisé), des attaquants peuvent intercepter et manipuler la communication entre le client Docker et le serveur Docker, exposant ainsi le système à des risques.

  • Solution : Je vais configurer Docker pour utiliser TLS (Transport Layer Security) afin d'assurer que les connexions avec:

  • {
      "tls": true,
      "tlscert": "/etc/docker/certs/server-cert.pem",
      "tlskey": "/etc/docker/certs/server-key.pem",
      "tlsverify": true,
      "tlscacert": "/etc/docker/certs/ca.pem",
      "host": "tcp://0.0.0.0:2376"
    }

J’ai configuré Docker pour envoyer ses logs vers un serveur syslog distant afin de centraliser la gestion des logs. Voici les modifications apportées

{
  "log-driver": "syslog",  // Configure Docker pour envoyer les logs via le protocole syslog.
  "log-opts": {
    "syslog-address": "udp://192.168.5.129:514"  // Définit l'adresse du serveur syslog distant (IP : 192.168.5.129, port : 514).
  }
}

7. Sécurisation des fichiers TLS dans Docker

chown root:root /etc/docker/certs/*.pem
chmod 400 /etc/docker/certs/server-key.pem
chmod 444 /etc/docker/certs/{ca,server-cert}.pem

8. Activer Docker Content Trust (DCT)

  • Risque : Si Docker Content Trust (DCT) est désactivé, il est possible de télécharger et utiliser des images non vérifiées. Cela expose le système à des images malveillantes.

  • Solution : Je vais activer Docker Content Trust afin de garantir que seules des images signées et vérifiées sont téléchargées et utilisées.

Commande :

export DOCKER_CONTENT_TRUST=1 
# Possibilité de le rendre permanant si on l'ajoute dans bashrc

9. Ne Pas Désactiver le Profil Seccomp par Défaut

  • Risque : Le profil seccomp est un mécanisme de sécurité qui limite les appels système que les conteneurs peuvent effectuer. Le désactiver augmente la surface d'attaque, permettant à un conteneur d'effectuer des actions potentiellement dangereuses.

  • Solution : Je vais m'assurer que le profil seccomp est activé et que les conteneurs n'utilisent pas des configurations trop permissives.

    Commande pour appliquer le profil seccomp par défaut :

    docker run --security-opt seccomp=default.json my-container

10. Créer une Partition Séparée pour Docker

  • Risque : Si Docker utilise la même partition que le système d'exploitation, une surcharge de l'espace disque utilisé par Docker peut impacter la performance et la stabilité du système.

  • Solution : Pour limiter ce risque, je vais créer une partition dédiée pour Docker, en particulier pour le dossier /var/lib/docker où Docker stocke ses images et conteneurs.

Résultat après hardenning

Nous pouvons observer que ces modifications ont permis d’améliorer drastiquement mon score afin d’atteindre un niveau de sécurité plus qu’acceptable. Le risque de continuer trop le hardening serait de provoquer des conflits de configuration, de restreindre excessivement certaines fonctionnalités nécessaires au bon fonctionnement des services, ou de compliquer la gestion et la maintenance des systèmes. Il est donc important de trouver un équilibre entre sécurité et fonctionnalité.

Last updated