Aller au contenu

Préparer le conteneur de session du terminal

Charger le profil AppArmor, choisir l'image de session, ajuster les bornes : ce que le poste doit fournir quand le bac à sable de session s'exécute sur le poste.

Le bac à sable d’une session s’exécute sur un hôte d’exécution dédié, qui est le mode de référence, ou sur le poste, dans un conteneur sans privilège et sans réseau. Cette variante convient aux socles de poste qui admettent un moteur de conteneurs (livre blanc sécurité V3, section 4.4). Avec un hôte d’exécution dédié, le poste ne porte que l’extension, et cette page ne s’applique pas.

Dans la variante sur le poste, chaque commande que l’agent lance depuis lemni par l’outil Bash s’exécute dans ce conteneur, et non dans votre shell. Cette page décrit ce que le poste doit fournir pour que le conteneur s’ouvre, et quoi faire devant chaque refus. Le choix du lieu d’exécution est décrit dans Le cycle d’une session agentique.

Le conteneur s’ouvre au premier appel de l’outil Bash de la session, une seule fois. Les commandes suivantes y sont lancées sans rouvrir de conteneur. Il est détruit à la fin de la session, et rien n’en subsiste (livre blanc sécurité V3, section 4.2).

Ce que la commande y trouve :

  • la racine du dépôt, montée au même chemin absolu que sur le poste. La commande s’exécute dans le répertoire courant de lemni ; un changement de branche par l’outil SwitchBranch déplace ce répertoire vers l’espace de travail de la branche, et un espace situé hors de la racine montée est refusé à la commande. Le dépôt est le seul volume en écriture (livre blanc sécurité V3, section 4.4) ;
  • l’interprète /bin/bash --noprofile --norc -c, sans votre profil de shell et sans les alias ni les fonctions qu’il définit. L’environnement transmis est débarrassé des variables qui portent du code (BASH_ENV, ENV, SHELLOPTS, BASHOPTS, ZDOTDIR, BASH_FUNC_*). Le produit ne tente pas d’autre shell ;
  • l’identifiant numérique de votre compte sur le poste, sous la forme uid:gid, pour que les fichiers écrits vous appartiennent. Un conteneur qui tournerait sous root sur le projet est refusé ;
  • un /tmp inscriptible de 512 Mio, qui disparaît avec le conteneur.

Ce qu’elle n’y trouve pas :

  • le reste du disque : le dossier personnel, les clés SSH, les autres dépôts ne sont pas montés ;
  • le réseau : le conteneur est créé sans interface réseau et sans résolution de noms. npm install, pip install ou go mod download y échouent ; les dépendances dont le projet a besoin se trouvent dans l’image de session ou dans le dépôt (livre blanc sécurité V3, section 4.1) ;
  • les variables d’environnement du poste, ses clés et ses jetons : aucun n’est monté (livre blanc sécurité V3, section 4.3). Le produit pose lui-même HOME, sur la racine du projet, et TMPDIR, sur /tmp.

Les verrous posés sur le conteneur sont les mêmes à chaque ouverture, et aucun réglage n’en retire un : toutes les capacités retirées, no-new-privileges, racine en lecture seule, filtre d’appels système du moteur, profil AppArmor lemniscate-session-agent, et des bornes de mémoire, de processeur et de nombre de processus.

Sur un hôte Debian ou Ubuntu, le moteur refuse de démarrer un conteneur dont le profil AppArmor n’est pas chargé dans le noyau. Le terminal relaie ce refus dans le résultat de l’outil Bash, sans exécuter la commande et sans ouvrir de session non confinée :

Session refused: the AppArmor profile "lemniscate-session-agent" is not loaded on this workstation.
Lemniscate opens no session without it and never runs one unconfined. Nothing was left behind.
To install it once and for all (it survives reboots), from a checkout of the Lemniscate sources:
sudo install -m 0644 durcissement/apparmor/lemniscate-session-agent /etc/apparmor.d/
sudo apparmor_parser -r /etc/apparmor.d/lemniscate-session-agent
On a managed workstation, its administrator installs it.

La commande lemni installée porte sa propre copie du profil, dans un dossier apparmor à côté du programme. Quand cette copie est trouvée, la première commande du refus la cite par son chemin absolu, si bien que les deux commandes se recopient telles quelles depuis n’importe quel répertoire. Quand aucune copie n’est trouvée, le refus renvoie au fichier des sources, durcissement/apparmor/lemniscate-session-agent, comme ci-dessus.

Les deux commandes ont chacune un rôle :

Fenêtre de terminal
sudo install -m 0644 durcissement/apparmor/lemniscate-session-agent /etc/apparmor.d/
sudo apparmor_parser -r /etc/apparmor.d/lemniscate-session-agent

La première copie le profil sous /etc/apparmor.d/, d’où le système le recharge à chaque démarrage. La seconde le charge tout de suite, sans redémarrer. Un apparmor_parser -r lancé seul, sur le fichier des sources, charge le profil en mémoire seulement : au redémarrage suivant, le profil n’est plus là et le refus revient. Sur un poste géré, c’est l’administrateur du parc qui installe le profil.

