L'environnement du shell
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 shell | Variable d'environnement | |
|---|---|---|
| Création | NOM=valeur | export NOM=valeur, ou export NOM sur une variable existante |
| Visible dans le shell courant | oui | oui |
| Transmise aux commandes lancées | non | oui, par copie |
| Lister | set (variables et fonctions) | env ou printenv |
| Usage typique | compteur, valeur temporaire dans un script | PATH, 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=...oucd /opt/signalementsne 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
~/.bashrcne 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,sourcesont 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 parPATH. - Bash mémorise les emplacements. Après avoir trouvé
lsune première fois, il garde son chemin dans une table de hachage (hashl'affiche) pour ne pas reparcourirPATHà chaque appel. Si vous installez une nouvelle version ailleurs, le shell peut continuer d'appeler l'ancienne :hash -rvide 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 à.. UnPATHmal construit (PATH="$PATH:$MON_REP"avecMON_REPvide) 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
| Variable | Rôle | Remarque |
|---|---|---|
HOME | répertoire personnel | ~ s'y développe |
USER, LOGNAME | nom de l'utilisateur | posées à la connexion ; ne pas s'y fier pour une décision de sécurité, elles se modifient comme n'importe quelle variable |
SHELL | shell de connexion de l'utilisateur | lu dans /etc/passwd, ce n'est pas forcément le shell en cours |
PWD, OLDPWD | répertoire courant et précédent | cd - utilise OLDPWD |
LANG, LC_*, LC_ALL | langue, format des dates, ordre de tri | voir ci-dessous |
TZ | fuseau horaire | ex. Europe/Paris |
EDITOR, VISUAL | éditeur à lancer | utilisé par crontab -e, git commit, visudo |
PS1 | invite du shell | variable 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 :
| Situation | Fichiers 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/profilelit/etc/bash.bashrcsi le shell est Bash et interactif, puis tous les fichiers/etc/profile.d/*.sh;- le
~/.profilecopié dans chaque nouveau compte (depuis/etc/skel) lit~/.bashrcsi le shell est Bash, puis ajoute~/binet~/.local/binàPATH; ~/.bashrccommence 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 service | aucun 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
PATHfixe,/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 ontUser=,HOME,LOGNAMEetSHELLde ce compte ;- les directives
Environment=etEnvironmentFile=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èssudoers(5)) lance la commande dans un environnement minimal :TERM,PATH,HOME,MAIL,SHELL,LOGNAME,USERet les variablesSUDO_*, plus celles qu'autorise explicitement la listeenv_keep.secure_pathremplace votrePATHpar 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
fiPré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" ;;
esacLe 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:appCe 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 contientDATABASE_URL. Ce fichier est en0640, propriété derootet du groupesignalements(leçon 9) : il n'apparaît pas dans l'unité, que tout le monde peut lire.ExecStart=donne un chemin complet : lePATHdu 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 :
- Bash cherche
ls(alias, fonction, commande interne, table de hachage, puisPATH) et trouve/usr/bin/ls. - Bash crée un processus enfant par
fork(2), qui est une copie de lui-même, environnement compris. - L'enfant appelle
execve("/usr/bin/ls", argv, envp), avec unenvpqui ne contient que les variables exportées. Le code de Bash est remplacé par celui dels. lslit les variables qui le concernent (LANG,LS_COLORS...) par la fonctiongetenv(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 etrootpeuvent le lire, les autres non. Le fichier appartient au propriétaire du processus, avec les droitsr--------.
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>/environpar le même compte et parroot, et s'affiche avecps 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
envlaissé 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, unPATHexplicite 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 -eet--env-filene font que construire l'envpdu processus principal. Les mêmes précautions sur les secrets s'appliquent, etdocker inspectles affiche en clair. - Uniformisez les sessions des administrateurs par
/etc/profile.d/plutôt que par des~/.bashrcpersonnels 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
doneread -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 avecsource. NOM=valeur commandedonne une variable à une seule commande ;env -ilance une commande dans un environnement vide.PATHest parcouru dans l'ordre, le premier exécutable trouvé gagne ;typeetcommand -vdisent ce qui s'exécutera ; un élément vide ou.ajoute le répertoire courant, ce qui est dangereux.LC_ALL=CetTZrendent les sorties stables et comparables.- Un shell de connexion lit
/etc/profileet~/.profile; un shell interactif lit~/.bashrc; un script ne lit rien ;/etc/environmentest 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 imposesecure_path: ne contournez pas ces protections. - L'environnement d'un processus est visible dans
/proc/<pid>/environpar son propriétaire etroot: 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 bashsur 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éellementsudo, et comment créer le comptesignalementssous lequel tourne le service.
Sources
- GNU Bash Reference Manual, 6.2 Bash Startup Files et 3.7.4 Environment
- POSIX.1-2024, Base Definitions, chapitre 8 : Environment Variables
- Linux man-pages, environ(7)
- Linux man-pages, execve(2)
- Linux man-pages, proc_pid_environ(5)
- systemd, systemd.exec(5) : Environment=, EnvironmentFile= et variables des processus lancés
- sudo, sudoers(5) : env_reset, env_keep, secure_path
- Linux-PAM, pam_env(8)
- William Shotts, The Linux Command Line, chapitre 11 : The Environment