Journal Utiliser Podman en mode rootless pour exécuter en service des containers rootless

Posté par  (site web personnel, Mastodon) . Licence CC By‑SA.
40
29
avr.
2022

Sommaire

Hello,

J'ai eu l'occasion de remonter mon serveur et tous ses services suite à un bug sur le système de fichier.

Je pouvais juste remonter les sauvegardes de ma Debian 11 Bullseye, mais j'avais prévu une nouvelle machine et je voulais en profiter pour refaire à la main les installations des services.

J'emploie Debian et j'utilise donc d'habitude les paquets Debian.

Mais, bien sûr, j'ai certains services, comme Firefox Sync et Grafana, qui ne sont pas disponible dans les répertoires Debian.

Cette fois, je voulais profiter d'avoir un peu plus d'expérience avec Docker pour installer ces services sous formes de container au lieu d'ajouter leur répertoire apt dans Debian (et donc leur donner les droits d'administration sur ma machine lors de l'installation et mise à jour).

Pour être un peu plus serein, je ne voulais pas utiliser Docker en mode "service avec les droits root" pour éviter de donner trop de privilèges aux containers.

J'ai donc voulu essayer d'utiliser podman parce que j'en avais pas mal entendu parler: podman est un gestionnaire de container qui est prévu pour pouvoir être employé par un utilisateur standard, c'est à dire sans droits d'administrations (autrement dit en mode rootless).

Pour info, Docker sait aussi s'exécuter en mode rootless, c'est juste un peu moins clé en main dans Debian.

J'ai un peu galéré pour la gestion des volumes et du réseau, alors je me suis dit que c'était une bonne occasion de partager cette expérience et de montrer comment j'ai pu monter un service sur mon serveur qui exécute un container en mode rootless.

Pour l'exemple, je vais prendre un service Grafana qui permet de visualiser l'état de mon serveur.

J'ai volontairement pris en exemple Grafana, parce qu'il soulève le problème des droits sur les volumes et parce qu'il a besoin de communiquer avec l'hôte dans ma configuration (pour accéder à Prometheus et à Postgresql installés sur l'hôte).

Le problème de droits sur les volumes est dû au fait que l'image de Grafana suit la bonne pratique d'utiliser, à l'intérieur du container, un utilisateur standard pour exécuter le processus Grafana.

Le mode rootless sera donc appliqué au gestionnaire de container de l'hôte (podman) et à l'intérieur du container lui-même (Grafana).

Préparation du système hôte

Au début de mes recherches, je suis tombé sur le site https://rootlesscontaine.rs/ qui explique bien comment débuter avec podman (Docker et d'autre gestionnaires) en mode rootless.

J'installe quelques outils, en suivant leurs conseils:

sudo apt install podman slirp4netns uidmap dbus-user-session fuse-overlayfs systemd-container
  • podman: le gestionnaire de container
  • slirp4netns: outil qui permet à l'utilisateur de gérer un namespace réseau sans privilèges d'administration
  • uidmap: utilitaires pour gérer les namespaces d'identifiants utilisateurs (les UID et GID) sans privilèges d'administration
  • dbus-user-session: les sessions dbus utilisateurs sont requis pour utiliser systemd avec les cgroup v2
  • fuse-overlayfs: pour les kernels < 5.11, il vaut mieux utiliser FUSE pour la gestion du système de fichier par utilisateur
  • systemd-container: permettra d'utiliser machinectl pour se connecter avec l'utilisateur et ainsi avoir la variable XDG_RUNTIME_DIR configurée

Configuration de l'utilisateur pour exécuter le service

Ensuite, je crée un user dédié au service:

sudo adduser --system --disabled-password grafana

Comme l'utilisateur devra toujours faire tourner ce service, il faut dire à systemd que cet utilisateur doit toujours avoir une session ouverte:

sudo loginctl enable-linger grafana

Comme indiqué sur rootlesscontaine.rs, je donne des plages d'UID et de GID à l'utilisateur pour qu'il puisse les utiliser pour exécuter des containers rootless.
Je modifie donc les fichiers /etc/subuid et /etc/subgid pour leur ajouter une ligne comme:

grafana:100000:65536

Ça veut dire que le système dédie une plage de 65'536 identifiants à l'utilisateur grafana (il peut donc utiliser les identifants de 100'000 à 165'535).

Une information très importante: si vous avez essayé d'exécuter une commande podman en tant qu'utilisateur standard avant d'avoir modifié ces deux fichiers, il faut dire à podman de "migrer" avec la commande (à exécuter en tant qu'utilisateur):

podman system migrate