Le refus est retenu pour toute la durée du processus : une fois le profil chargé, relancez lemni pour ouvrir une session.

Le profil embarqué est la copie exacte du fichier des sources. Il décrit la disposition des montages que le produit impose, et non la charge de travail du projet : un compilateur ou un gestionnaire de paquets de plus dans l’image ne le rend pas faux.

Sur un hôte dont le moteur n’applique pas AppArmor, RHEL et ses dérivés par exemple, le conteneur s’ouvre quand même. Le terminal écrit alors, une fois, sur sa sortie d’erreur, une annonce qui commence par [CONFINEMENT] NO MANDATORY ACCESS CONTROL. Elle nomme ce qui reste appliqué (capacités, no-new-privileges, filtre d’appels système, racine en lecture seule, bornes de ressources) et ce qui est perdu : le second verrou qui refuse les écritures hors du volume de travail de la session, et le refus nommé de mount et de ptrace. Le produit ne livre pas de politique SELinux.

Quand le moteur manque, ou n’applique pas seccomp

Section intitulée « Quand le moteur manque, ou n’applique pas seccomp »

Le terminal interroge le moteur avant d’ouvrir le conteneur. Deux situations donnent un refus, et dans les deux la commande n’est pas exécutée :

  • aucun moteur ne répond : le refus commence par No container engine answered et demande d’installer un moteur compatible Docker ;
  • le moteur n’applique pas de filtre d’appels système, ou son profil seccomp par défaut est désarmé : le refus commence par This container engine does not apply a system-call filter et demande de réactiver seccomp.

Le moteur interrogé est docker. Pour en nommer un autre, Podman par exemple, renseignez LEMNISCATE_CONTAINER_ENGINE avec le nom de son programme.

L’image de session fait partie des artefacts livrés. Comme les autres images de conteneurs, elle est signée, et elle vous parvient par l’un de deux chemins (livre blanc sécurité V3, sections 9.1 et 9.3) :

  • en mode connecté, elle est poussée dans votre registre privé ;
  • en mode isolé, elle est livrée sur média, dans l’archive signée, puis chargée dans le moteur de conteneurs par votre équipe.

Dans les deux cas, votre équipe vérifie la signature avec la clé que vous détenez avant de mettre l’image à disposition des postes. Aucun registre public et aucun dépôt de paquets externe n’intervient : le produit ne dépend d’aucun service externe (livre blanc sécurité V3, section 11.3).

Quand un projet a besoin d’un compilateur ou d’un outil de plus, votre équipe construit une image dérivée de l’image livrée et la publie dans votre registre privé. Les outils disponibles pour l’agent sont ceux de l’image : le conteneur n’a pas de réseau et n’installe rien.

Nommez l’image que la session utilise :

Fenêtre de terminal
export LEMNISCATE_SESSION_IMAGE=registre.exemple/outils-projet:1.4

Si le moteur ne trouve pas l’image, la session ne s’ouvre pas ; le message donne la cause rapportée par le moteur et nomme LEMNISCATE_SESSION_IMAGE.

L’image nommée est utilisée telle quelle. Elle doit fournir deux programmes :

  • /bin/bash, auquel chaque commande de l’agent est confiée. Le terminal le vérifie dès l’ouverture du conteneur. Dans une image sans bash, chaque commande est refusée avec un message qui nomme l’image, l’interprète attendu et LEMNISCATE_SESSION_IMAGE ; aucune commande n’est confiée à /bin/sh à la place, parce que l’analyse de sécurité de la commande raisonne en bash ;
  • /bin/sh, dont le produit se sert pour maintenir le conteneur en vie et interrompre les commandes.

Une commande qui sort avec le code 127 (commande introuvable) reçoit une note qui commence par [lemniscate] Command not found in the session image et nomme l’image, pour que la cause ne soit pas cherchée dans le profil de votre shell.

VariableDéfautFormat accepté
LEMNISCATE_SESSION_MEMORY2gun entier positif suivi d’une unité : 512m, 4g
LEMNISCATE_SESSION_CPUS2un nombre positif, décimal accepté : 1.5
LEMNISCATE_SESSION_PIDS512un entier positif ; pas de valeur « illimité »

La borne de mémoire inclut le swap. Une valeur d’un autre format, zéro compris, est refusée à l’ouverture avec un message qui nomme la variable et le format attendu. Les valeurs par défaut arrêtent un processus emballé avant qu’il n’atteigne le poste ; un projet dont la construction demande plus les relève.

Le conteneur monte le répertoire sur lequel lemni a été ouvert. Trois répertoires sont refusés comme racine de travail, avec un message qui dit pourquoi :

  • le dossier personnel du compte (HOME) : le monter donnerait à l’agent les clés SSH, les jetons d’accès aux services en ligne et les autres dépôts ;
  • la racine de la machine ;
  • un chemin relatif : l’endroit où l’agent travaille ne dépend pas du répertoire de qui a lancé le programme.

Dans les trois cas, ouvrez lemni sur le répertoire du projet.