Docker pour les devs - 15 - Permissions et utilisateurs

Root par défaut, instruction USER, UID/GID, le cauchemar des bind mounts et comment faire tourner tes conteneurs en non-root.

  1. 01 Docker pour les devs - 00 - Pourquoi Docker change tout
  2. 02 Docker pour les devs - 01 - Containers vs VMs
  3. 03 Docker pour les devs - 02 - L'architecture de Docker
  4. 04 Docker pour les devs - 03 - Docker Desktop, Engine et alternatives
  5. 05 Docker pour les devs - 04 - Écrire un Dockerfile
  6. 06 Docker pour les devs - 05 - Layers et cache
  7. 07 Docker pour les devs - 06 - Le .dockerignore
  8. 08 Docker pour les devs - 07 - Multi-stage builds
  9. 09 Docker pour les devs - 08 - Choisir son image de base
  10. 10 Docker pour les devs - 09 - Docker Compose, les bases
  11. 11 Docker pour les devs - 10 - Docker Compose avance
  12. 12 Docker pour les devs - 11 - Networking Docker, les bases
  13. 13 Docker pour les devs - 12 - Networking Docker avance
  14. 14 Docker pour les devs - 13 - Volumes et persistance
  15. 15 Docker pour les devs - 14 - Variables d'environnement et secrets
  16. 16 Docker pour les devs - 15 - Permissions et utilisateurs
  17. 17 Docker pour les devs - 16 - Docker et monorepo
  18. 18 Docker pour les devs - 17 - Dev vs Prod
  19. 19 Docker pour les devs - 18 - ENTRYPOINT, CMD et scripts d'initialisation
  20. 20 Docker pour les devs - 19 - Debugger ses conteneurs
  21. 21 Docker pour les devs - 20 - Bases de donnees dans Docker
  22. 22 Docker pour les devs - 21 - Sauvegardes et restauration
  23. 23 Docker pour les devs - 22 - Conteneuriser un frontend
  24. 24 Docker pour les devs - 23 - Sécurité des conteneurs
  25. 25 Docker pour les devs - 24 - Optimisation des images
  26. 26 Docker pour les devs - 25 - Builds multi-platform
  27. 27 Docker pour les devs - 26 - Limiter les ressources de tes conteneurs
  28. 28 Docker pour les devs - 27 - Gerer les logs comme un adulte
  29. 29 Docker pour les devs - 28 - Healthchecks et restart policies
  30. 30 Docker pour les devs - 29 - Nettoyer Docker avant qu'il mange ton disque
  31. 31 Docker pour les devs - 30 - Registries et stratégie de tags
  32. 32 Docker pour les devs - 31 - Docker en CI/CD
  33. 33 Docker pour les devs - 32 - Au-dela de Compose
  34. 34 Docker pour les devs - 33 - Glossaire Docker de A a Z

15 - Permissions et utilisateurs

Ce que tu vas apprendre

  • Pourquoi les conteneurs tournent en root par défaut et pourquoi c'est un problème
  • L'instruction USER dans le Dockerfile
  • Le mapping UID/GID entre l'hote et le conteneur
  • L'enfer des permissions avec les bind mounts
  • Les solutions : fixuid, gosu, et le flag --user

Prerequisites


Tu créés un fichier de log depuis ton conteneur. Tu regardes sur ta machine : le fichier appartient a root. Tu ne peux pas le supprimer sans sudo. Ca t'est deja arrive ? C'est le problème de permissions Docker le plus courant, et il rend fou les gens qui debutent.

Root par défaut

Par défaut, les processus dans un conteneur Docker tournent en tant que root (UID 0). Si tu n'as jamais ajoute d'instruction USER dans ton Dockerfile, tout tourne en root.

En dev, ca semble anodin. En production, c'est un risque. Si un attaquant exploite une faille dans ton application conteneurisee, il obtient les droits root dans le conteneur. Et dans certaines configurations, root dans le conteneur = root sur l'hote.

bash# Verifie sous quel utilisateur tourne ton conteneur
docker compose exec app whoami
# root

L'instruction USER

La solution la plus directe : créer un utilisateur non-root dans ton Dockerfile :

dockerfileFROM node:20-alpine

# Creer un utilisateur avec un UID specifique
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -D appuser

WORKDIR /app
COPY --chown=appuser:appgroup . .

USER appuser
CMD ["node", "dist/index.js"]

A partir de l'instruction USER, toutes les commandes RUN, CMD et ENTRYPOINT suivantes s'executent en tant que appuser.

Les images officielles Node incluent deja un utilisateur node (UID 1000). Tu peux l'utiliser directement :

dockerfileFROM node:20-alpine
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ["node", "dist/index.js"]

Sur paltemps.fr, tous mes conteneurs de production tournent avec USER node. Ca m'a pris 5 minutes a configurer et ca ferme un vecteur d'attaque.

UID/GID : le lien entre hote et conteneur

Les permissions Linux ne connaissent pas les noms d'utilisateurs. Elles fonctionnent avec des numeros : UID (User ID) et GID (Group ID). Quand un fichier appartient a UID 1000 dans le conteneur, il appartient aussi a UID 1000 sur l'hote, meme si cet UID correspond a un utilisateur différent.

