systemd permite ejecutar aplicaciones con usuarios dedicados, reinicios controlados, límites de recursos, credenciales aisladas y restricciones sobre el sistema de archivos..
Una unidad bien configurada no sustituye la seguridad de la aplicación, pero reduce su superficie de acceso y facilita el diagnóstico operativo.
Cuándo conviene aplicarlo
Este enfoque resulta útil para:
- APIs propias.
- Workers y consumidores de colas.
- Bots internos.
- Agentes de monitoreo.
- Procesos que deben iniciar con el sistema.
- Aplicaciones instaladas directamente en una máquina virtual o servidor.
Si la aplicación ya se ejecuta dentro de un orquestador, evita duplicar innecesariamente las responsabilidades de reinicio y aislamiento.
Requisitos previos
Necesitarás:
- Un servidor Linux con
systemd. - El binario o script de la aplicación.
- Un comando de validación o prueba de configuración.
- Un usuario dedicado o
DynamicUser=. - Rutas de estado y caché identificadas.
- Dependencias externas conocidas.
- Un procedimiento de actualización y rollback.
Comprueba la versión disponible:
systemd --versionNo todas las directivas existen en versiones antiguas. Valida la unidad en la misma distribución donde se ejecutará.
Arquitectura de trabajo
La secuencia recomendada es:
- Instalar el binario con propiedad de
root. - Crear un usuario sin login.
- Definir la unidad y las rutas administradas.
- Incorporar las restricciones compatibles.
- Validar la sintaxis y la aplicación.
- Iniciar sin habilitar todavía el arranque automático.
- Comprobar estado, logs y recursos.
- Habilitar el servicio después de una prueba satisfactoria.
El proceso debe poder escribir únicamente en rutas expresamente previstas. Los logs deberían enviarse a stdout y stderr, que systemd-journald captura automáticamente.
Paso 1: instalar el binario de forma controlada
Crea el directorio y copia el ejecutable como root:
sudo install \
--directory \
--owner root \
--group root \
--mode 0755 \
/opt/app
sudo install \
--owner root \
--group root \
--mode 0755 \
./app \
/opt/app/appEl usuario del servicio debe poder ejecutar el binario, pero no modificarlo. Para despliegues con rollback, conserva versiones separadas y utiliza un enlace current administrado exclusivamente por root.
Si se trata de un script, ExecStart debe invocar explícitamente un intérprete instalado:
ExecStart=/usr/bin/python3 /opt/app/main.pyExecStart no interpreta tuberías, redirecciones ni sintaxis completa de shell. Evita agregar /bin/sh -c salvo que sea estrictamente necesario.
Paso 2: crear un usuario sin login
Localiza la ruta de nologin, que varía entre distribuciones:
command -v nologinCrea el usuario y su grupo:
sudo useradd \
--system \
--user-group \
--no-create-home \
--shell "$(command -v nologin)" \
appsvcComprueba el resultado:
getent passwd appsvc
getent group appsvcNo agregues este usuario a grupos privilegiados como sudo, docker o grupos con acceso amplio a dispositivos.
Para servicios simples también puede utilizarse DynamicUser=yes, combinado con StateDirectory= y RuntimeDirectory=. No lo mezcles con un usuario estático sin evaluar primero la propiedad de los datos persistentes.
Paso 3: separar configuración y secretos
Un archivo EnvironmentFile es adecuado para configuración no sensible:
sudo install \
--directory \
--owner root \
--group root \
--mode 0750 \
/etc/app
sudo install \
--owner root \
--group root \
--mode 0600 \
./app.env \
/etc/app/app.envEjemplo:
APP_LISTEN_ADDRESS=127.0.0.1:8080
APP_LOG_LEVEL=infoNo uses variables de entorno como almacén principal de contraseñas o tokens. Se heredan por el árbol de procesos y pueden quedar expuestas mediante interfaces de diagnóstico.
systemd ofrece credenciales que se entregan como archivos privados durante la activación del servicio. Documentación oficial de credenciales.
Prepara una credencial raíz:
sudo install \
--directory \
--owner root \
--group root \
--mode 0700 \
/etc/app/credentials
sudo install \
--owner root \
--group root \
--mode 0600 \
/ruta/segura/api-token \
/etc/app/credentials/api-tokenLa aplicación debería leer el token desde una ruta, no recibirlo como argumento. En sistemas compatibles también puedes utilizar systemd-creds y LoadCredentialEncrypted= para protegerlo cifrado en reposo.
Paso 4: crear una unidad completa
Guarda el siguiente contenido como app.service:
[Unit]
Description=Aplicación interna
Documentation=https://docs.ejemplo.com/app
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=exec
User=appsvc
Group=appsvc
WorkingDirectory=/opt/app
ExecStart=/opt/app/app
Restart=on-failure
RestartSec=5s
TimeoutStartSec=30s
TimeoutStopSec=30s
EnvironmentFile=/etc/app/app.env
LoadCredential=api-token:/etc/app/credentials/api-token
Environment=APP_TOKEN_FILE=%d/api-token
StateDirectory=app
StateDirectoryMode=0750
RuntimeDirectory=app
RuntimeDirectoryMode=0750
CacheDirectory=app
CacheDirectoryMode=0750
UMask=0077
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
LockPersonality=yes
CapabilityBoundingSet=
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
MemoryMax=512M
TasksMax=128
LimitNOFILE=8192
StandardOutput=journal
StandardError=journal
SyslogIdentifier=app
[Install]
WantedBy=multi-user.targetInstálala con propiedad de root:
sudo install \
--owner root \
--group root \
--mode 0644 \
app.service \
/etc/systemd/system/app.serviceType=exec permite que el arranque falle si systemd no puede ejecutar realmente el binario, por ejemplo, porque la ruta no existe o el usuario no está disponible.
Paso 5: administrar las rutas de escritura
StateDirectory=app crea /var/lib/app y asigna la propiedad al usuario del servicio. RuntimeDirectory=app crea /run/app, mientras que CacheDirectory=app administra /var/cache/app.
Estas rutas continúan siendo escribibles aunque se utilice:
ProtectSystem=strictEsto suele ser preferible a mantener una lista extensa de ReadWritePaths=. Opciones de ejecución y sandbox de systemd.
Si la aplicación necesita otra ruta, agrégala de forma específica:
ReadWritePaths=/srv/app/uploadsNo habilites /var, /opt o /etc completos. Comprueba primero la propiedad y el propósito de cada directorio.
Paso 6: adaptar el endurecimiento a la aplicación
Las opciones de aislamiento deben habilitarse progresivamente:
PrivateDevices=yespuede impedir el acceso a GPU, cámara o dispositivos seriales.RestrictAddressFamilies=debe incluir las familias de sockets realmente utilizadas.ProtectHome=yesimpide leer rutas de usuarios.CapabilityBoundingSet=vacío elimina capacidades Linux adicionales.ProtectSystem=strictvuelve el sistema de archivos de solo lectura.- Algunas aplicaciones JIT pueden ser incompatibles con restricciones adicionales como
MemoryDenyWriteExecute=yes.
No copies una lista de endurecimiento sin probar el servicio. Una puntuación más alta no compensa una aplicación que deja de funcionar o pierde datos.
Si la aplicación necesita escuchar en un puerto inferior a 1024, suele ser preferible utilizar un proxy. Como alternativa, concede únicamente CAP_NET_BIND_SERVICE, sin ejecutar todo el proceso como root.
Paso 7: validar antes de cargar la unidad
Comprueba el archivo:
sudo systemd-analyze verify \
/etc/systemd/system/app.serviceValida permisos y rutas:
namei -l /opt/app/app
sudo -u appsvc test -x /opt/app/app
sudo -u appsvc test -d /opt/appSi la aplicación ofrece una validación, ejecútala con el usuario real:
sudo -u appsvc \
/opt/app/app --check-configSustituye --check-config por el comando admitido. No inventes una validación basada solo en iniciar el servicio si la aplicación ofrece un mecanismo específico.
Paso 8: aplicar y comprobar
Recarga la configuración:
sudo systemctl daemon-reloadInicia el servicio sin habilitar todavía el arranque:
sudo systemctl start app.service
sudo systemctl is-active app.serviceRevisa el estado:
sudo systemctl status \
--no-pager \
--full \
app.serviceConsulta los logs del arranque actual:
sudo journalctl \
--unit app.service \
--boot \
--lines 100 \
--no-pagerSigue los eventos:
sudo journalctl \
--unit app.service \
--followDespués de comprobar la aplicación, habilita el arranque automático:
sudo systemctl enable app.serviceSeparar start de enable evita dejar activado al inicio un servicio que todavía no pasó las pruebas.
Paso 9: medir recursos y exposición
Consulta los valores efectivos:
sudo systemctl show app.service \
--property User \
--property Group \
--property MainPID \
--property MemoryCurrent \
--property MemoryPeak \
--property TasksCurrent \
--property NRestartsEvalúa el aislamiento:
sudo systemd-analyze security \
--no-pager \
app.servicesystemd-analyze security es una evaluación heurística de las restricciones aplicadas. No analiza vulnerabilidades del binario, permisos de negocio, protocolos ni secretos. Referencia de systemd-analyze.
Los límites MemoryMax=, TasksMax= y CPUQuota= se implementan mediante los controles de recursos de systemd y cgroups. Deben definirse después de medir el consumo normal y los picos. Controles de recursos de systemd.
Por ejemplo:
MemoryHigh=384M
MemoryMax=512M
CPUQuota=150%
TasksMax=128CPUQuota=100% equivale aproximadamente a la capacidad de un CPU, no al 100 % de toda una máquina multicore.
Paso 10: comprobar reinicios controlados
Con:
Restart=on-failure
RestartSec=5ssystemd reinicia el proceso después de una salida no satisfactoria o determinadas señales. Un systemctl stop intencional no dispara ese reinicio.
Las directivas:
StartLimitIntervalSec=60
StartLimitBurst=5evitan un bucle ilimitado. Si se supera el límite, la unidad entra en estado failed.
Diagnostica antes de reactivarla:
sudo systemctl status app.service
sudo journalctl -u app.service --since '-10 minutes'Después de corregir la causa:
sudo systemctl reset-failed app.service
sudo systemctl start app.serviceNo aumentes StartLimitBurst para ocultar un fallo persistente. Eso eleva el consumo y puede saturar dependencias externas.
Paso 11: probar parada y rollback
En staging, comprueba:
sudo systemctl stop app.service
sudo systemctl is-active app.serviceConfirma que:
- El proceso reciba
SIGTERM. - Termine antes de
TimeoutStopSec. - Libere sockets y locks.
- No pierda datos pendientes.
- No vuelva a iniciarse por
Restart=on-failure.
Para rollback:
- Detén el servicio.
- Restaura la versión anterior del binario o enlace
current. - Restaura la unidad anterior si también cambió.
- Ejecuta
systemd-analyze verify. - Ejecuta
daemon-reload. - Inicia y repite las pruebas.
Nunca reemplaces un binario en ejecución con permisos del propio usuario del servicio.
Problemas frecuentes
El servicio no puede escribir
Consulta las rutas administradas:
sudo systemctl show app.service \
--property StateDirectory \
--property RuntimeDirectory \
--property CacheDirectoryAgrega una ruta concreta mediante ReadWritePaths= solo si no corresponde a StateDirectory, RuntimeDirectory, CacheDirectory o LogsDirectory.
El servicio entra en un bucle de reinicio
Revisa el primer error, no únicamente el último:
sudo journalctl \
--unit app.service \
--since '-15 minutes' \
--no-pagerCorrige la causa y utiliza reset-failed. No elimines los límites de arranque.
Funciona manualmente, pero falla con systemd
La ejecución manual puede heredar PATH, directorio actual, permisos y variables de la shell. Define rutas absolutas y reproduce la prueba como appsvc.