J'ai galéré un peu à comprendre pourquoi je continuais d'avoir des erreurs qui me disaient potentially insufficient UIDs or GIDs available in user namespace alors que je venais de corriger le fichier /etc/subgid.

Préparer et démarrer le service

Pour se connecter avec l'utilisateur sur mon serveur, je dois utiliser machinectl:

sudo machinectl shell grafana@

Comme j'ai créé un utilisateur avec l'option --system, Debian n'a pas copié les fichiers par défaut.
Si vous avez besoin de certains fichiers, pensez à les copier:

cp -a /etc/skel/.config/ ~

Pour stocker la configuration du container, je créé un fichier .env dans le dossier utilisateur.
Ce fichier permettra de changer la configuration du container sans avoir besoin de modifier le fichier .service de systemd.

# Ajuster le masque utilisateur pour refuser la lecteur par les autres
umask 077
editor grafana.env

Pour Grafana, il faut lire la documentation et j'y ai mis:

# Nous allons monter un volume dans le dossier /grafana
GF_PATHS_CONFIG=/grafana/grafana.ini
# Pour remonter les logs à Podman, il faut mettre les logs dans stdout et stderr
GF_LOG_MODE=console

Ensuite, je crée un volume avec la configuration de Grafana.

# Ajuster le masque utilisateur pour refuser la lecteur par les autres
umask 077
mkdir grafana
editor grafana/grafana.ini

J'avais déjà un fichier grafana.ini de mon ancien serveur, je l'ai copié et j'ai fait quelques ajustements:

En mode rootless, podman utilise le service slirp4netns et le gateway réseau vu par le container a toujours l'adresse IP 10.0.2.2.

Mon fichier grafana.ini comptait sur le service Postgresql du serveur local, j'ai donc dû remplacer localhost par 10.0.2.2 comme serveur Postgresql.

L'image Docker de Grafana suit la bonne pratique de modifier l'UID de l'utilisateur qui exécute le binaire dans le container.
L'utilisateur de Grafana utiliser l'UID 472, mais celui-ci n'existe pas vraiment sur le système hôte, on ne peut donc pas faire un simple chown 472:nogroup -R grafana.

Pour pouvoir partager ce dossier via un volume, il faut demander à podman d'ajuster les droits au dossier:

podman unshare chown 472:0 -R grafana

Pour plus d'explications sur cette partie de la configuration des droits du volume, vous pouvez suivre cet article de Dan Walsh.

Enfin, on peut exécuter le container Grafana:

podman run --name=grafana -p 3000:3000 --env-file ~/grafana.env --volume /home/grafana/grafana:/grafana --net slirp4netns:allow_host_loopback=true docker.io/grafana/grafana-oss

C'est une commande podman (ou Docker assez classique), à l'exception de l'option --net slirp4netns:allow_host_loopback=true qui demande à slirp4netns d'autoriser la communication depuis le container vers l'hôte.

J'ai besoin de cette communication pour permettre à Grafana de joindre Postgresql et Prometheus (j'ai dû aussi mettre à jour le DataSource Prometheus pour utiliser l'adresse IP 10.0.2.2).

Créer un service systemd et le démarrer

Enfin, on est prêt à configurer systemd pour démarrer ce container automatiquement.

Pour ça, j'ai suivi cet article et j'y ai entre autre appris que podman est un gestionnaire de pod, une notion que j'utilise régulièrement avec OpenShift (et donc Kubernetes). Un pod peut contenir un ensemble de containers et leur permet de communiquer ensemble avec le hostname localhost.

Comme vous pouvez le voir dans l'article, podman propose une commande qui génère les fichiers services systemd à installer dans le bon répertoire (dans notre cas 1 seul fichier, parce qu'on a crée un container sans pod):

mkdir -p ~/.config/systemd/user
cd ~/.config/systemd/user
podman generate systemd --name --new --restart-policy=on-failure -f grafana

Enfin, il faut avertir systemd de démarrer au boot ce service et de l'exécuter maintenant:

systemctl --user daemon-reload
systemctl --user enable --now container-grafana.service

Conclusion

