Aller au contenu
L'environnement du shell

L'environnement du shell

200 Pratiquer ⏱ 1 h linuxbashubuntusystemd

À la fin, vous saurez

  • Distinguer une variable de shell d'une variable d'environnement, et prédire ce qu'un processus enfant reçoit
  • Expliquer pourquoi un script ne peut pas modifier l'environnement du shell qui l'a lancé, et utiliser source à bon escient
  • Diagnostiquer quelle commande le shell exécute réellement, à l'aide de PATH, type et command -v
  • Choisir le bon fichier de démarrage (/etc/environment, /etc/profile.d, ~/.profile, ~/.bashrc) pour une variable ou un alias
  • Fournir sa configuration à un service systemd et comprendre ce que sudo retire de l'environnement
  • Repérer les risques des secrets placés dans des variables d'environnement

Prérequis

Testé avec bash 5.2.21 coreutils 9.4 sudo 1.9.15p5 systemd 255 ubuntu 24.04 , vérifié le 5 octobre 2026

Pourquoi

Votre première semaine dans l'équipe Signalements, on vous demande de vérifier qu'une nouvelle version de l'API démarre bien sur sig-app-1. Vous vous connectez, vous lancez l'application à la main pour voir, et tout fonctionne : elle trouve sa base de données, elle écoute sur le port 8000, /sante répond. Rassuré, vous redémarrez le service. Il échoue aussitôt, avec un message qui dit que DATABASE_URL n'est pas définie.

La veille, une collègue a vécu la situation inverse : un script de maintenance fonctionnait sous son compte, mais lancé avec sudo, il ne trouvait plus une commande installée dans /opt/signalements/bin, avec un sobre command not found.

Ces deux incidents ont la même cause, et c'est l'une des plus fréquentes sur un serveur : l'environnement. Chaque processus Linux transporte avec lui un petit ensemble de variables (où sont les commandes, quelle est la langue, où est la base de données) qu'il a reçu de son parent au moment où il a été lancé. Votre shell interactif a construit le sien en lisant plusieurs fichiers de démarrage ; systemd et sudo construisent le leur autrement, délibérément, pour des raisons de sécurité et de reproductibilité. Tant que l'on ne sait pas d'où vient l'environnement d'un processus, on ne comprend pas pourquoi « ça marche chez moi ».

Cette leçon vous apprend à lire et à modifier l'environnement, à savoir qui l'hérite, à maîtriser la variable PATH, qui décide quelle commande s'exécute réellement, et à placer chaque réglage dans le bon fichier.

Les concepts

Deux sortes de variables

Le shell manipule des variables : un nom, un signe =, une valeur, sans espace autour du =.

$ COULEUR=bleu
$ echo "$COULEUR"
bleu

Cette variable est une variable de shell : elle existe dans le processus Bash où vous l'avez créée, et nulle part ailleurs. Pour qu'elle soit transmise aux programmes que ce shell lance, il faut la marquer pour l'exportation avec export. Elle devient alors une variable d'environnement (environment variable).

La distinction est le point central de la leçon. Un processus Linux possède un environnement : une liste de chaînes de la forme NOM=valeur, que le noyau lui remet au démarrage. La page de manuel environ(7) le résume : lorsqu'un processus est créé, il hérite d'une copie de l'environnement de son parent. Une copie, pas un lien : les modifications ultérieures, d'un côté comme de l'autre, ne se propagent pas.

Variable de shellVariable d'environnement
CréationNOM=valeurexport NOM=valeur, ou export NOM sur une variable existante
Visible dans le shell courantouioui
Transmise aux commandes lancéesnonoui, par copie
Listerset (variables et fonctions)env ou printenv
Usage typiquecompteur, valeur temporaire dans un scriptPATH, HOME, LANG, configuration d'une application

Par convention, les variables d'environnement s'écrivent en majuscules. Ce n'est pas qu'une habitude : la norme POSIX précise que les noms utilisés par les utilitaires standard ne contiennent que des majuscules, des chiffres et le tiret bas, et que les noms contenant des minuscules sont réservés aux applications. Dans vos propres scripts, des variables internes en minuscules ne risquent donc pas d'écraser une variable du système.

L'héritage ne va que dans un sens

Un processus ne peut jamais modifier l'environnement de son parent. Il reçoit une copie, la modifie s'il le veut, la transmet à ses propres enfants, et tout disparaît quand il se termine. Les conséquences sont importantes et souvent contre-intuitives :

  • un script qui fait export DATABASE_URL=... ou cd /opt/signalements ne change rien dans le terminal qui l'a lancé ;
  • une variable exportée dans votre session SSH n'existe pas pour le service systemd, qui n'est pas un descendant de votre shell ;
  • modifier ~/.bashrc ne change rien aux terminaux déjà ouverts.