No encuentra el secreto
Comprueba que la aplicación lea el archivo indicado por APP_TOKEN_FILE. La variable contiene una ruta, no el secreto.
No imprimas la credencial ni ejecutes systemd-creds cat durante diagnósticos normales.
El servicio no inicia después de endurecerlo
Desactiva temporalmente una directiva cada vez en staging. Revisa accesos a home, dispositivos, sockets, escritura y llamadas al kernel antes de decidir qué excepción necesita.
Advertencias de seguridad
- No ejecutes aplicaciones propias como
rootsin justificación. - Mantén unidad y binario bajo propiedad de
root. - No guardes secretos directamente en la unidad.
- Usa
EnvironmentFilesolo para configuración no sensible. - Declara rutas de escritura mínimas.
- No habilites aislamiento sin pruebas.
- Protege y respalda las credenciales necesarias para recuperación.
- Controla la retención y el espacio de
journald. - Revisa los permisos de sockets Unix creados por la aplicación.
Las credenciales de systemd se entregan como archivos privados durante la activación, no se propagan como variables por todo el árbol de procesos y pueden cifrarse mediante systemd-creds. Credenciales de sistema y servicios.
Buenas prácticas para producción
- Versiona las unidades junto con la aplicación.
- Valida con
systemd-analyze verify. - Inicia y comprueba antes de ejecutar
enable. - Utiliza
StateDirectory=para datos persistentes. - Envía logs estructurados a stdout y stderr.
- Configura límites a partir de métricas reales.
- Mantén reinicios y backoff acotados.
- Conserva una versión anterior para rollback.
- Prueba la parada ordenada.
- Revisa periódicamente
systemd-analyze security. - Documenta cada excepción de sandbox.
- Ejecuta las pruebas con el usuario real del servicio.