Gestión Segura de Variables de Entorno en Unidades de Servicio de Systemd
Configura variables de entorno de systemd con Environment, EnvironmentFile, drop-ins y un manejo más seguro de secretos.
Gestión Segura de Variables de Entorno en Unidades de Servicio de Systemd
Las variables de entorno son convenientes, pero no son automáticamente privadas. En un servicio de systemd, pueden aparecer en archivos de unidad, drop-ins, systemctl show, herramientas de inspección de procesos, informes de fallos, registros de depuración o paquetes de soporte copiados. Eso no significa que nunca puedas usarlas. Significa que debes ser deliberado sobre qué incluyes en ellas y quién puede leer los archivos que las definen.
Esta guía cubre las dos directivas comunes, Environment= y EnvironmentFile=, y luego muestra cómo usar drop-ins para que la configuración local se mantenga separada de las unidades gestionadas por paquetes.
El Rol de las Variables de Entorno en Systemd
Las variables de entorno proporcionan una forma directa de configurar un servicio sin cambiar su código. Cuando systemd inicia un servicio, construye el entorno del proceso y aplica las variables definidas en la unidad antes de ejecutar ExecStart=.
Systemd proporciona dos directivas principales dentro de la sección [Service] de un archivo de unidad para gestionar estas variables.
1. Definición Directa: La Directiva Environment
Este método te permite definir variables directamente dentro del archivo de unidad de Systemd. Es adecuado para parámetros de configuración no sensibles que rara vez cambian.
Uso y Sintaxis
La directiva Environment acepta una lista separada por espacios de asignaciones de variables en el formato "KEY=VALUE".
# /etc/systemd/system/my-app.service
[Unit]
Description=Servicio de Mi Aplicación
[Service]
User=myuser
WorkingDirectory=/opt/my-app
# Define variables directamente en el archivo de unidad
Environment="APP_PORT=8080" "NODE_ENV=production"
ExecStart=/usr/local/bin/my-app --start
[Install]
WantedBy=multi-user.target
Limitaciones y Seguridad
Aunque es conveniente, la directiva Environment es un mal lugar para contraseñas, tokens o credenciales de bases de datos. Los archivos de unidad a menudo se almacenan en sistemas de gestión de configuración, se copian en tickets, o son legibles por operadores que necesitan inspeccionar el comportamiento del servicio pero no deberían ver secretos. Úsala para valores como puertos, flags de funcionalidades, niveles de registro y rutas.
2. Configuración Externa: La Directiva EnvironmentFile
Para configuraciones más grandes, cargar variables desde un archivo externo suele ser más limpio. Te permite gestionar los permisos del archivo de variables independientemente del archivo de unidad principal. También mantiene las unidades proporcionadas por paquetes legibles mientras que la configuración local reside en /etc.
Uso y Sintaxis
La directiva EnvironmentFile toma una ruta absoluta a un archivo de configuración. Systemd lee este archivo línea por línea, tratando cada línea como una posible asignación KEY=VALUE.
[Service]
# Carga variables desde un archivo externo
EnvironmentFile=/etc/config/my-app-settings.conf
ExecStart=/usr/local/bin/my-app --start
Formato del Archivo de Entorno
El archivo externo debe adherirse a un formato simple similar al de shell:
- Las líneas que comienzan con
#se tratan como comentarios. - Las líneas que comienzan con una asignación de variable vacía (
VAR=) borrarán la variable si se había establecido previamente. - Las variables se definen como
KEY=VALUE. - Se admite poner el valor entre comillas (
KEY="VALUE WITH SPACES").
# /etc/config/my-app-settings.conf
# Variables no sensibles
MAX_WORKERS=4
LOG_LEVEL=INFO
# Variable sensible (requiere permisos de archivo estrictos y control de acceso cuidadoso)
DB_PASSWORD=SecureRandomString12345
Evita hábitos de shell que el analizador de archivos de entorno de systemd no soporta como esperas. No escribas export KEY=value. No pongas espacios alrededor del signo igual. Si el valor contiene espacios, ponlo entre comillas. Si el valor contiene comillas literales, barras invertidas o saltos de línea, pruébalo antes de confiar en él en producción.
Manejo de Archivos Faltantes
Por defecto, si el archivo especificado por EnvironmentFile no existe, Systemd fallará al iniciar el servicio. Si el archivo de entorno es opcional, puedes prefijar la ruta del archivo con un guion (-):
EnvironmentFile=-/etc/config/optional-settings.conf
Si el archivo está prefijado con -, Systemd ignorará los errores causados por la ausencia del archivo.
Mejor Práctica: Usar Unidades Drop-in para Datos Sensibles
Modificar el archivo de unidad principal (por ejemplo, /usr/lib/systemd/system/my-app.service) generalmente no se recomienda, especialmente si el archivo es gestionado por un gestor de paquetes. En su lugar, usa archivos de unidad drop-in para aplicar anulaciones o adiciones de configuración.
Esta práctica es importante porque separa los valores predeterminados del proveedor de la configuración local. También facilita las auditorías: la unidad dice de dónde se carga la configuración, y los permisos de ese archivo indican quién puede leerlo.
Configuración Paso a Paso de Drop-in
1. Localizar/Crear el Directorio Drop-in
Para un servicio llamado my-app.service, el directorio drop-in debe llamarse my-app.service.d/ y residir en la jerarquía /etc/systemd/system/.
sudo mkdir -p /etc/systemd/system/my-app.service.d/
2. Crear la Anulación de Configuración
Crea un archivo dentro del directorio drop-in (por ejemplo, secrets.conf). Este archivo solo necesita la sección [Service] y las directivas específicas que deseas anular o agregar.
# /etc/systemd/system/my-app.service.d/secrets.conf
[Service]
# Carga el archivo de credenciales seguras
EnvironmentFile=/etc/secrets/my-app-credentials.env
3. Asegurar el Archivo de Entorno Externo
Este es el paso de seguridad más crítico. Asegúrate de que el archivo externo que contiene los secretos tenga permisos restrictivos. Idealmente, debería ser propiedad de root:root y solo legible por el usuario root o el propio usuario del servicio.
# Crear el archivo de secretos
sudo touch /etc/secrets/my-app-credentials.env
# Poblar el archivo con secretos
sudo sh -c 'echo "DB_PASS=S3cr3tP@ssw0rd" >> /etc/secrets/my-app-credentials.env'
# Establecer permisos restrictivos
sudo chmod 600 /etc/secrets/my-app-credentials.env
Si el archivo referenciado por EnvironmentFile contiene credenciales, mantenlo legible solo por la cuenta que necesita gestionar el servicio. 0600 root:root es común cuando systemd lee el archivo antes de reducir privilegios con User=, pero algunos modelos operativos usan un grupo dedicado propiedad de root y 0640. La parte importante es que los usuarios ordinarios no puedan leer el archivo.
También sé honesto sobre el riesgo restante. Las variables de entorno son más fáciles de manejar que los argumentos de línea de comandos hardcodeados, pero aún no son un sistema completo de gestión de secretos. Para credenciales de alto riesgo, considera un almacén de secretos dedicado, credenciales de corta duración, credenciales de systemd en distribuciones más nuevas, o un mecanismo específico de la aplicación que lea un archivo protegido directamente.
Solución de Problemas y Verificación
Después de realizar cualquier cambio en los archivos de unidad o drop-ins, debes recargar la configuración del gestor de Systemd.
sudo systemctl daemon-reload
sudo systemctl restart my-app.service
Para verificar qué variables de entorno han sido cargadas exitosamente por Systemd para un servicio en ejecución, usa el comando systemctl show y consulta específicamente la propiedad Environment:
systemctl show my-app.service --property=Environment
Ejemplo de Salida (mostrando variables cargadas):
Environment=APP_PORT=8080 NODE_ENV=production DB_PASS=S3cr3tP@ssw0rd
Ese comando es útil para depuración, pero también es un recordatorio: cualquiera que tenga permiso para ejecutar los comandos de inspección adecuados como root puede ver los valores. No pegues esta salida en chats compartidos, tickets o informes de errores públicos sin redactarla.
Si el servicio falla al iniciar, verifica los registros del servicio usando journalctl -xeu my-app.service. Las razones comunes de fallo relacionadas con variables de entorno incluyen:
- Ruta de archivo incorrecta en
EnvironmentFile. - Archivo faltante (y la ruta no estaba prefijada con
-). - Sintaxis incorrecta de variable en el archivo de entorno externo (por ejemplo, espacios alrededor del signo
=).
Patrones Prácticos Que Funcionan
| Escenario | Directiva a Usar | Mejor Práctica de Ubicación | Consideraciones de Seguridad |
|---|---|---|---|
| Configuración Estática No Sensible | Environment |
Archivo de unidad directo o drop-in | Riesgo de seguridad bajo. |
| Credenciales Sensibles (Secretos) | EnvironmentFile |
Archivo externo, referenciado mediante un drop-in (*.service.d/) |
CRÍTICO: El archivo de entorno debe tener permisos 0600. |
| Modularidad y Anulaciones | EnvironmentFile |
Archivo de unidad drop-in | Separa la configuración de los valores predeterminados del proveedor. |
Al aprovechar la directiva EnvironmentFile dentro de una unidad drop-in dedicada y garantizar permisos de archivo estrictos, los administradores pueden gestionar de manera segura y flexible las configuraciones de servicio, adhiriéndose a los principios de mínimo privilegio y separación de preocupaciones.
Para un servicio interno pequeño, una configuración razonable a menudo se ve así:
# /etc/systemd/system/my-app.service.d/env.conf
[Service]
Environment="APP_ENV=production"
EnvironmentFile=/etc/my-app/runtime.env
EnvironmentFile=-/etc/my-app/local.env
runtime.env contiene valores requeridos. local.env es opcional y permite a un operador anular una configuración durante una ventana de mantenimiento sin editar la unidad principal. Después de un cambio:
sudo systemctl daemon-reload
sudo systemctl restart my-app.service
sudo journalctl -u my-app.service -n 50 --no-pager
El hábito más seguro es simple: mantén los valores predeterminados no sensibles en la unidad o en un archivo de configuración normal, mantén los secretos fuera de las unidades propiedad del paquete, bloquea cualquier archivo que contenga credenciales y verifica el entorno cargado sin filtrarlo a lugares donde no pertenece.
Errores Comunes Que Vale la Pena Evitar
El primer error es poner secretos en ExecStart=:
ExecStart=/usr/local/bin/my-app --db-password=s3cret
Eso parece inofensivo cuando tienes prisa, pero los argumentos de línea de comandos a menudo son más fáciles de exponer que los archivos de entorno. Pueden aparecer en listados de procesos, herramientas de monitoreo, historial de shell, informes de fallos o definiciones de servicio copiadas. Si la aplicación admite leer un archivo de configuración protegido, eso suele ser mejor. Si espera una variable de entorno, usa un EnvironmentFile= protegido y mantén el valor fuera de la línea de comandos.
El segundo error es editar la unidad del proveedor directamente. Una actualización de paquete puede reemplazar el archivo, y el próximo reinicio puede eliminar silenciosamente tus configuraciones de entorno. Usa un drop-in:
sudo systemctl edit my-app.service
Luego agrega solo la anulación local:
[Service]
EnvironmentFile=/etc/my-app/my-app.env
El tercer error es asumir que el servicio ve el mismo entorno de shell que ves en tu terminal. Generalmente no es así. Tu shell interactivo puede tener variables de .bashrc, .profile, una sesión SSH o una herramienta de despliegue. Un servicio del sistema se inicia desde el entorno gestionado de systemd. Si la aplicación necesita PATH, JAVA_HOME, NODE_ENV, LD_LIBRARY_PATH o un valor similar, defínelo explícitamente o usa rutas absolutas.
Por ejemplo, esto es frágil:
ExecStart=npm start
Esto es más fácil de razonar:
WorkingDirectory=/opt/my-app
Environment="NODE_ENV=production"
ExecStart=/usr/bin/npm start
El cuarto error es hacer que el archivo de entorno sea escribible por el usuario del servicio cuando no es necesario. Una aplicación web que puede sobrescribir su propio archivo de entorno puede convertir un error normal de aplicación en un problema de persistencia. En muchas configuraciones, el usuario del servicio debería leer datos de la aplicación y escribir registros o cargas, pero no debería poder reescribir las credenciales utilizadas para iniciar el servicio.
Cuándo las Variables de Entorno Son la Herramienta Incorrecta
Las variables de entorno son populares porque son simples, pero no siempre son la mejor interfaz. Si un valor es grande, estructurado, se rota a menudo o es compartido por varios servicios, un archivo de configuración real o un almacén de secretos suele ser más fácil de gestionar.
Una URL de base de datos es una variable de entorno razonable:
DATABASE_URL=postgresql://[email protected]:5432/app
Un documento JSON completo de cuenta de servicio es menos agradable. Las comillas se vuelven incómodas, los saltos de línea accidentales causan fallos, y es más probable que la gente lo pegue en registros mientras depura. En ese caso, almacena el JSON en un archivo protegido y pasa la ruta del archivo:
GOOGLE_APPLICATION_CREDENTIALS=/etc/my-app/google-service-account.json
Luego protege el archivo JSON por separado:
sudo chown root:my-app /etc/my-app/google-service-account.json
sudo chmod 640 /etc/my-app/google-service-account.json
Esto no hace que el secreto sea mágico. La aplicación aún puede leerlo. Root aún puede leerlo. Pero evita meter un secreto complejo en el analizador de entorno de systemd y hace que la auditoría a nivel de archivo sea más clara.
Una Lista de Verificación de Revisión Más Segura
Antes de reiniciar un servicio que usa variables de entorno, verifica cuatro cosas:
systemctl cat my-app.service
sudo ls -l /etc/my-app/my-app.env
sudo systemd-analyze verify /etc/systemd/system/my-app.service
sudo systemctl daemon-reload
systemctl cat confirma qué drop-ins están activos. ls -l confirma que los permisos son los que pretendías. systemd-analyze verify puede detectar algunos problemas de sintaxis de unidad antes de reiniciar. No validará todas las configuraciones específicas de la aplicación, pero sigue siendo una barrera de seguridad útil.
Después del reinicio, verifica el journal en busca de errores de inicio:
sudo systemctl restart my-app.service
sudo journalctl -u my-app.service -n 100 --no-pager
Si necesitas confirmar que una variable se cargó, consúltala cuidadosamente y redacta la salida antes de compartirla. Para servicios sensibles, prefiero verificar primero una variable no secreta como APP_ENV o LOG_LEVEL. Si esa se cargó desde el mismo archivo, la ruta del archivo y la sintaxis del analizador probablemente sean correctas, y puede que no necesites imprimir los valores que contienen secretos.
Un último punto práctico: planifica la rotación antes de que la necesites. Si una contraseña o token está almacenado en un archivo de entorno, anota qué servicio debe reiniciarse después de que el valor cambie y si ese reinicio causa tiempo de inactividad. Una credencial que es fácil de establecer pero difícil de rotar eventualmente se convertirá en un incidente. Para servicios pequeños, el runbook de rotación puede ser solo cuatro líneas:
sudoedit /etc/my-app/my-app.env
sudo systemctl restart my-app.service
sudo systemctl status my-app.service
sudo journalctl -u my-app.service -n 50 --no-pager
Eso es suficiente si todos conocen el radio de explosión. Para sistemas más grandes, prefiere credenciales que puedan superponerse durante la rotación, para que puedas desplegar el nuevo valor, verificarlo y eliminar el valor antiguo sin una ventana de interrupción apresurada.