Je suis assez content d'avoir pu installer des services qui n'existent pas dans les paquets Debian sans avoir eu besoin de leur donner un accès root (ce qui se passe quand on utilise apt pour l'installation et les mises à jour).

C'était plus long et compliqué que prévu, mais au final ça tourne.

Peut être que l'installation d'une version minimale de Kubernetes ou d'OpenShift (enfin, OKD) aurait été plus rapide, je ne sais pas et je ne sais pas non plus quelles sont les prérequis réseaux et matériels pour ces outils.

Pour l'instant, j'ai vu ces inconvénients:

  • Je n'ai plus la vérification GPG faite par apt, je fais directement confiance à la gestion du service publique Docker Hub.
  • Les services systèmes de systemd ne sont pas accessibles depuis l'utilisateur. J'ai dû donc pour chaque utilisateur copier un service notify-admin@.service pour pouvoir ajouter l'option OnFailure qui va bien.
  • Les logs des containers ne sont accessibles uniquement avec podman logs grafana. J'espérais les voir dans les logs de l'utilisateur avec journalctl --user, mais je n'ai rien du tout. Je dois manquer d'indices pour configurer systemd-journald et podman. Le top aurait était que l'utilisateur root de l'hôte puisse voir tous les logs de tous les services avec journalctl pour me simplifier la gestion de mon serveur.

Pour aller encore un peu plus loin, il faudrait encore sécuriser ces services avec la configuration AppArmor ou SELinux qui va bien (Debian installe par défaut AppArmor et RedHat SELinux).

  • # inconvénients

    Posté par  (Mastodon) . Évalué à 9.

    Je n'ai plus la vérification GPG faite par apt, je fais directement confiance à la gestion du service publique Docker Hub.

    Note que tu peux très bien construire ta propre image de grafana à partir des paquets signés d'une distrib qui le fournit.

    Les logs des containers ne sont accessibles uniquement avec podman logs grafana. J'espérais les voir dans les logs de l'utilisateur avec journalctl --user, mais je n'ai rien du tout. Je dois manquer d'indices pour configurer systemd-journald et podman. Le top aurait était que l'utilisateur root de l'hôte puisse voir tous les logs de tous les services avec journalctl pour me simplifier la gestion de mon serveur.

    Il est possible d'avoir les logs dans systemd avec podman.

    • Deux méthode: fournir ta propre unit file pour le service au lieu d'utiliser podman generate et démarrer le pod comme s'il était lancé en interactif, non détaché.

    • Utiliser l'option --log-driver=journald (mais je ne sais pas si c'est déjà supporté par la version de podman proposée par debian).

    • [^] # Re: inconvénients

      Posté par  (site web personnel, Mastodon) . Évalué à 7.

      Merci pour les idées!

      J'ai oublié de mettre le fichier service généré par podman (j'ai juste ajouté la ligne OnFailure):

      # container-grafana.service
      # autogenerated by Podman 3.0.1
      # Thu Apr 28 23:09:17 CEST 2022
      
      [Unit]
      Description=Podman container-grafana.service
      Documentation=man:podman-generate-systemd(1)
      Wants=network.target
      After=network-online.target
      OnFailure=notify-admin@%n
      
      [Service]
      Environment=PODMAN_SYSTEMD_UNIT=%n
      Restart=on-failure
      TimeoutStopSec=70
      ExecStartPre=/bin/rm -f %t/container-grafana.pid %t/container-grafana.ctr-id
      ExecStart=/usr/bin/podman run --conmon-pidfile %t/container-grafana.pid --cidfile %t/container-grafana.ctr-id --cgroups=no-conmon -d --replace --name=grafana -p 3000:3000 --env-file /home/grafana/grafana.env --volume /home/grafana/grafana:/grafana --net slirp4netns:allow_host_loopback=true docker.io/grafana/grafana-oss
      ExecStop=/usr/bin/podman stop --ignore --cidfile %t/container-grafana.ctr-id -t 10
      ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/container-grafana.ctr-id
      PIDFile=%t/container-grafana.pid
      Type=forking
      
      [Install]
      WantedBy=multi-user.target default.target

      Pour tes idées, je commence par la deuxième qui repose sur celle proposée par la génération automatique de podman.

      Utiliser l'option --log-driver=journald (mais je ne sais pas si c'est déjà supporté par la version de podman proposée par debian).

      J'avais essayé ça, mais j'ai eu l'impression que ça ne marchait pas.

      En gardant le fichier tel que généré et en ajoutant les arguments --log-driver=journald --log-opt tag=grafana, les logs existent bien, mais ils apparaissent dans le journal système et ils sont préfixés par conmon.

      Ça m'a vraiment surpris que ça apparaissent dans le journal --system. C'est parce que journald détecte que l'utilisateur grafana est un utilisateur système (avec un UID < 1000) et il redirige ses logs directement dans les logs systèmes.

      Donc, malgré moi, lors de la création de l'utilisateur, j'avais pris le bon choix de demander d'en faire un utilisateur système car ça me centralise les logs (ce que je cherchais à faire justement :)).

      Pour le problème du préfixe conmon, il faudrait que j'essaie l'option --log-opt tag=grafana, mais avec podman 4, d'après ce bug (Debian a un podman en version 3.1).

      Pour l'instant, je vais essayer ta première idée, car je vais pouvoir changer le tag syslog avec ça.

      Deux méthode: fournir ta propre unit file pour le service au lieu d'utiliser podman generate et démarrer le pod comme s'il était lancé en interactif, non détaché.

      Je n'y avais pas pensé, merci.

      J'ai essayé et ça semble bien fonctionner, même si ce n'est pas la solution officielle.

      Pour le faire, j'ai enlever l'argument -d de la commande podman run, j'ai passé le Type en simple. En plus, j'ai ajouté l'option SyslogIdentifier=%N pour éviter de voir toujours écrit podman dans les logs.

  • # signature

    Posté par  (site web personnel) . Évalué à 7.

    Je n'ai plus la vérification GPG faite par apt, je fais directement confiance à la gestion du service publique Docker Hub

    A priori ce n'est pas le cas pour grafana, mais il est tout à fait possible de signer les images et de forcer le client Docker pour ne récupérer que des images signées.
    Je n'ai par contre pas vérifié si ca peut fonctionner avec podman par contre.
    https://docs.docker.com/engine/security/trust/

    Maintenant, cela nécessite surtout que l'éditeur de l'image signe, ce n'est pas automatique.

    Juste un détail car je pense que je ne l'ai pas vu, mais une bonne pratique reste de sélectionner le tag souhaité et de ne pas dépendre de manière implicite du tag latest.
    D'ailleurs, comment tu mets à jour ? Aujourd'hui latest semble être 8.5.1, mais si demain il y a une 8.5.2 comment ca se passe ?
    L'avantage si tu force le tag dans ton service c'est que la mise à jour (et donc le pull de la nouvelle image) sera assez transparent puisque tu n'as qu'à changer la version dans le ficher de service et le redémarrer.

    • [^] # Re: signature

      Posté par  (site web personnel, Mastodon) . Évalué à 3.

      Merci pour l'info, je ne savais pas que les images pouvaient être signées avec Docker.

      C'est assez chouette, parce que ça permettrait d'avoir la même confiance que pour l'ajout du repository apt externe et ce sans avoir à donner les droits root.

      Pour le point des tags "latest", c'est très juste, il faut que je corrige ça.

  • # Merci !

    Posté par  . Évalué à 3.

    Merci beaucoup pour ce journal avec les explications détaillées pour Podman. Moi qui ai passé plusieurs semaines sur le mode rootless de Docker, la gestion des UID-GID et le partage de volumes entre hôte et container, j'ai lu avec beaucoup d'attention ton retour d'expérience. Jusque là je n'avais pas vraiment testé Podman, et encore moins en rootless. Ce journal va me lancer !

    Autrement, j'ai lu l'article dont tu as donné le lien pour expliquer comment changer le propriétaire d'un dossier, avec podman unshare chown UID:GID dossier mais je n'ai pas compris comment cela fonctionne, comment à ce moment là on peut changer le propriétaire vers un UID autre sans être root hôte… Est-ce que la commande en question est exécutée dans la namespace par l'utilisateur root (root du namespace) ?
    Quelque chose m'échappe, et je lirai avec attention toute réponse.

    • [^] # Re: Merci !

      Posté par  (site web personnel, Mastodon) . Évalué à 4.

      Merci beaucoup pour ce journal avec les explications détaillées pour Podman. Moi qui ai passé plusieurs semaines sur le mode rootless de Docker, la gestion des UID-GID et le partage de volumes entre hôte et container, j'ai lu avec beaucoup d'attention ton retour d'expérience. Jusque là je n'avais pas vraiment testé Podman, et encore moins en rootless. Ce journal va me lancer !

      De rien :) J'avais trouvé difficile de rassembler toutes les informations nécessaires, alors je me suis dit que ça pourrait être utile à d'autres.

      Si tu as déjà Docker en mode rootless, peut être que tu n'as pas besoin de changer de système.

      Est-ce que la commande en question est exécutée dans la namespace par l'utilisateur root (root du namespace) ?

      D'après le man de podman-unshare, tu as tout à fait compris ce qui se passe:

      podman-unshare(1)()

      NAME
      podman-unshare - Run a command inside of a modified user namespace

      SYNOPSIS
      podman unshare [--] [command]

      DESCRIPTION
      Launches a process (by default, $SHELL) in a new user namespace. The user namespace is configured so that the invoking user's UID and primary GID appear to be UID 0 and GID 0, respectively. Any ranges which match that user and group in /etc/subuid and /etc/subgid are also mapped in as themselves with the help of the newuidmap(1) and newgidmap(1) helpers.

      […]

      EXAMPLE
      $ podman unshare id
      uid=0(root) gid=0(root) groups=0(root),65534(nobody)

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.