- Alors que les logiciels ont évolué à grande vitesse, le système de variables d’environnement des systèmes d’exploitation conserve encore une structure vieille de plusieurs décennies
- Les variables d’environnement prennent la forme d’un dictionnaire global de chaînes, une structure simple sans espace de noms ni typage
- Sous Linux, les variables d’environnement sont transmises du processus parent à l’enfant via l’appel système
execve
- Bash, glibc, Python, etc. gèrent chacun les variables d’environnement sous forme de table de hachage, tableau ou enveloppe de dictionnaire
- La norme POSIX n’impose pas que les noms soient uniquement en majuscules et applique en pratique des règles souples, avec notamment l’usage de noms en minuscules recommandé dans certains cas
Que sont les variables d’environnement ?
- Même si les langages de programmation ont évolué rapidement, l’infrastructure d’exécution des processus fournie par les systèmes d’exploitation — en particulier la partie variables d’environnement — a très peu changé
- Lorsqu’il faut transmettre des paramètres d’exécution au lancement d’une application sans fichier dédié ni IPC, on se retrouve en pratique à devoir utiliser une interface fondée sur les variables d’environnement
- Les variables d’environnement jouent le rôle d’un dictionnaire plat de chaînes, sans espace de noms ni types
Structure de création et de transmission des variables d’environnement
- Les variables d’environnement sont une méthode traditionnelle de transmission de valeurs entre processus ; elles sont transmises lorsque le processus parent lance le processus enfant
- Autrement dit, elles sont héritées du processus parent par le processus enfant
- Sous Linux, l’appel système
execve reçoit comme arguments le binaire à exécuter, les arguments, ainsi que le tableau des variables d’environnement (envp)
- Exemple de commande exécutée :
ls -lah
- filename:
/usr/bin/ls
- argv:
['ls', '-lah']
- envp:
['PATH=...','USER=...']
- Le processus parent peut transmettre tel quel l’environnement existant au processus enfant, ou construire un environnement entièrement nouveau
- Presque tous les outils (Bash,
subprocess.run de Python, execl de la bibliothèque C, etc.) transmettent les variables d’environnement telles quelles
- À l’inverse, certains outils comme
login construisent un nouvel environnement
Emplacement de stockage et traitement interne des variables d’environnement
- Au démarrage d’un programme, le noyau stocke les variables d’environnement sur la pile sous forme de chaînes null-terminated
- Ces données sont difficiles à modifier directement par le programme ; elles sont donc généralement copiées et gérées dans une structure interne propre au programme
- Méthode de stockage des variables d’environnement selon les langages et shells
- Bash : gestion via une table de hachage (dictionnaire) de structure empilée
- Création d’une map de portée locale à chaque appel de fonction
- Seules les variables marquées
export sont transmises au processus enfant
- Les variables déclarées avec
local peuvent aussi être transmises au processus enfant via export
- Exemple :
export PATH permet de répercuter une modification locale vers l’enfant sans affecter la portée globale
- glibc (bibliothèque C) : gestion de
environ, structure en tableau dynamique, via putenv et getenv
- Avec une structure en tableau, la recherche comme la modification ont toutes deux une complexité temporelle linéaire
- Ce n’est donc pas adapté à un usage comme stockage de données demandant de fortes performances
- Python : exposées en interne via
os.environ comme un dictionnaire, mais en réalité connectées au tableau environ de la bibliothèque C
- Lorsqu’une valeur de
os.environ est modifiée, os.putenv est appelé, ce qui répercute le changement dans la bibliothèque C
- L’inverse n’est pas synchronisé, d’où une relation à sens unique
Format des variables d’environnement et plage autorisée
- Le noyau Linux et glibc sont très permissifs sur le format des variables d’environnement
- Il peut exister plusieurs valeurs en doublon pour un même nom
- Un enregistrement sans
= est également possible, et il n’y a pas de restriction sur les caractères spéciaux comme les emoji
- Limites de taille disponibles
- Variable individuelle : 128 KiB (en général sur un environnement x64)
- Total cumulé : 2 MiB (partagé avec les arguments de ligne de commande)
- Les variables d’environnement sont limitées à un quart de l’espace de pile
Particularités et cas limites des variables d’environnement
- Bash, face à des variables d’environnement étranges (doublons, entrées sans
=, etc.), supprime les noms dupliqués et ignore les entrées anormales
- Si un nom de variable contient des espaces, Bash ne peut pas le référencer, mais il peut tout de même le transmettre au processus enfant
- Par exemple, Nushell ou Python peuvent créer des variables dont le nom contient des espaces
- Bash stocke ce type d’entrée dans une table de hachage distincte (
invalid_env)
Règles standard de format et de nommage des variables d’environnement
- La norme POSIX reconnaît comme variable tout nom ne contenant pas de signe égal (
=)
- Recommandation officielle : le nom ne devrait contenir que des majuscules, des chiffres et des underscores (sans chiffre en première position)
- Les variables en minuscules sont destinées à un espace de noms réservé aux applications
- Les outils standard n’utilisent que les majuscules, mais l’usage de variables en minuscules est également autorisé
- En pratique, les développeurs utilisent surtout la convention ALL_UPPERCASE
- Règle recommandée : utiliser pour les noms de variables l’expression régulière
^[A-Z_][A-Z0-9_]*$, et pour les valeurs l’UTF-8
- En cas d’exception ou d’inquiétude de compatibilité, il est recommandé d’utiliser le Portable Character Set (ASCII) de POSIX
Conclusion
- Les variables d’environnement restent une interface vieillissante mais indispensable, servant de frontière entre le système d’exploitation et les applications
- Malgré leurs limites structurelles, Bash, C, Python, etc. continuent de les exploiter en les enveloppant chacun à leur manière
- Dans les systèmes modernes, le besoin de modes de gestion de configuration plus explicites, avec espace de noms clair et système de types, se fait de plus en plus sentir
2 commentaires
Cela semblait perdre un peu de son importance à première vue, mais avec l'arrivée de Docker et du cloud, c'est redevenu inévitable.
Commentaires sur Hacker News
Je travaille comme SRE/sysadmin/DevOps/autre, et même si l’article de blog ne parlait que de façon assez simple de la standardisation des variables d’environnement, je voudrais souligner que les alternatives provoquent elles aussi une frustration comparable, surtout quand des secrets entrent en jeu
Une architecture dans laquelle l’application accède à un coffre à secrets spécifique comme Hashicorp Vault/OpenBao/Secrets Manager entraîne vite un verrouillage fournisseur sérieux, et le remplacement devient très difficile jusque dans les bibliothèques
La disponibilité de Vault devient alors critique, et les équipes d’exploitation se retrouvent dans une situation très délicate lorsqu’il faut faire une mise à niveau ou de la maintenance
Quand on transmet les secrets via un fichier de config, on se retrouve aussi embarrassé quant à la manière de les y placer, car les fichiers de config se trouvent souvent sur des chemins publics
Au final, on finit par dépendre soit d’un système « privilégié qui remplace les valeurs via un template avant de les transmettre à l’app », soit du fait de « stocker tout le fichier de config dans le coffre à secrets pour le transmettre à l’app »
Le templating est propice aux erreurs, et déplacer l’intégralité du fichier de config vers un coffre à secrets est aussi source de stress, car quelqu’un peut l’uploader de travers
Aujourd’hui, la plupart des systèmes tournent sur des conteneurs, et sauf dans les entreprises très rigoureuses sur l’infra, les fichiers de config se retrouvent toujours à des endroits improbables, ce qui rend le montage encore plus confus et multiplie les erreurs
Quel que soit le format utilisé, JSON/YAML/TOML, les bugs particuliers sont monnaie courante, comme par exemple le problème Norway de YAML
J’ai déjà vu des approches où les secrets sont fournis via l’API Kubernetes Secrets, mais là aussi on se heurte à un fort verrouillage fournisseur
À moins de concevoir spécialement un système de type operator, je ne recommande pas activement cette méthode
J’ai aussi vu des problèmes liés à la définition de variables d’environnement via des subprocess, mais j’ai l’impression qu’aujourd’hui les équipes préfèrent des systèmes fondés sur un bus de messages, plus robustes et capables de s’étendre indépendamment
Dans notre équipe, nous avions créé une petite bibliothèque générique de gestion des secrets, sur laquelle on branchait simplement en plugin des backends spécifiques à chaque fournisseur comme AWS Secrets Manager
Avec un cache local configurable et des options pour contourner le cache selon les paramètres, toute la logique réellement dépendante du fournisseur restait cantonnée au backend, ce qui permettait de garder la bibliothèque et l’application indépendantes du fournisseur
Lors du passage à Vault, il a suffi d’ajouter un backend et de changer la configuration, et tout s’est appliqué sans incident
Je me demande pourquoi l’API Kubernetes Secret est perçue comme un problème de verrouillage fournisseur
Est-ce que c’était parce que vous vouliez utiliser les deployment yaml à autre chose qu’à un déploiement Kubernetes ?
Pour la plupart des applications, on peut monter le secret dans le conteneur puis l’injecter dans l’application sous forme de variable d’environnement ou de fichier json, ce qui permet de le lire et de l’écrire indépendamment de l’environnement
Il me semble aussi que le chiffrement du backend etcd peut être configuré avec KMS
J’ai du mal à comprendre en quoi recevoir des secrets via l’API Kubernetes Secrets constitue un verrouillage
Fondamentalement, les secrets K8s ne sont pas stockés chiffrés, donc à mon avis cela n’a de sens que si l’on (0) utilise bien K8s, (1) a mis en place le chiffrement côté control plane, et (2) utilise obligatoirement une solution supplémentaire comme un pilote CSI
Et le Secret Store CSI Driver prend en charge plusieurs backends, comme Conjur, donc c’est plutôt l’inverse d’un verrouillage
Pour ces raisons, nous continuons à utiliser surtout les env vars et dotenv pour la configuration
Une structure de configuration basée sur les variables d’environnement est extrêmement simple et compatible avec divers outils, y compris les gestionnaires de secrets
Ces dernières années, j’ai commencé à m’intéresser un peu à sOps basé sur YAML
YAML est vraiment intuitif pour représenter la structure de configuration d’une application, et avec sops il est facile de chiffrer et gérer seulement certaines parties
La gestion des clés GPG reste toutefois délicate, mais on peut la résoudre avec Vault ou OpenBao
Cela dit, cela réintroduit encore une fois la question du verrouillage fournisseur, même si OpenBao semble un peu moins concerné
On peut aussi récupérer les variables d’environnement à partir du résultat d’une commande, ce qui permet de traiter ça sans verrouillage fournisseur et sans étape de templating
Autre fait intéressant :
setenv()est fondamentalement cassé dans POSIX, au point que je pense qu’il ne faut jamais l’utiliser dans du code de bibliothèqueMême dans le code applicatif, cela doit rester un dernier recours, et uniquement avant la création de threads
getenv()renvoie directement le pointeur d’origine de la variable d’environnement, donc quandsetenv()remplace une variable, il n’y a aucun mécanisme de protectionIl faut être extrêmement prudent
À mon sens, la bonne façon de définir correctement les variables d’environnement est de les configurer via
execve()Cette méthode n’est adaptée que lorsqu’on transmet des informations par variables d’environnement juste avant ou juste après
exec()Je ne comprends pas pourquoi on voudrait utiliser setenv dans du code de bibliothèque
Solaris a résolu ce problème, mais Linux persiste encore avec la même approche
NetBSD propose depuis longtemps une alternative sûre appelée
getenv_r(), et FreeBSD l’a récemment adoptéemacOS suivra probablement bientôt
Il y a déjà eu des tentatives pour l’ajouter à glibc ou POSIX, mais elles ont été rejetées
J’espère qu’une fois diffusé sur plusieurs plateformes, cela finira par être officiellement accepté
Documentation NetBSD de getenv_r
Commit FreeBSD
Les variables d’environnement sont souvent utilisées pour transmettre des secrets, mais je ne pense pas que ce soit une très bonne pratique
Sous Linux, tous les processus exécutés par le même utilisateur peuvent inspecter les variables d’environnement les uns des autres
Quel que soit le modèle de menace retenu, c’est préoccupant, surtout sur les machines des développeurs où énormément de processus tournent sous le même utilisateur
Ce problème devient encore plus grave avec des éléments comme les agents LLM, quand de nombreux processus se promènent en dehors des conteneurs
En outre, les variables d’environnement sont généralement héritées telles quelles par les processus enfants, ce qui tend à exposer sans discernement les secrets même lorsqu’un seul processus en a réellement besoin
systemdexpose les variables d’environnement à tous les clients système via DBUS, et sa documentation officielle avertit de ne pas y stocker de secretsSi c’est vrai, cela voudrait dire que des variables d’environnement définies dans une unité réservée à root pourraient être visibles par des utilisateurs ordinaires, ce qui serait assez choquant pour beaucoup d’administrateurs système
Au final, je pense que la seule solution permettant d’échapper à l’exposition via les variables d’environnement et les fichiers en clair est une architecture où le gestionnaire de secrets transmet les secrets via un partage de fichier temporaire, comme avec le
op clide 1Password, flask ou terraformLe système de credentials de
systemdsuit cette logique. Mais il est encore peu pris en chargeSi quelqu’un connaît une bonne méthode pour transmettre des secrets sans variables d’environnement ni fichiers en clair, j’aimerais bien la connaître
À titre de référence, dans le cas du client
opde 1Password, il faut mon approbation à chaque session, donc je trouve cela sûr dans une session CLI ; même si un processus malveillant appelait le binaireop, une approbation séparée serait exigéeLe problème restant est alors de savoir comment transmettre ce secret au processus qui en a réellement besoin, et on a l’impression de revenir au point de départ
Lien vers la documentation officielle systemd sur les variables d’environnement
Depuis environ 2012, les variables d’environnement sont devenues aussi sûres que la mémoire ordinaire
Historique du commit correspondant
Pour lire les variables d’environnement d’un autre processus, il faut impérativement avoir les droits
ptrace, et si l’on peut déjà utiliserptrace, on peut de toute façon lire tous les secrets, donc cette inquiétude n’aurait pas vraiment de sensLes informations de ligne de commande (
cmdline) sont un autre sujet, mais les variables d’environnement ne s’exposent plus aussi facilement de cette manièreDans le modèle de sécurité de la plupart des systèmes d’exploitation, exécuter sous un même utilisateur revient à accorder pleinement tous les droits de cet utilisateur
Il existe quelques mécanismes supplémentaires comme capsicum sur FreeBSD, landlock, SELinux, AppArmor sous Linux, ou les integrity labels sous Windows, mais la plupart ont des limites bien marquées
En pratique, je suis libre de tuer, suspendre ou déboguer mes propres processus, et je peux toujours accéder aux secrets d’un processus qui m’appartient via
ptrace/process_vm_readv/ReadProcessMemory, etc.Il existe bien des modèles de sécurité complètement différents, comme des OS parfaitement fondés sur les capabilities, mais la grande majorité suit encore ce modèle, et il faut en connaître les limites et les responsabilités
memfd_secretme vient à l’esprit comme bonne méthode pour transmettre des secrets sans variables d’environnement ni fichiers en clairpage man de memfd_secret
Le support n’est pas très répandu selon les langages ou frameworks, donc cela pourrait valoir le coup d’essayer via FFI en Rust, ou en Go si c’est possible aussi
J’avais envisagé de l’encapsuler directement côté PHP, mais j’ai abandonné car je ne voulais pas aller jusqu’à modifier php-fpm
En pratique, le plus sûr serait qu’un gestionnaire de processus ouvre à l’avance un descripteur de fichier secret puis le transmette au processus enfant, afin qu’il puisse l’utiliser sans exposition en mémoire ou ailleurs
Le modèle de sécurité Unix classique reste encore très utilisé, avec quelques améliorations, mais il montre clairement ses limites dans les environnements récents ou à faible coût
S’il faut cacher des secrets à d’autres processus, la bonne pratique consiste d’emblée à les exécuter sous des utilisateurs différents
Sinon, il y a aussi l’approche consistant à y accéder à distance, mais elle entraîne elle aussi ses inconvénients et sa complexité
Aujourd’hui, sur les plateformes de conteneurs, il est plutôt recommandé de transmettre la config ou les secrets via des variables d’environnement
Dans un conteneur, les autres processus sont conçus pour ne pas pouvoir inspecter les variables d’environnement
Le fait que les variables d’environnement soient héritées par les processus enfants est intentionnel, car l’entité qui configure l’environnement avec la valeur du secret définit aussi directement cet environnement
Je ne considère pas la plupart des problèmes évoqués comme particulièrement graves, mais je suis prêt à en discuter concrètement si besoin
Beaucoup de commentaires se concentrent sur la gestion des secrets et ses problèmes, mais cela vaut aussi la peine de réfléchir aux avantages des variables d’environnement
Les variables d’environnement constituent une « liaison dynamique de variables à portée indéfinie » qui relie structurellement les processus Unix
Plutôt que de les comparer simplement à des fichiers texte, il faut se rappeler que leur raison d’être est la transmission de contexte pour faire passer en toute sécurité des informations aux processus enfants
Plus la structure des processus est complexe — shells imbriqués, sous-processus de programmes complexes, etc. — plus le rôle des variables d’environnement devient pertinent
Je recommande vraiment Varlock, c’est très utile
Il permet de définir clairement les variables d’environnement nécessaires à un projet, si elles sont obligatoires ou facultatives, leur type, et même d’où elles doivent venir, ce qui facilite beaucoup leur gestion
Site officiel de Varlock
D’après mon expérience en conditions réelles, pour illustrer à quel point les variables d’environnement peuvent devenir complexes, j’ai complètement perdu pied un jour en essayant de déboguer l’endroit où une certaine variable
ENVétait définie dans une ancienne entrepriseAu départ, je pensais qu’elle était définie dans
.bashrcou un autre endroit simple du même genre, mais en réalité elle passait par au moins dix couches : niveau entreprise, région, division, équipe, individuel, etc.J’ai finalement dû activer les flags de debug de bash pour remonter laborieusement la trace et comprendre où elle était définie
Je ne sais pas si d’autres langages le prennent en charge, mais Node.js a récemment ajouté un flag en ligne de commande qui permet de tracer précisément l’accès et les modifications des variables d’environnement
Documentation Node.js sur
--trace-envComme les valeurs peuvent être définies ou modifiées via d’innombrables API, cela me semble extrêmement utile pour les débogages complexes
C’est le genre de cas qui fait penser : « un seul espace de noms ne suffirait-il pas ? »
J’ai renoncé aux variables d’environnement il y a longtemps
J’utilise maintenant un fichier
dmd.confà côté du compilateur, que celui-ci lit directementLe problème le plus grave des variables d’environnement, c’est leur caractère implicite et opaque
Dans le monde *nix, la plupart des applications ont tendance à dépendre des variables d’environnement
Même lorsqu’une méthode de configuration explicite et transparente est aussi prise en charge — fichier de configuration, service distant, argument en ligne de commande — la prise en charge des variables d’environnement reste une tradition du milieu
Au fond, les variables d’environnement sont aussi une table de hachage globale, clonée et étendue pour les processus enfants ; c’était peut-être une conception raisonnable en 1979, mais aujourd’hui cela devient souvent toxique
Par exemple, Kubernetes pollue par défaut l’environnement des conteneurs avec des variables d’environnement de type « service link »
Si les variables d’environnement attendues par l’application entrent en conflit avec ces variables par défaut, le débogage devient extrêmement difficile
Référence à la documentation officielle de Kubernetes
Au-delà de cela, j’ai l’impression qu’il y a énormément d’autres cas où l’on conserve sans esprit critique des cadres hérités, comme
/bin,/usr/bin,/lib,/usr/libRéférence : Q&R Ubuntu sur le maintien des répertoires historiques
hjklcomme un autre exemple typique de ce conservatismeDans vi,
hjklvient des terminaux rudimentaires d’il y a 40 ans, et ces terminaux se vendaient peu(encore moins qu’un Nokia N9)
Chaque fois que je définis une variable d’environnement sous Linux, un sentiment d’anxiété m’envahit
La manière officiellement correcte de faire varie un peu selon les distributions, et même en suivant les guides en ligne, tout disparaît après un redémarrage ou quand on ferme le terminal
J’aimerais qu’il existe un éditeur GUI simple de variables d’environnement globales comme sous Windows
Sous Windows, il y a bien l’inconvénient de devoir rouvrir le terminal pour prendre en compte les changements, mais à part ça, cela fonctionne toujours bien
C’est normal que les variables d’environnement ne persistent pas d’une session à l’autre ; il faut donc les écrire à un endroit réexécuté à chaque session, comme au login ou à l’ouverture du terminal
À la connexion,
.bash_profileest exécuté, et pour les sessions enfants, c’est.bashrcSi l’on fait un
sourcede.bashrcdepuis.bash_profileet que l’on place la plupart des réglages dans.bashrc, cela devient plus facile à gérerSi l’on n’utilise pas Bash mais un autre shell comme zsh ou fish, il faut s’aligner sur le fonctionnement de ce shell
Sous Linux, il n’existe pas de GUI officiel et unifié pour les variables d’environnement applicable à tous les terminaux
On pourrait bien créer une GUI avec un parsing complexe, mais en pratique il est plus simple de modifier cela avec un éditeur de texte
Comme j’utilise principalement Linux, le comportement de Windows me paraît au contraire plus inconfortable
Trop d’applications polluent les variables d’environnement, et quand quelque chose ne fonctionne pas, on finit souvent par découvrir que
$SOFTWAREs’exécutait depuis un dossier étrange ou quelque chose du genreSi l’on utilise
systemd, on peut aussi écrireKEY=VALUEdans/etc/environmentou/etc/environment.d/On pourrait d’ailleurs sans doute créer une GUI pour cette méthode
Cela dit, les variables d’environnement ne peuvent pas être injectées dans un processus déjà en cours d’exécution, donc il faut redémarrer pour appliquer les changements, ce qui reste une limite
Référence à la documentation officielle de systemd
BD xkcd sur les standards
Elle illustre avec humour le fait que Linux a déjà 14 méthodes concurrentes pour définir des variables d’environnement, et que si l’on propose de les unifier, on se retrouve le lendemain avec un 15e standard
Mon anecdote préférée sur les variables d’environnement, c’est que des choses comme
PS1, que tout le monde considère naturellement comme des variables d’environnement, n’en sont en réalité pas ; ce sont des variables du shellOn ne peut même pas voir
PS1avec la commandeenv