- Aunque el software ha avanzado rápidamente, el sistema de variables de entorno del sistema operativo aún conserva una estructura de hace décadas
- Las variables de entorno tienen la forma de un diccionario global de cadenas, una estructura simple sin namespace ni tipos
- En Linux, las variables de entorno se transmiten del proceso padre al hijo mediante la llamada al sistema
execve
- Bash, glibc y Python administran las variables de entorno respectivamente como hashmaps, arreglos y envolturas de diccionario
- El estándar POSIX no exige solo mayúsculas en los nombres y, de hecho, tiene reglas flexibles, como recomendar el uso de nombres en minúsculas
Qué son las variables de entorno
- Aunque los lenguajes de programación han evolucionado rápidamente, la estructura base para ejecutar procesos que ofrece el sistema operativo, especialmente la parte de las variables de entorno, casi no ha cambiado
- Al ejecutar una aplicación, si se quiere pasar parámetros de runtime sin archivos separados ni IPC, en la práctica no queda más que usar una interfaz basada en variables de entorno
- Las variables de entorno funcionan como un diccionario plano de cadenas, sin namespace ni tipos
Estructura de creación y transmisión de variables de entorno
- Las variables de entorno son un método tradicional para pasar valores entre procesos, y se envían junto con la ejecución de un proceso hijo por parte del proceso padre
- Es decir, se heredan del proceso padre al hijo
- En Linux, la syscall
execve recibe como argumentos el ejecutable, los parámetros y el arreglo de variables de entorno (envp)
- Ejemplo del comando de ejecución:
ls -lah
- filename:
/usr/bin/ls
- argv:
['ls', '-lah']
- envp:
['PATH=...','USER=...']
- El proceso padre puede pasarle al hijo el entorno existente tal cual o construir un entorno completamente nuevo
- Casi todas las herramientas (Bash,
subprocess.run de Python, la biblioteca de C execl, etc.) pasan las variables de entorno tal como están
- Como excepción, algunas herramientas como
login construyen un entorno nuevo
Dónde se almacenan y cómo se procesan internamente
- Al iniciar un programa, el kernel guarda las variables de entorno en la pila en forma de cadenas terminadas en null
- Estos datos son difíciles de modificar directamente desde el programa, por lo que normalmente se copian y se administran con una estructura propia dentro del proceso
- Cómo almacenan las variables de entorno distintos lenguajes y shells
- Bash: las administra como un hashmap (diccionario) con estructura de pila
- En cada llamada de función se crea un mapa de scope local
- Solo las variables con
export se transmiten al proceso hijo
- Incluso las variables declaradas con
local pueden pasarse al proceso hijo mediante export
- Ejemplo: con
export PATH, un cambio local se refleja en el hijo, pero no afecta al entorno global
- glibc (biblioteca de C): administra
environ con putenv y getenv como una estructura de arreglo dinámico
- Al ser un arreglo, tanto las consultas como los cambios tienen complejidad temporal lineal
- Por eso no es adecuado para almacenar datos con altas exigencias de rendimiento
- Python: internamente lo expone como un diccionario a través de
os.environ, pero en realidad está vinculado al arreglo environ de la biblioteca de C
- Cuando cambia un valor en
os.environ, se llama a os.putenv y también se refleja en la biblioteca de C
- En sentido contrario no se sincroniza, así que existe unidireccionalidad
Formato y rangos permitidos de las variables de entorno
- El kernel de Linux y glibc son muy tolerantes con el formato de las variables de entorno
- Puede haber varios valores duplicando el mismo nombre
- Incluso se pueden registrar sin
= y no hay restricciones sobre caracteres especiales como emojis
- Límites de tamaño disponibles
- Variable individual: 128 KiB (normalmente en entornos x64)
- Suma total: 2 MiB (compartida con los argumentos de línea de comandos)
- Las variables de entorno están limitadas a no superar 1/4 del espacio de pila
Particularidades y casos límite
- Bash, en el caso de variables de entorno extrañas (duplicadas, entradas sin
=, etc.), elimina nombres duplicados e ignora entradas anómalas
- Si el nombre de una variable contiene espacios, Bash no puede referenciarlo, pero aun así puede transmitirlo al proceso hijo
- Por ejemplo, Nushell y Python pueden crear variables con espacios en el nombre
- Bash administra estas entradas en un hashmap separado (
invalid_env)
Formato estándar y reglas de nombres
- El estándar POSIX considera variable cualquier nombre que no contenga signo igual (
=)
- Recomendación oficial: el nombre debe usar solo mayúsculas, números y guiones bajos (y no puede empezar con un número)
- Las variables en minúsculas están pensadas como namespace exclusivo de aplicaciones
- Las herramientas estándar usan solo mayúsculas, pero también se permite usar variables en minúsculas
- En la práctica, los desarrolladores suelen nombrarlas en estilo ALL_UPPERCASE
- Regla recomendada: usar la expresión regular
^[A-Z_][A-Z0-9_]*$ para el nombre y UTF-8 para el valor
- Si preocupan las excepciones o la compatibilidad, se recomienda usar el Portable Character Set (ASCII) de POSIX
Conclusión
- Las variables de entorno siguen siendo una interfaz vieja pero indispensable, que actúa como frontera entre el sistema operativo y las aplicaciones
- A pesar de sus limitaciones estructurales, Bash, C y Python, entre otros, las siguen usando envueltas de distintas maneras
- En sistemas modernos, cada vez se necesita más una gestión de configuración con namespaces claros y un sistema de tipos
2 comentarios
Aunque parecía perder importancia a simple vista, con la llegada de Docker y la nube volvió a ser algo imposible de evitar.
Opiniones en Hacker News
Trabajo como SRE/sysadmin/DevOps/lo que sea; en el blog solo se hablaba de forma ligera sobre la estandarización de variables de entorno, pero quiero señalar que las alternativas también suelen ser igual de frustrantes, especialmente cuando hay secretos de por medio
Cuando una aplicación está diseñada para acceder a un almacén de secretos específico como Hashicorp Vault/OpenBao/Secrets Manager, rápidamente termina en una fuerte dependencia del proveedor, y reemplazarlo se vuelve muy difícil porque el impacto llega hasta el nivel de las librerías
La disponibilidad de Vault pasa a ser críticamente importante, y cuando hay que hacer upgrades o mantenimiento, el equipo de operaciones queda en una situación muy complicada
Si pasas secretos mediante archivos de configuración, entonces surge el problema de cómo contener esos secretos, porque los archivos de config suelen estar en rutas públicas
Al final terminas dependiendo de una de dos cosas: “reemplazo por plantillas antes de que el sistema privilegiado se lo entregue a la app” o “guardar el archivo de config completo en el almacén de secretos y entregárselo a la app”
El trabajo con plantillas es propenso a errores, y mover el archivo de config completo al almacén de secretos también genera estrés porque siempre existe el riesgo de que alguien suba algo incorrecto
Hoy en día la mayoría de los sistemas corren sobre contenedores, y salvo que sea una empresa con una infraestructura muy bien cuidada, los archivos de config siempre terminan en lugares raros, lo que hace que el proceso de montarlos sea todavía más confuso y propenso a errores
Da igual si usas JSON/YAML/TOML o cualquier otro formato: los bugs peculiares son cosa de todos los días; por ejemplo, está el problema de Norway en YAML
He visto sistemas que reciben secretos mediante la Kubernetes Secrets API, pero eso también se topa con el problema de una fuerte dependencia del proveedor
A menos que estés diseñando algo específicamente como un operador, no recomendaría activamente ese enfoque
También he visto problemas derivados de configurar variables de entorno a través de subprocess, pero siento que hoy los equipos prefieren sistemas basados en buses de mensajes, porque son más robustos y permiten escalar de forma independiente
En nuestro equipo tuvimos la experiencia de crear una librería ligera y genérica para secrets, y solo enchufábamos backends específicos del proveedor, como AWS Secrets Manager, por medio de plugins
Permitía configurar caché local y opciones para omitir la caché por parámetro, así que toda la lógica realmente dependiente del proveedor quedaba confinada al backend, y la librería y la aplicación podían mantenerse sin dependencia del proveedor
Incluso al migrar a Vault, solo agregamos un backend y cambiamos la configuración, y funcionó sin mayores problemas
Me da curiosidad por qué la Kubernetes secret API se percibe como un problema de dependencia del proveedor
¿Será que intentaban usar deployment yaml para algo distinto a un despliegue en Kubernetes?
En la mayoría de las apps, si montas el secret dentro del contenedor y luego lo inyectas a la app como variable de entorno o como archivo json, puedes leerlo y usarlo de forma independiente del entorno
Según entiendo, el cifrado del backend etcd también puede configurarse con KMS
No termino de entender por qué recibir secretos mediante la Kubernetes Secrets API sería lock-in
En esencia, los K8s secrets no se almacenan cifrados por defecto, así que para que realmente tenga sentido, necesitas (0) usar K8s, (1) haber configurado cifrado en el control plane y (2) usar obligatoriamente una solución adicional como un driver CSI
Y además, Secret Store CSI Driver soporta múltiples backends como Conjur, así que más bien va en dirección contraria al lock-in
Por todo esto, nosotros seguimos usando config principalmente con env vars y dotenv
La estructura de configuración basada en variables de entorno es demasiado simple y además funciona bien con herramientas diversas, incluidos gestores de secretos
En los últimos años también me ha empezado a interesar poco a poco
sopsbasado en YAMLYAML es realmente intuitivo para expresar la estructura de configuración de una app, y con
sopses fácil cifrar y administrar solo partes específicasEso sí, manejar claves GPG puede ser complicado, aunque se puede resolver con algo como Vault u OpenBao
Pero claro, en ese proceso vuelve a aparecer el problema del lock-in del proveedor, aunque OpenBao parece un poco menos problemático en ese sentido
También puedes recibir variables de entorno como resultado de ejecutar un comando, así que es posible manejar esto sin proceso de plantillas y sin lock-in del proveedor
Un dato curioso más:
setenv()está fundamentalmente roto en POSIX, así que creo que nunca debería usarse en código de libreríasIncluso cuando tengas que usarlo en código de aplicación, debería ser el último recurso, y solo antes de crear hilos
getenv()devuelve directamente un puntero al entorno original, así que cuandosetenv()sobrescribe una variable, no hay ninguna protecciónHay que tener muchísimo cuidado
Creo que la forma correcta de establecer variables de entorno es usando
execve()Este enfoque solo es adecuado cuando transmites información por variables de entorno justo antes o después de
exec()No entiendo por qué alguien querría usar
setenvdentro de código de libreríaSolaris resolvió este problema, pero Linux sigue aferrado al mismo enfoque
NetBSD ha tenido desde hace mucho una alternativa segura llamada
getenv_r(), y recientemente FreeBSD también la adoptóProbablemente macOS la siga pronto
Ya hubo intentos de meterla en glibc o POSIX, pero fueron rechazados
Espero que cuando se difunda más entre plataformas, algún día termine siendo aceptada oficialmente
Documentación de
getenv_ren NetBSDCommit de FreeBSD
Las variables de entorno se usan con frecuencia para pasar secretos, pero no me parece una buena práctica
En Linux, todos los procesos que corren bajo el mismo usuario pueden mirar las variables de entorno entre sí
Sea cual sea tu modelo de amenazas, esto preocupa especialmente en las máquinas de los desarrolladores, donde suele haber muchísimos procesos corriendo con el mismo usuario
Este problema se vuelve más grave cuando hay muchos procesos moviéndose fuera del contenedor, como con agentes LLM
Además, las variables de entorno normalmente se heredan tal cual a los procesos hijo, así que incluso si solo un proceso necesita el secreto, este tiende a quedar expuesto innecesariamente
systemdmuestra las variables de entorno a todos los clientes del sistema mediante DBUS, y en la documentación oficial también se advierte que no se deben guardar secretos en variables de entornoSi eso es cierto, significaría que variables de entorno configuradas en unidades solo para root podrían ser visibles incluso para usuarios normales, lo cual sería bastante impactante para muchos administradores de sistemas
Al final, creo que la única solución que realmente evita la exposición en variables de entorno y archivos en texto plano es una estructura en la que el gestor de secretos entregue el secret mediante archivos temporales compartidos (por ejemplo, 1Password
opcli, flask, terraform, etc.)El sistema de credentials de
systemdfunciona así. Pero todavía tiene poco soporteSi alguien conoce una buena forma de pasar secretos sin usar variables de entorno ni archivos en texto plano, me gustaría que la compartiera
En el caso del cliente
opde 1Password, siento que es seguro para usar en sesiones CLI porque cada sesión requiere mi aprobación, así que incluso si algún proceso malicioso invocara el binarioop, igual necesitaría una aprobación aparteAhora el problema que queda es cómo pasar ese secreto al proceso que realmente lo necesita, y siento que eso nos regresa al punto de partida
Enlace de referencia a la documentación oficial de
systemdsobre variables de entornoDesde alrededor de 2012, las variables de entorno son tan seguras como la memoria normal
Registro del commit relacionado
Para leer las variables de entorno de otro proceso se requiere obligatoriamente permiso de
ptrace, y si ya puedes leer porptrace, en realidad también puedes leer todos los secretos, así que preocuparse por esto no aporta muchoLa información de línea de comandos (
cmdline) es otra historia, pero las variables de entorno ya no quedan expuestas tan fácilmente por esa víaEn el modelo de seguridad de la mayoría de los sistemas operativos, ejecutar algo bajo un mismo usuario equivale a entregarle por completo todos los privilegios de ese usuario
Hay excepciones en forma de funciones de seguridad adicionales como capsicum en FreeBSD, landlock en Linux, SELinux, AppArmor o las integrity labels de Windows, pero la mayoría tienen limitaciones claras
Al final, si es mi proceso, puedo matarlo, pausarlo o depurarlo libremente, y mediante
ptrace/process_vm_readv/ReadProcessMemory, etc., siempre podré acceder a los secretos de un proceso que me perteneceExisten modelos de seguridad completamente distintos (sistemas operativos perfectamente basados en capabilities), pero la gran mayoría sigue este modelo, así que hay que entender sus límites y responsabilidades
Una buena forma de pasar secretos sin usar variables de entorno ni archivos en texto plano podría ser
memfd_secretPágina man de
memfd_secretNo hay mucho soporte por lenguaje o framework, así que valdría la pena probarlo vía FFI, especialmente en Rust o quizá también en Go
En algún momento pensé en envolverlo directamente para PHP, pero no quería llegar al punto de modificar
php-fpm, así que lo dejéEn la práctica, lo más seguro sería que el process manager abriera por adelantado el file descriptor del secret y luego se lo pasara al proceso hijo, para poder usarlo sin exponerlo en memoria ni en otros lados
El modelo clásico de seguridad de Unix sigue usándose ampliamente, aunque con pequeñas mejoras, pero sus límites son muy evidentes en entornos baratos de cómputo o en entornos modernos
Si necesitas ocultar secretos de otros procesos, la forma correcta desde el inicio es separarlos y ejecutarlos bajo otro usuario
O bien usar acceso remoto desde el principio, aunque eso también trae desventajas y complejidad
Hoy en día, en plataformas de contenedores suele recomendarse pasar config o secretos mediante variables de entorno
Dentro del contenedor, está diseñado para que otros procesos no puedan inspeccionar las variables de entorno
Que las variables de entorno se hereden a los procesos hijo también es parte intencional del diseño, porque quien configura el entorno y posee el valor del secret es quien establece directamente ese entorno
No veo como un gran problema la mayoría de los puntos de preocupación mencionados, aunque si hace falta podría discutirlos de forma más concreta
Muchos comentarios se enfocan en la gestión de secretos y sus problemas, pero también vale la pena pensar un momento en las ventajas de las variables de entorno
Las variables de entorno son un “binding dinámico y extensible de variables con alcance indefinido” que conecta estructuralmente a los procesos Unix
Más que compararlas de inmediato con un archivo de texto simple, conviene recordar que su razón de existir está en transmitir contexto de forma segura a los procesos hijo
Cuanto más compleja es la estructura de procesos —shells anidados, subprocesos de programas complejos, etc.— más se luce el papel de las variables de entorno
Quiero recomendar
Varlock, que de verdad me parece muy útilTe permite definir con claridad qué variables de entorno requiere un proyecto, cuáles son obligatorias u opcionales, su tipo de dato e incluso de dónde obtenerlas, y es fácil de administrar
Sitio oficial de Varlock
misePor experiencia práctica, un ejemplo de lo complejas que pueden volverse las variables de entorno: en una empresa donde trabajé hace años, alguna vez intenté depurar de dónde se estaba configurando cierta variable
ENVy fue un caos totalAl principio pensé que se definía en
.bashrco en algún lugar sencillo, pero en realidad se configuraba a través de al menos 10 capas: nivel compañía, región, unidad de negocio, equipo, individuo, etc.Al final solo pude rastrear una por una dónde se establecía activando las flags de depuración de bash
No sé si otros lenguajes también lo soportan, pero Node.js añadió recientemente una flag de línea de comandos que permite rastrear con precisión el acceso y los cambios en variables de entorno
Documentación de Node.js sobre
--trace-envComo los valores pueden configurarse o modificarse mediante muchísimas APIs, imagino que debe ser muy útil para depuración compleja
Es uno de esos casos que te hacen pensar: “¿no bastaría con un solo namespace?”
Hace mucho tiempo dejé de usar variables de entorno
Ahora pongo un archivo
dmd.confjunto al compilador y hago que el compilador lo lea directamenteEl problema más grave de las variables de entorno es su carácter implícito y opaco
En el mundo *nix, la mayoría de las apps tienden a depender de variables de entorno
Aunque haya soporte adicional para métodos de configuración explícitos y transparentes (archivos de configuración, servicios remotos, argumentos de línea de comandos), el soporte para variables de entorno sigue siendo la tradición de este ecosistema
Al final, las variables de entorno también son un hash map global que se clona y amplía para los procesos hijo; en 1979 eso quizá era un diseño razonable, pero hoy muchas veces termina siendo veneno
Por ejemplo, Kubernetes contamina por defecto el entorno del contenedor con variables de entorno de “service link”
Si las variables que espera la app chocan con las env vars por defecto, depurarlo se vuelve extremadamente difícil
Referencia a la documentación oficial de Kubernetes
Además de eso, siento que hay muchísimas prácticas que mantienen sin cuestionar marcos viejos como
/bin,/usr/bin,/lib,/usr/libReferencia: Q&A de Ubuntu sobre mantener directorios legacy
hjkltambién podrían verse como un ejemplo representativo de este tipo de tradición anticuadahjklen vi viene de una terminal tonta de hace 40 años, y además era una terminal de pocas ventas(incluso menos que la Nokia N9)
Cada vez que configuro variables de entorno en Linux me entra una sensación de inseguridad
La manera “oficial” de hacer que funcionen varía un poco entre distribuciones, y aunque sigas guías de internet, todo desaparece al reiniciar o al cerrar la terminal
Ojalá existiera un editor GUI simple para variables de entorno globales, como en Windows
Windows tiene la molestia de que hay que abrir una terminal nueva para que se apliquen los cambios, pero fuera de eso siempre funciona bien
Las variables de entorno, por definición, no persisten cuando cambia la sesión, así que lo normal es escribirlas en algún lugar que se ejecute de nuevo en cada sesión (login/terminal, etc.)
Al iniciar sesión se ejecuta
.bash_profile, y en las sesiones hijas se ejecuta.bashrcSi haces
sourcede.bashrcdesde.bash_profiley dejas la mayor parte de la configuración en.bashrc, es más fácil administrarloSi no usas Bash sino otro shell como zsh o fish, entonces debes ajustarlo según ese shell
En Linux no existe una GUI oficial y unificada para variables de entorno que aplique a todas las terminales
Se podría crear una GUI que hiciera parsing complejo, pero en la práctica suele ser más fácil editarlo con un editor de texto
Desde mi perspectiva, que uso Linux principalmente, el comportamiento de Windows me resulta todavía más incómodo
Demasiadas apps contaminan las variables de entorno, así que cuando algo falla, al final descubres que
$SOFTWAREse estaba ejecutando desde una carpeta extraña o algo parecido, y eso genera mucha confusiónSi usas
systemd, también es posible escribirKEY=VALUEen/etc/environmento/etc/environment.d/De hecho, parece que se podría hacer una GUI para eso
Pero las variables de entorno no pueden inyectarse en procesos ya en ejecución; tienen la limitación de que solo se aplican tras reiniciar el proceso
Referencia a la documentación oficial de
systemdCómic Standards de xkcd
Muestra de forma divertida que en Linux ya hay como 14 formas compitiendo para configurar variables de entorno, así que si alguien dice “unifiquémoslas en una sola”, al día siguiente habrá 15 estándares
Mi dato curioso favorito sobre variables de entorno es que cosas como
PS1, que todo el mundo da por hecho que son variables de entorno, en realidad no lo son, sino variables del shellNi siquiera puedes ver
PS1con el comandoenv