Pour qu'un fichier de commandes modifie le shell courant, il ne faut pas l'exécuter dans un nouveau processus mais le lire dans le shell actuel, avec la commande interne source (ou son équivalent POSIX, le point .). C'est exactement ce que font les fichiers de démarrage, et ce que vous faites quand vous tapez source ~/.bashrc après l'avoir modifié.

La variable PATH

Quand vous tapez ls, rien ne dit au shell où se trouve ce programme. Le shell consulte la variable PATH : une liste de répertoires séparés par des deux-points, parcourus dans l'ordre. Le premier répertoire qui contient un fichier exécutable nommé ls gagne ; les suivants ne sont pas consultés. Un nom qui contient une barre oblique (./deploie.sh, /usr/bin/ls) ne passe pas par PATH : il désigne directement un fichier.

Sur Ubuntu 24.04, /etc/environment fixe une valeur de départ qui ressemble à ceci :

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"

L'ordre est voulu : /usr/local (logiciels installés localement par l'administrateur) passe avant /usr (logiciels de la distribution), pour qu'une version compilée sur place l'emporte sur celle du paquet. Le fichier ~/.profile fourni par Ubuntu ajoute ensuite ~/bin et ~/.local/bin au début, s'ils existent : vos propres outils passent avant tout le reste.