bash# Dans le conteneur
id
# uid=1001(appuser) gid=1001(appgroup)

# Sur l'hote, le fichier cree appartient a UID 1001
ls -ln /path/to/mounted/file
# -rw-r--r-- 1 1001 1001 42 Mar 29 docker-15-permissions.md

Si ton utilisateur sur l'hote est UID 1000, et que le conteneur créé des fichiers en tant que UID 1001, tu ne pourras pas les modifier sans changer les permissions.

L'enfer des bind mounts

Les bind mounts sont là où les permissions deviennent penibles. Le scénario classique :

  1. Tu montes ./src dans le conteneur
  2. Le conteneur tourne en root (UID 0)
  3. L'app créé des fichiers (logs, caches, node_modules)
  4. Ces fichiers appartiennent a root sur ta machine
  5. Ton éditeur ne peut pas les modifier, rm demande sudo

Le problème inverse existe aussi : tu montes un répertoire qui appartient a ton utilisateur (UID 1000) sur l'hote, mais le conteneur tourne en tant que appuser (UID 1001). L'app ne peut pas écrire dans le volume.

Solution 1 : matcher les UIDs

La solution la plus simple : faire en sorte que l'utilisateur du conteneur ait le meme UID que ton utilisateur hote :

dockerfileFROM node:20-alpine
ARG USER_UID=1000
ARG USER_GID=1000

RUN deluser node && \
    addgroup -g $USER_GID appgroup && \
    adduser -u $USER_UID -G appgroup -D appuser

USER appuser

Et dans Compose :

yamlservices:
  app:
    build:
      context: .
      args:
        USER_UID: 1000
        USER_GID: 1000

Ca fonctionne bien quand tout le monde dans l'équipe a le meme UID (c'est souvent le cas : 1000 est le premier utilisateur créé sur Linux). Sur Mac et Windows avec Docker Desktop, le problème est gere differemment car les fichiers passent par la VM.

Solution 2 : le flag --user

Tu peux forcer l'UID/GID au lancement sans modifier le Dockerfile :

bashdocker run --user $(id -u):$(id -g) myimage

Ou dans Compose :

yamlservices:
  app:
    user: "${UID:-1000}:${GID:-1000}"

Le problème : si l'utilisateur n'existe pas dans /etc/passwd du conteneur, certains outils se plaignent ("I have no name!"). Ca fonctionne quand meme pour la plupart des cas.

Solution 3 : fixuid

fixuid est un outil qui ajuste dynamiquement l'UID/GID de l'utilisateur du conteneur au démarrage :

dockerfileFROM node:20-alpine

RUN addgroup -g 1000 appgroup && \
    adduser -u 1000 -G appgroup -D appuser

# Installer fixuid
RUN USER=appuser && \
    GROUP=appgroup && \
    curl -SsL https://github.com/boxboat/fixuid/releases/download/v0.6.0/fixuid-0.6.0-linux-amd64.tar.gz | tar -C /usr/local/bin -xzf - && \
    chown root:root /usr/local/bin/fixuid && \
    chmod 4755 /usr/local/bin/fixuid && \
    mkdir -p /etc/fixuid && \
    printf "user: $USER\ngroup: $GROUP\n" > /etc/fixuid/config.yml

USER appuser:appgroup
ENTRYPOINT ["fixuid"]
CMD ["node", "dist/index.js"]

Au démarrage, fixuid change l'UID/GID de appuser pour matcher celui passe avec --user. C'est transparent pour l'application.

Solution 4 : gosu pour changer d'utilisateur

gosu est un remplacement de su et sudo adapte aux conteneurs :

dockerfileFROM node:20-alpine
RUN apk add --no-cache gosu

COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
bash#!/bin/sh
# entrypoint.sh
# Ajuster les permissions si necessaire
chown -R node:node /app/data

# Lancer l'app en tant que node
exec gosu node node dist/index.js

L'entrypoint tourne en root pour ajuster les permissions, puis gosu drop les privileges avant de lancer l'application.

Les bonnes pratiques

  1. Ajoute toujours USER dans tes Dockerfiles de production
  2. Utilise --chown dans les instructions COPY et ADD
  3. Pour le dev avec bind mounts, aligne l'UID du conteneur sur celui de l'hote
  4. Ne monte jamais un bind mount en écriture si tu n'en as pas besoin (:ro)
  5. Teste les permissions dans ta CI : docker run --user 1000:1000 myimage whoami

Résumé

  • Les conteneurs tournent en root par défaut : toujours ajouter USER en production
  • Les permissions sont basees sur les UID/GID, pas les noms d'utilisateurs
  • Les bind mounts heritent des permissions de l'hote : aligner les UIDs resout la plupart des problèmes
  • fixuid et gosu sont des outils specialises pour les cas complexes
  • Le flag --user force l'UID/GID sans modifier l'image

Navigation : Precedent : 14 - Variables d'environnement et secrets | Suivant : 16 - Docker en monorepo


Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.