Trois détails comptent en pratique :

  • Toutes les commandes ne sont pas des fichiers. cd, echo, export, source sont des commandes internes (builtins) de Bash ; d'autres noms sont des alias ou des fonctions. Le shell cherche dans cet ordre : alias, fonctions, commandes internes, puis fichiers trouvés par PATH.
  • Bash mémorise les emplacements. Après avoir trouvé ls une première fois, il garde son chemin dans une table de hachage (hash l'affiche) pour ne pas reparcourir PATH à chaque appel. Si vous installez une nouvelle version ailleurs, le shell peut continuer d'appeler l'ancienne : hash -r vide la table.
  • Un élément vide désigne le répertoire courant. La norme POSIX le qualifie de fonctionnalité héritée : deux deux-points consécutifs, un deux-points au début ou à la fin de PATH équivalent à .. Un PATH mal construit (PATH="$PATH:$MON_REP" avec MON_REP vide) ajoute donc le répertoire courant sans que personne ne l'ait voulu. La section Sécurité explique pourquoi c'est dangereux.

Les variables que vous rencontrerez partout

VariableRôleRemarque
HOMErépertoire personnel~ s'y développe
USER, LOGNAMEnom de l'utilisateurposées à la connexion ; ne pas s'y fier pour une décision de sécurité, elles se modifient comme n'importe quelle variable
SHELLshell de connexion de l'utilisateurlu dans /etc/passwd, ce n'est pas forcément le shell en cours
PWD, OLDPWDrépertoire courant et précédentcd - utilise OLDPWD
LANG, LC_*, LC_ALLlangue, format des dates, ordre de trivoir ci-dessous
TZfuseau horaireex. Europe/Paris
EDITOR, VISUALéditeur à lancerutilisé par crontab -e, git commit, visudo
PS1invite du shellvariable de shell, rarement exportée

Les variables de localisation méritent une attention particulière, parce qu'elles changent la sortie des commandes. LANG fixe la valeur par défaut, chaque LC_* (LC_TIME, LC_COLLATE, LC_NUMERIC...) la remplace pour une catégorie, et LC_ALL les écrase toutes. Les messages d'erreur, le format des dates et même l'ordre de tri en dépendent. Un script qui analyse la sortie d'une commande doit fixer LC_ALL=C (ou C.UTF-8) pour obtenir une sortie stable, quelle que soit la langue du poste qui l'exécute.

Les fichiers de démarrage

Votre environnement interactif n'arrive pas tout fait : il est construit, à chaque ouverture de session, par une succession de fichiers. Deux notions décident lesquels sont lus. Le manuel de Bash les définit ainsi :

  • un shell de connexion (login shell) est celui dont le nom (argument zéro) commence par un tiret, ou qui est lancé avec l'option --login : c'est le shell que vous obtenez en vous connectant en SSH ou sur une console texte ;
  • un shell interactif lit ses commandes au clavier : il n'a reçu ni script ni option -c, et son entrée et sa sortie d'erreur sont reliées à un terminal.

Un terminal ouvert dans un bureau graphique est en général interactif mais pas de connexion. Un script lancé par bash script.sh n'est ni l'un ni l'autre. Les fichiers lus en découlent :

SituationFichiers lus par Bash, dans l'ordre
Shell de connexion interactif (SSH, console)/etc/profile, puis le premier qui existe parmi ~/.bash_profile, ~/.bash_login, ~/.profile
Shell interactif sans connexion (nouveau terminal, bash tapé à la main)/etc/bash.bashrc (ajout de Debian et Ubuntu), puis ~/.bashrc
Shell non interactif (script)aucun, sauf le fichier désigné par la variable BASH_ENV si elle existe
Fin d'un shell de connexion~/.bash_logout

Ce tableau seul expliquerait mal ce que vous observez sur Ubuntu, parce que les fichiers fournis par la distribution s'appellent les uns les autres :

  • /etc/profile lit /etc/bash.bashrc si le shell est Bash et interactif, puis tous les fichiers /etc/profile.d/*.sh ;
  • le ~/.profile copié dans chaque nouveau compte (depuis /etc/skel) lit ~/.bashrc si le shell est Bash, puis ajoute ~/bin et ~/.local/bin à PATH ;
  • ~/.bashrc commence par vérifier que le shell est interactif, et s'arrête immédiatement sinon.

Il existe enfin un fichier qui n'est pas lu par le shell : /etc/environment. Il est lu à l'ouverture de session par le module PAM pam_env, avant même que le shell démarre, pour les sessions qui passent par PAM (connexion en console, SSH, sudo). Sa syntaxe n'est pas celle du shell : des lignes NOM=valeur, sans export, sans $AUTRE_VARIABLE développée, sans commande.

D'où la règle de rangement :

Pour...Le bon endroit
une variable pour tous les utilisateurs, toutes sessions/etc/environment (valeurs simples) ou un fichier /etc/profile.d/<nom>.sh
une variable d'environnement pour vous seul~/.profile (lu une fois à la connexion, hérité ensuite)
un alias, une fonction, l'invite, les options du shell interactif~/.bashrc (lu par chaque shell interactif ; alias et fonctions ne s'héritent pas)
la configuration d'un serviceaucun de ces fichiers : l'unité systemd (voir plus bas)

Évitez de créer ~/.bash_profile sur Ubuntu : s'il existe, Bash ne lit plus ~/.profile, et toute la chaîne prévue par la distribution saute.

Alias et fonctions

Un alias remplace un mot par un texte, au début d'une commande : alias ll='ls -alF' (défini par le ~/.bashrc d'Ubuntu). Une fonction est un petit programme nommé, avec des arguments :

journal() {
  journalctl -u "${1:-signalements}" --since "${2:-1 hour ago}" --no-pager
}

journal affiche la dernière heure de journaux de Signalements ; journal nginx "10 min ago" celle d'un autre service. Alias et fonctions vivent dans le shell : ils ne sont pas transmis aux scripts ni aux autres programmes, d'où leur place dans ~/.bashrc. Et ils n'existent pas dans les scripts, ni sous sudo, ni dans un service : ne construisez jamais un script sur un alias.

L'environnement d'un service systemd

Un service n'est pas lancé par votre shell, mais par systemd, le gestionnaire de services (leçon 11). La page systemd.exec(5) le dit explicitement : les processus lancés par le gestionnaire système n'héritent pas de son environnement ; leur environnement est assemblé à partir de quelques sources précises. Parmi elles :

  • un PATH fixe, /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin ;
  • LANG, tiré de la configuration de localisation du système ;
  • USER, et pour les unités qui ont User=, HOME, LOGNAME et SHELL de ce compte ;
  • les directives Environment= et EnvironmentFile= de l'unité.

Rien de ce qui est dans votre ~/.bashrc, votre ~/.profile ou votre session SSH n'y entre. C'est la cause du « ça marche à la main mais pas en service » de l'introduction : vous aviez exporté DATABASE_URL dans votre terminal pour votre test, le service ne la reçoit que si l'unité la déclare.

EnvironmentFile= lit un fichier de lignes NOM=valeur. Sa syntaxe ressemble à celle du shell sans en être : pas de commande, pas de export à mettre, et les règles de guillemets sont détaillées dans systemd.exec(5). Un tiret devant le chemin (EnvironmentFile=-/etc/signalements/env) rend le fichier facultatif. Le fichier est lu par systemd, qui tourne en root, juste avant de lancer le processus : il peut donc être illisible pour le compte du service lui-même.

sudo et l'environnement

sudo exécute une commande avec l'identité d'un autre utilisateur, root le plus souvent (leçon 8). Lui laisser votre environnement serait une faille : il suffirait de placer un faux ls en tête de son PATH pour le faire exécuter par root. La configuration par défaut de sudo, dans /etc/sudoers sur Ubuntu, contient donc :

Defaults	env_reset
Defaults	secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"
  • env_reset (activé par défaut, d'après sudoers(5)) lance la commande dans un environnement minimal : TERM, PATH, HOME, MAIL, SHELL, LOGNAME, USER et les variables SUDO_*, plus celles qu'autorise explicitement la liste env_keep.
  • secure_path remplace votre PATH par une valeur sûre.

D'où le second incident de l'introduction : /opt/signalements/bin était dans le PATH de votre collègue, pas dans secure_path. La bonne correction n'est pas de désactiver ces protections, mais d'appeler la commande par son chemin complet (sudo /opt/signalements/bin/migrer), ou de l'installer dans un répertoire prévu pour cela.

En pratique

Les sorties de cette section ont été produites sur Ubuntu 24.04 avec Bash 5.2, dans un shell lancé avec un environnement réduit (env -i) pour que rien de personnel n'apparaisse ; le répertoire personnel y est /home/camille. Les messages sont ceux d'un système configuré en anglais, comme la plupart des serveurs.

Exporter, ou pas

$ COULEUR=bleu
$ bash -c 'echo "enfant : [$COULEUR]"'
enfant : []
$ export COULEUR
$ bash -c 'echo "enfant : [$COULEUR]"'
enfant : [bleu]
$ declare -p COULEUR
declare -x COULEUR="bleu"

bash -c '...' lance un nouveau processus Bash qui exécute la chaîne donnée : c'est un enfant. Tant que COULEUR n'est qu'une variable de shell, l'enfant ne la voit pas. Après export, il en reçoit une copie. declare -p montre les attributs d'une variable : -x signifie « exportée ».

On peut aussi donner une variable à une seule commande, sans la créer dans le shell :

$ VERSION=1.3.0 bash -c 'echo "VERSION=$VERSION"'
VERSION=1.3.0
$ echo "VERSION dans le shell : [${VERSION-non définie}]"
VERSION dans le shell : [non définie]

C'est la forme à préférer pour un réglage ponctuel : LC_ALL=C sort fichier, TZ=UTC date. Le shell ne garde aucune trace de la variable après la commande. La syntaxe ${VERSION-non définie} affiche un texte de remplacement si la variable n'existe pas.

Un script ne change pas son parent

Écrivez un petit script change.sh :

#!/bin/bash
export COULEUR=rouge
cd /tmp
echo "dans le script : $COULEUR, $PWD"

Exécutez-le, puis lisez-le avec source (ici sous sa forme .) :

$ chmod +x change.sh
$ ./change.sh
dans le script : rouge, /tmp
$ echo "après : $COULEUR, $PWD"
après : bleu, /home/camille
$ . ./change.sh
dans le script : rouge, /tmp
$ echo "après source : $COULEUR, $PWD"
après source : rouge, /tmp

Exécuté, le script tourne dans un processus enfant : son export et son cd meurent avec lui. Lu avec ., il s'exécute dans le shell courant, qui change de couleur et de répertoire. Réservez source aux fichiers conçus pour cela (fichiers de configuration, fonctions) : un script lu ainsi qui fait exit ferme votre terminal.

Lister l'environnement

$ env | sort
COULEUR=rouge
HOME=/home/camille
OLDPWD=/tmp
PATH=/usr/local/bin:/usr/bin:/bin
PWD=/home/camille
SHLVL=1
TERM=xterm
_=/usr/bin/env

env sans argument affiche l'environnement qu'il a reçu, c'est-à-dire les variables exportées par le shell. printenv NOM affiche une seule valeur. set, à l'inverse, affiche toutes les variables du shell, exportées ou non, et les fonctions : la liste est longue. Deux variables ici sont posées par Bash lui-même : SHLVL compte les shells imbriqués, _ contient le dernier argument de la commande précédente (ici le chemin d'env).

env sait aussi lancer une commande dans un environnement modifié. env -i commande part d'un environnement vide : c'est ce qui a servi à produire les sorties de cette leçon, et c'est un excellent moyen de reproduire ce que voit un service, qui démarre lui aussi avec un environnement presque vide.

Savoir quelle commande s'exécute

$ type ls cd echo
ls is /usr/bin/ls
cd is a shell builtin
echo is a shell builtin
$ command -v python3 ls cd
/usr/bin/python3
/usr/bin/ls
cd

type dit ce que le shell exécutera pour un nom : un fichier (avec son chemin), une commande interne, un alias ou une fonction. Dans votre terminal Ubuntu habituel, type ls répond ls is aliased to 'ls --color=auto', parce que ~/.bashrc définit cet alias ; dans un script, l'alias n'existe pas. command -v donne une réponse courte, utilisable dans un script :

if ! command -v jq >/dev/null; then
  echo "jq est requis" >&2
  exit 1
fi

Préférez command -v à which, qui est un programme externe et ne connaît ni les commandes internes, ni les alias, ni les fonctions.

L'ordre de PATH, démontré

Créez un répertoire bin contenant un faux ls, et placez-le en tête de PATH pour une seule commande :

$ mkdir -p bin
$ printf '#!/bin/sh\necho "faux ls !"\n' > bin/ls && chmod +x bin/ls
$ PATH="$PWD/bin:$PATH" ls
faux ls !
$ PATH="$PWD/bin:$PATH" command -v ls
/home/camille/bin/ls

Le premier ls trouvé gagne. C'est utile (votre version d'un outil passe avant celle du système) et dangereux (n'importe quel répertoire inscriptible placé avant /usr/bin permet de détourner une commande).

L'élément vide est plus sournois. Placez dans le répertoire courant un script nommé gti, la faute de frappe classique pour git :

$ printf '#!/bin/sh\necho "exécuté depuis le répertoire courant !"\n' > gti && chmod +x gti
$ PATH=/usr/bin:/bin
$ gti
bash: line 1: gti: command not found
$ PATH="$PATH:"
$ gti
exécuté depuis le répertoire courant !
$ command -v gti
./gti

Un seul deux-points final, et le répertoire courant fait désormais partie de la recherche.

Ajouter un répertoire à PATH proprement

Pour ajouter /opt/signalements/bin à votre PATH de façon durable, ajoutez à la fin de ~/.profile :

case ":$PATH:" in
  *:/opt/signalements/bin:*) ;;
  *) PATH="$PATH:/opt/signalements/bin" ;;
esac

Le case évite d'ajouter le répertoire deux fois si le fichier est relu. Le répertoire est ajouté à la fin : les outils de l'application ne masqueront jamais une commande du système. ~/.profile n'est lu qu'à la connexion : déconnectez-vous et reconnectez-vous, ou faites source ~/.profile dans la session courante.

Pour tous les utilisateurs, déposez plutôt un fichier /etc/profile.d/signalements.sh contenant les mêmes lignes (sudo requis). Il sera lu par /etc/profile à chaque connexion.

Des sorties stables avec LC_ALL

$ printf 'b\nB\na\nA\né\ne\n' > tri.txt
$ LC_ALL=fr_FR.UTF-8 sort tri.txt | tr '\n' ' '
a A b B e é
$ LC_ALL=C sort tri.txt | tr '\n' ' '
A B a b e é
$ LC_ALL=fr_FR.UTF-8 date -d 2026-10-05T14:00
lun. 05 oct. 2026 14:00:00 CEST
$ LC_ALL=C date -d 2026-10-05T14:00
Mon Oct  5 14:00:00 CEST 2026

Le même fichier se trie différemment : en français, l'ordre suit l'alphabet (minuscule et majuscule côte à côte) ; en locale C, il suit les codes des octets (toutes les majuscules avant toutes les minuscules). Un script qui compare deux listes triées, ou qui découpe une date, doit fixer la locale, sinon il change de comportement selon la machine. locale affiche les réglages en cours, locale -a les locales installées.

Le fuseau horaire se règle de la même façon, par commande :

$ TZ=Europe/Paris date -d 2026-10-05T12:00Z
lun. 05 oct. 2026 14:00:00 CEST

Configurer le service Signalements

Revenons au premier incident. L'unité du service, /etc/systemd/system/signalements.service, doit déclarer sa configuration elle-même :

[Service]
User=signalements
WorkingDirectory=/opt/signalements
Environment=APP_VERSION=1.3.0
EnvironmentFile=/etc/signalements/env
ExecStart=/opt/signalements/venv/bin/gunicorn --bind 127.0.0.1:8000 --workers 2 app:app

Ce n'est qu'un extrait de la section [Service] ; l'unité complète est expliquée à la leçon 11.

  • Environment= pour une valeur non sensible, écrite directement dans l'unité.
  • EnvironmentFile= pour le fichier qui contient DATABASE_URL. Ce fichier est en 0640, propriété de root et du groupe signalements (leçon 9) : il n'apparaît pas dans l'unité, que tout le monde peut lire.
  • ExecStart= donne un chemin complet : le PATH du service est fixe, ne comptez pas dessus pour trouver un exécutable dans un environnement virtuel Python.

Après toute modification d'une unité : sudo systemctl daemon-reload, puis sudo systemctl restart signalements. Pour voir l'environnement que systemd donnera au service :

$ systemctl show signalements -p Environment -p EnvironmentFiles

La commande affiche les variables d'Environment= et la liste des fichiers d'EnvironmentFile= (sans leur contenu). Pour voir l'environnement réel du processus en marche, il faut lire /proc (section suivante), ce qui demande les droits de root pour un processus d'un autre compte.

sudo, l'environnement et le PATH

$ sudo env | sort
$ sudo -V

La première commande montre l'environnement réduit que reçoit une commande lancée par sudo : comparez-le à env. La seconde, lancée par root (donc sudo sudo -V), affiche toute la configuration, dont les listes env_keep et env_check. Si un script a besoin d'une variable particulière sous sudo, passez-la explicitement (sudo LOG_LEVEL=debug /opt/signalements/bin/migrer), ce que la politique de sudo autorise ou refuse selon sa configuration, plutôt que d'utiliser sudo -E, qui demande de conserver tout votre environnement.

Sous le capot

execve et le tableau envp

Au niveau du noyau, un programme se lance par l'appel système execve(2), qui prend trois arguments : le chemin du fichier à exécuter, le tableau des arguments (argv) et le tableau de l'environnement (envp). Ce troisième argument, une simple liste de chaînes NOM=valeur terminée par un pointeur nul, est tout ce que le programme reçoit. Le noyau ne sait rien des variables de shell, de export ni des fichiers de démarrage : c'est le shell qui, au moment de lancer une commande, construit envp à partir de ses variables marquées pour l'exportation.

La séquence complète, quand vous tapez ls :

  1. Bash cherche ls (alias, fonction, commande interne, table de hachage, puis PATH) et trouve /usr/bin/ls.
  2. Bash crée un processus enfant par fork(2), qui est une copie de lui-même, environnement compris.
  3. L'enfant appelle execve("/usr/bin/ls", argv, envp), avec un envp qui ne contient que les variables exportées. Le code de Bash est remplacé par celui de ls.
  4. ls lit les variables qui le concernent (LANG, LS_COLORS...) par la fonction getenv(3) de la bibliothèque C.

Le parent n'est jamais modifié : c'est la raison mécanique de la règle « l'héritage ne va que dans un sens ».

/proc//environ

Le noyau conserve l'environnement initial de chaque processus et l'expose dans le pseudo-fichier /proc/<pid>/environ, sous forme de chaînes séparées par des octets nuls. Lancez un processus avec une variable, et lisez-le :

$ SECRET=s3cr3t sleep 30 &
$ tr '\0' '\n' < /proc/$!/environ
SECRET=s3cr3t
PWD=/home/camille
HOME=/home/camille
SHLVL=0
PATH=/usr/local/bin:/usr/bin:/bin
_=/usr/bin/sleep

$! contient le numéro (PID) du dernier processus lancé en arrière-plan. Deux précisions de proc_pid_environ(5) :

  • le fichier contient l'environnement au moment de l'execve ; si le programme modifie ensuite son environnement, le fichier ne le montre pas ;
  • l'accès est contrôlé comme pour le débogage d'un processus (ptrace) : en pratique, le même utilisateur et root peuvent le lire, les autres non. Le fichier appartient au propriétaire du processus, avec les droits r--------.

C'est par ce fichier que ps e affiche l'environnement de vos processus à la suite de leur ligne de commande :

$ ps -o pid,args e -p $!
    PID COMMAND
 592383 sleep 30 SECRET=s3cr3t PWD=/home/camille HOME=/home/camille SHLVL=0 PATH=/us

Pièges courants

NOM = valeur avec des espaces. Bash comprend alors « lancer la commande NOM avec les arguments = et valeur », et répond NOM: command not found. Pas d'espace autour du =.

Oublier export. La variable existe dans votre shell, echo l'affiche, mais le programme que vous lancez ne la voit pas. declare -p NOM dit si elle est exportée (-x).

Modifier ~/.bashrc et s'étonner que rien ne change. Le fichier n'est lu qu'au démarrage d'un shell interactif. source ~/.bashrc, ou ouvrez un nouveau terminal. Pour ~/.profile, reconnectez-vous.

Créer ~/.bash_profile sur Ubuntu. ~/.profile cesse d'être lu, et avec lui l'ajout de ~/.local/bin à PATH et la lecture de ~/.bashrc dans les sessions SSH. Si un outil l'a créé, faites-lui lire ~/.profile (. ~/.profile en première ligne) ou supprimez-le.

Mettre une variable d'environnement dans ~/.bashrc pour un programme graphique ou un service. Ni l'un ni l'autre ne lit ce fichier. Les services se configurent dans leur unité, les sessions dans ~/.profile ou /etc/environment.

Un PATH remplacé au lieu d'être complété. PATH=/opt/signalements/bin (sans $PATH) laisse un shell où plus aucune commande n'est trouvée : ls: command not found. Réparez dans la session avec PATH=/usr/local/bin:/usr/bin:/bin, puis corrigez le fichier fautif.

« command not found » sous sudo, mais pas sans. C'est secure_path. Appelez la commande par son chemin complet.

Une ancienne version qui continue de s'exécuter. Vous avez installé un outil dans /usr/local/bin, mais le shell appelle toujours /usr/bin/outil : il a mémorisé le chemin. hash -r, ou type outil pour vérifier.

Une sortie qui change d'une machine à l'autre. Tri, dates, séparateurs décimaux, messages d'erreur : tout dépend de la locale. Dans un script qui analyse une sortie, export LC_ALL=C.UTF-8 en tête.

Sécurité

Le répertoire courant dans PATH est une porte ouverte. Si . (ou un élément vide) figure dans le PATH de root, il suffit qu'un utilisateur dépose un exécutable nommé ls ou gti dans /tmp, et attende qu'un administrateur y tape la commande. C'est la raison pour laquelle aucune distribution ne le met dans PATH et pour laquelle sudo impose secure_path. Vérifiez le vôtre : echo "$PATH" | tr ':' '\n' ne doit montrer ni ., ni ligne vide, ni répertoire inscriptible par d'autres que vous placé avant les répertoires du système.

Les secrets dans des variables d'environnement fuient facilement. C'est la façon la plus répandue de passer DATABASE_URL à une application, et elle a des limites à connaître :

  • l'environnement est lisible dans /proc/<pid>/environ par le même compte et par root, et s'affiche avec ps e ;
  • il est hérité par tous les processus enfants, y compris un outil tiers que l'application lancerait ;
  • il finit souvent dans les journaux : un message d'erreur qui affiche l'environnement, une page de débogage, un env laissé dans un script de CI ;
  • la documentation de systemd l'écrit sans détour : les variables d'environnement ne conviennent pas pour transmettre des secrets à un service, parce que celles d'une unité sont exposées aux clients non privilégiés par son interface D-Bus. Elle recommande ses credentials (LoadCredential=), que le cours Linux : administration système présentera.

EnvironmentFile= vaut mieux qu'Environment= pour un secret, puisque la valeur n'est pas dans l'unité et que le fichier peut être protégé ; mais une fois dans le processus, la valeur reste une variable d'environnement. Le cours Gestion des secrets traite les solutions plus robustes.

Ne demandez pas à sudo de garder votre environnement. sudo -E ou une liste env_keep trop large transmettent à root des variables qui changent le comportement des programmes. Certaines sont redoutables : LD_PRELOAD demande à l'éditeur de liens dynamique de charger une bibliothèque de votre choix dans tout programme lancé, avant les autres ; PYTHONPATH ou PERL5LIB changent le code chargé par les interpréteurs. sudo les filtre par défaut ; ne défaites pas ce filtrage.

USER ne prouve rien. N'importe quel processus peut fixer USER=root. Pour savoir qui l'on est, id -u interroge le noyau (leçon 8).

En production

  • Toute la configuration d'un service est dans son unité, ou dans les fichiers qu'elle désigne, versionnés et déployés par un outil de gestion de configuration (cours Ansible : les fondamentaux). Rien ne doit dépendre de la session de la personne qui a installé la machine.
  • Les scripts d'exploitation fixent leur environnement en tête : set -euo pipefail (cours Bash pour l'automatisation), export LC_ALL=C.UTF-8, un PATH explicite si le script tourne sous cron ou sous systemd, et des chemins complets pour les outils propres à l'application.
  • Les conteneurs reprennent le même modèle : dans le cours Docker : les fondamentaux, docker run -e et --env-file ne font que construire l'envp du processus principal. Les mêmes précautions sur les secrets s'appliquent, et docker inspect les affiche en clair.
  • Uniformisez les sessions des administrateurs par /etc/profile.d/ plutôt que par des ~/.bashrc personnels recopiés à la main : un fichier, déployé partout, versionné.
  • Chez Lyneko, les applications tournent dans Kubernetes, où la configuration non sensible arrive en variables d'environnement et les secrets par des objets dédiés, synchronisés depuis le gestionnaire de secrets de Scaleway (voir GitOps avec Argo CD, leçon 8). Le principe est le même que sur sig-app-1 : l'environnement d'un processus est déclaré, pas hérité d'une session.

Exercices

1. Prédire l'héritage (niveau 100). Sans les exécuter, dites ce qu'affiche chaque ligne echo, puis vérifiez.

A=1
export B=2
bash -c 'echo "1: $A $B"; export C=3'
echo "2: $A $B $C"
A=4 bash -c 'echo "3: $A"'
echo "4: $A"
Solution

1: 2 (A n'est pas exportée, l'enfant ne la voit pas ; B l'est). 2: 1 2 (le export C=3 de l'enfant est mort avec lui). 3: 4 (une affectation devant une commande place la variable dans l'environnement de cette commande seulement). 4: 1 (le shell n'a pas été modifié par la ligne précédente).

2. Le service qui ne trouve pas sa base (niveau 200). Un collègue a ajouté export DATABASE_URL=postgresql://... à la fin de son ~/.bashrc sur sig-app-1, puis a redémarré le service, qui échoue toujours. Expliquez pourquoi, et proposez la correction complète, commandes comprises, en protégeant le mot de passe.

Solution

~/.bashrc n'est lu que par les shells interactifs de ce collègue ; le service est lancé par systemd, qui ne lit aucun fichier de démarrage du shell et ne transmet pas l'environnement de la session. Correction : retirer la ligne de ~/.bashrc (le secret n'a rien à y faire), écrire DATABASE_URL=... dans /etc/signalements/env, en 0640, propriétaire root, groupe signalements (sudo install -m 0640 -o root -g signalements /dev/null /etc/signalements/env, puis sudoedit /etc/signalements/env), ajouter EnvironmentFile=/etc/signalements/env dans la section [Service] de l'unité (sudo systemctl edit signalements crée un fichier de surcharge propre), puis sudo systemctl restart signalements et vérifier /sante. systemctl edit recharge la configuration de systemd tout seul ; après une modification directe du fichier d'unité, il faut sudo systemctl daemon-reload.

3. Auditer un PATH (niveau 200). Écrivez une commande ou un petit script qui, pour la valeur actuelle de PATH, affiche chaque répertoire sur une ligne et signale : un élément vide ou ., un répertoire qui n'existe pas, un répertoire inscriptible par le groupe ou par tous.

Solution
#!/bin/bash
IFS=: read -r -a reps <<< "$PATH:"   # le ':' final garde un éventuel élément vide en fin de liste
for r in "${reps[@]}"; do
  if [ -z "$r" ] || [ "$r" = "." ]; then
    echo "DANGER  élément vide ou '.' : répertoire courant"
  elif [ ! -d "$r" ]; then
    echo "absent  $r"
  elif [ -n "$(find "$r" -maxdepth 0 -perm /022)" ]; then
    echo "DANGER  $r est inscriptible par le groupe ou par tous"
  else
    echo "ok      $r"
  fi
done

read -a découpe la chaîne sur le séparateur IFS dans un tableau. find -maxdepth 0 -perm /022 teste le répertoire lui-même : /022 signifie « au moins un des bits d'écriture du groupe ou des autres est positionné » (leçon 9). L'ajout du : final compense le fait que read ignore un dernier champ vide. Vous pourriez compléter en vérifiant aussi le propriétaire de chaque répertoire.

4. Sous sudo (niveau 200). Sur votre machine d'essai, comparez env | sort et sudo env | sort. Quelles variables disparaissent, lesquelles changent ? Retrouvez dans sudoers(5) l'option responsable de chaque différence.

Solution

La plupart des variables disparaissent (LANG peut subsister, réintroduite par pam_env depuis /etc/default/locale, ou par la liste env_keep selon la configuration). PATH prend la valeur de secure_path. HOME, USER, LOGNAME, SHELL et MAIL désignent root (sur Ubuntu, HOME vaut /root sous sudo). Apparaissent SUDO_USER, SUDO_UID, SUDO_GID et SUDO_COMMAND, qui disent qui a lancé la commande. Options responsables : env_reset pour le tri, env_keep et env_check pour les exceptions, secure_path pour PATH.

Récapitulatif

  • Une variable de shell reste dans le shell ; une variable d'environnement (export) est copiée dans chaque processus lancé. L'héritage ne va que du parent vers l'enfant : un script ne modifie jamais son parent, sauf s'il est lu avec source.
  • NOM=valeur commande donne une variable à une seule commande ; env -i lance une commande dans un environnement vide.
  • PATH est parcouru dans l'ordre, le premier exécutable trouvé gagne ; type et command -v disent ce qui s'exécutera ; un élément vide ou . ajoute le répertoire courant, ce qui est dangereux.
  • LC_ALL=C et TZ rendent les sorties stables et comparables.
  • Un shell de connexion lit /etc/profile et ~/.profile ; un shell interactif lit ~/.bashrc ; un script ne lit rien ; /etc/environment est lu par PAM. Variables dans ~/.profile, alias et fonctions dans ~/.bashrc.
  • Un service systemd ne voit rien de votre session : Environment=, EnvironmentFile=, chemins complets.
  • sudo réinitialise l'environnement (env_reset) et impose secure_path : ne contournez pas ces protections.
  • L'environnement d'un processus est visible dans /proc/<pid>/environ par son propriétaire et root : un secret qui y vit peut fuir.

Pour aller plus loin

  • La section Bash Startup Files du manuel de Bash, courte et précise, et le paragraphe INVOCATION de man bash sur votre système (qui mentionne /etc/bash.bashrc, ajout propre à Debian).
  • man 7 environ, pour la liste des variables usuelles et leur rôle.
  • La section Environment variables in spawned processes de systemd.exec(5), pour savoir exactement ce que reçoit un service.
  • Le chapitre 11 de The Linux Command Line de William Shotts, disponible librement, pour une autre présentation des fichiers de démarrage.
  • La leçon suivante, qui explique qui est root, ce que fait réellement sudo, et comment créer le compte signalements sous lequel tourne le service.
Voir ma constellation →

Sources