12

Systemd y Rust para Linuxeros

Vistas: 0
Systemd y Rust para Linuxeros

Hasta ahora has construido APIs REST, las has metido en contenedores Docker y les has puesto healthchecks y métricas. Tu código funciona, está observable y se despliega con un docker compose up -d.

Pero hay servicios que no necesitan HTTP. No necesitan un puerto abierto ni un balanceador delante. Necesitan ejecutarse en segundo plano, en la máquina host, vigilando el sistema, rotando logs, sincronizando archivos, o haciendo cualquier tarea periódica que antes hacías con cron.

Si vienes de Bash, piensa en esto como pasar de:

# Script que ejecutas a mano o con crontab
*/5 * * * * /home/usuario/check-disk.sh >> /var/log/checks.log

# Y tienes que acordarte de mirar el log
tail -f /var/log/checks.log

A esto:

# Daemon systemd que arranca solo, escribe a journald y se reinicia si falla
sudo systemctl start crustaceo-monitor
sudo journalctl -u crustaceo-monitor -f

En este capítulo construyes crustaceo-monitor, un daemon de monitoreo escrito en Rust que se instala como servicio systemd, escribe alertas a journald, responde a señales para graceful shutdown y checks inmediatos, y se configura con un archivo TOML.

¿Por qué systemd y no Docker para esto?

Antes de escribir una línea de código, tienes que entender cuándo usas Docker y cuándo usas systemd. No son excluyentes: se complementan.

CriterioDockersystemd
AislamientoCompleto (namespaces, cgroups)Parcial (hardening del unit)
Acceso al hardwareLimitado (hay que exponer)Directo
Dependencia del hostMínima (solo kernel)Total (systemd, journald, red local)
Arranque en bootDocker arranca despuéssystemd arranca en orden
Loggingdocker logs → stdoutjournalctl → journald
Monitorización del procesoHealthchecks HTTPSystemd watchdog, señales UNIX
ComplejidadMedia-alta (orquestación)Baja (unit file)

Para un daemon de monitoreo del sistema, la respuesta es clara: systemd. Necesitas acceso a /proc, /sys, a los discos montados, a los procesos del sistema. Meter eso en un contenedor añade complejidad sin beneficio real.

Máxima de atareao.es: «Docker para aislar servicios que sirven red. systemd para servicios que tocan el sistema.»

Dicho esto, si quieres que crustaceo-monitor se ejecute dentro de un contenedor —por ejemplo en un clúster de Kubernetes—, puedes hacerlo. Pero el escenario natural de este capítulo es un servidor Linux con systemd, como cualquier VPS de tu infraestructura self-hosted.

Daemonización: el mito del fork

Si vienes de Bash o de C clásico, igual piensas que un daemon necesita hacer fork(), que el proceso hijo herede el PID 1, y que el padre muera. Eso se llamaba «daemonización a la antigua» y se hacía así:

# Pseudocódigo de daemonización clásica
fork()           # Crear hijo
if padre: exit() # El padre muere
setsid()         # Nueva sesión, nuevo grupo de procesos
fork() again     # Segundo fork para asegurar que no retomamos terminal
chdir("/")       # No bloquear ningún mount point
umask(0)         # No heredar máscara del shell
close(stdin/stdout/stderr)  # Cerrar herederos de la terminal

Todo eso era necesario porque los daemon se lanzaban desde un terminal y tenían que desvincularse para no morir cuando el usuario cerraba la sesión.

Con systemd, todo eso sobra.

Cuando systemd ejecuta tu servicio:

  • Tu proceso se lanza en su propio cgroup, aislado de la terminal de usuario.
  • systemd captura stdout y lo redirige a journald automáticamente.
  • Si pones Type=simple (el valor por defecto), systemd considera que el servicio está «listo» en cuanto se ejecuta el binario.
  • Si tu proceso muere, systemd lo reinicia según la directiva Restart=.

No necesitas fork, no necesitas setsid, no necesitas cerrar fds. Escribes un programa normal que imprime a stdout y systemd se encarga del resto.

Tu main() en Rust es un fn main() normal, o un async fn main() con tokio. Nada especial. Sin magia de daemonización.

Logging a journald: stdout es suficiente

Una de las preguntas más frecuentes es: «¿necesito la crate journald para escribir al log del sistema?».

La respuesta corta es: no, si usas systemd.

Cuando un servicio systemd se ejecuta con Type=simple, systemd captura todo lo que el proceso escribe a stdout y lo inyecta en journald. Cada línea se convierte en una entrada del log con metadatos automáticos: PID, unit, timestamp, prioridad.

Esto significa que tu println!() en Rust ya es logging a journald. No necesitas librerías especiales.

Pero hay matices:

  • Prioridad: Por defecto, systemd asigna prioridad info a stdout y err a stderr. Puedes controlarlo con StandardOutput=journal y StandardError=journal en el unit file.
  • Estructura: Cada println! es una entrada separada. Si quieres logs multi-línea como una sola entrada, usa eprintln!.
  • Formato: Systemd parsea automáticamente logs estructurados si usas el formato KEY=VALUE. Pero para este tutorial, texto plano es más que suficiente.

Si en el futuro necesitas enviar logs estructurados con campos personalizados (como CHECK_NAME=disk, STATUS=CRITICAL), la crate journald te permite escribir directamente al socket de journald con metadatos arbitrarios. Pero para el 90% de los casos, println! basta.

// Esto ya es logging a journald cuando corres bajo systemd
println!("[2026-06-24 10:00:00] ALERT | disk | Disco al 95%");

// Si quieres prioridad de error, usa stderr
eprintln!("[2026-06-24 10:00:00] CRITICAL | memory | Memoria agotada");

Manejo de señales: graceful shutdown y SIGUSR1

Un daemon bien educado responde a las señales del sistema. Cuando el administrador ejecuta systemctl stop, systemd envía SIGTERM al proceso. Si tu daemon no lo captura, el kernel lo mata con SIGKILL a los pocos segundos y no tienes oportunidad de cerrar recursos, escribir un estado final o notificar a otros procesos.

Las señales que te importan en este proyecto:

SeñalPropósitoAcción en crustaceo-monitor
SIGTERMApagado ordenado desde systemdActivar flag SHUTDOWN, salir del bucle, terminar con 0
SIGINTCtrl+C desde terminalIgual que SIGTERM (para pruebas en desarrollo)
SIGHUPRecarga tradicional de daemonsForzar comprobación inmediata, como si fuera un nuevo ciclo
SIGUSR1Señal de usuario definida por el programadorForzar comprobación inmediata sin esperar al intervalo

Cómo funciona en el código

Usas tokio::signal::unix que es la forma canónica de manejar señales en una aplicación tokio asíncrona. No necesitas la crate signal-hook porque tokio ya incluye soporte para señales UNIX en su módulo tokio::signal::unix.

El patrón es simple pero efectivo:

use tokio::signal::unix::{signal, SignalKind};

let mut sigterm = signal(SignalKind::terminate())?;
let mut sigusr1 = signal(SignalKind::user_defined1())?;

tokio::spawn(async move {
    loop {
        tokio::select! {
            _ = sigterm.recv() => {
                SHUTDOWN.store(true, Ordering::Relaxed);
                break;
            }
            _ = sigusr1.recv() => {
                FORCE_CHECK.store(true, Ordering::Relaxed);
            }
        }
    }
});

Cada SignalKind crea un receptor. Cuando la señal llega, el select! despierta la rama correspondiente. No hay polling, no hay CPU perdida — tokio usa signalfd en Linux internamente, que es un mecanismo eficiente del kernel.

El truco de los flags atómicos

En lugar de usar canales (que complican el código), usamos dos AtomicBool globales. Son estáticos, no necesitan referencia compartida, y su acceso es prácticamente gratuito en términos de rendimiento.

static SHUTDOWN: AtomicBool = AtomicBool::new(false);
static FORCE_CHECK: AtomicBool = AtomicBool::new(false);

El bucle principal del daemon itera cada segundo (no espera el intervalo entero) precisamente para poder reaccionar rápido a estas señales:

for _ in 0..interval {
    if SHUTDOWN.load(Ordering::Relaxed) {
        break;  // Salir ordenadamente
    }
    if FORCE_CHECK.swap(false, Ordering::Relaxed) {
        run_checks(&config, &mut sys);  // Check inmediato
    }
    sleep(Duration::from_secs(1)).await;
}

El Ordering::Relaxed es suficiente aquí porque la única sincronización que necesitamos es que el valor se propague entre hilos en algún momento. No hay operaciones complejas de lectura-modificación-escritura que requieran ordenar memoria.

Probar las señales manualmente

Cuando el daemon está corriendo, puedes probar las señales desde otra terminal:

# Obtener el PID del daemon
PID=$(systemctl show -p MainPID --value crustaceo-monitor)
echo $PID

# Enviar SIGUSR1 para check inmediato
kill -SIGUSR1 $PID

# Ver la respuesta en los logs
journalctl -u crustaceo-monitor -n 3
# Deberías ver: ⚡ Recibida SIGUSR1 — forzando comprobación inmediata

Esta capacidad de reaccionar a SIGUSR1 es extremadamente útil en entornos de producción: puedes forzar una comprobación sin tener que cambiar la configuración, sin reiniciar el servicio, y sin esperar al siguiente intervalo. Por ejemplo, si sospechas que un servicio acaba de caer, envías SIGUSR1 y ves el resultado en los logs al instante.

Variables de entorno desde el unit file

Systemd permite inyectar variables de entorno en el servicio de dos formas:

Environment=

Directiva directa en el unit file. Útil para valores cortos y fijos.

[Service]
Environment=RUST_BACKTRACE=1
Environment=RUST_LOG=crustaceo_monitor=info

EnvironmentFile=

Apuntar a un archivo con pares CLAVE=valor. Útil para secretos o configuraciones que no quieres en el repositorio.

[Service]
EnvironmentFile=/etc/crustaceo-monitor/env

El archivo /etc/crustaceo-monitor/env contiene:

RUST_BACKTRACE=1
RUST_LOG=crustaceo_monitor=debug

Tu programa Rust accede a estas variables con std::env::var("RUST_LOG") como siempre.

Hardening del servicio

Systemd tiene un sistema de hardening que te permite restringir lo que el servicio puede hacer. Es una capa de seguridad adicional que complementa los permisos de usuario y las capabilities de Linux.

En el unit file de crustaceo-monitor aplicamos estas directivas:

[Service]
CapabilityBoundingSet=CAP_DAC_OVERRIDE CAP_SYS_PTRACE
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallArchitectures=native
SystemCallFilter=@system-service
LockPersonality=true
RemoveIPC=true
PrivateDevices=true

Cada una hace lo siguiente:

  • CapabilityBoundingSet: Limita las capabilities del kernel que el proceso puede usar. Solo permitimos las mínimas: CAP_DAC_OVERRIDE (acceso a archivos del sistema para leer /etc/crustaceo-monitor/config.toml) y CAP_SYS_PTRACE (lectura de /proc para métricas de CPU y procesos con sysinfo). Si no necesitas métricas de procesos de otros usuarios, puedes quitar la segunda.
  • NoNewPrivileges: Impide que el proceso o sus hijos escalen privilegios vía sudo, setuid, etc. Esto es crítico: incluso si el binario tuviera una vulnerabilidad que intente ejecutar sudo, systemd lo bloquea.
  • PrivateTmp: Cada ejecución tiene su propio /tmp montado como bind mount privado. No comparte archivos temporales con otros procesos del sistema, eliminando una clase completa de ataques (race conditions en /tmp).
  • ProtectSystem=strict: El sistema de archivos raíz se monta como solo lectura. El proceso no puede escribir en /usr, /etc (a no ser que se marquen explícitamente con ReadWritePaths=). Esto protege la integridad del sistema incluso si el proceso es comprometido.
  • ProtectHome: /home, /root y /run/user son inaccesibles. El proceso no puede leer tus claves SSH, tu historial de bash, ni ningún otro dato de usuario.
  • ProtectKernelTunables: /sys y /proc/sys se montan como solo lectura. El proceso no puede modificar parámetros del kernel.
  • ProtectKernelModules: No se pueden cargar ni descargar módulos del kernel. Un proceso comprometido no puede cargar un módulo malicioso.
  • ProtectControlGroups: No se pueden modificar los cgroups. Un proceso no puede escapar de sus limitaciones de recursos.
  • MemoryDenyWriteExecute: Impide crear páginas de memoria que sean simultáneamente escribibles y ejecutables. Esto bloquea una técnica común de explotación de vulnerabilidades.
  • RestrictRealtime: No se puede usar planificación en tiempo real (SCHED_RR, SCHED_FIFO). Evita que un proceso monopolice la CPU.
  • RestrictAddressFamilies: Solo se permiten las familias de direcciones especificadas. Para crustaceo-monitor, que necesita socket UNIX (para systemctl is-active) y posiblemente red local (AF_INET/AF_INET6), esta lista es suficiente. Si solo hicieras comprobaciones locales, podrías limitarte a AF_UNIX.
  • SystemCallArchitectures: Solo se permiten llamadas al sistema de la arquitectura nativa (x86_64 en nuestro caso). Esto bloquea cualquier intento de ejecutar código de 32 bits, que a veces se usa para saltarse ciertas restricciones de seguridad.
  • SystemCallFilter=@system-service: Whitelist de llamadas al sistema permitidas. El grupo @system-service incluye las syscalls que necesita un servicio normal (read, write, open, close, etc.) y bloquea cientos de syscalls peligrosas (kexec, reboot, bpf, etc.).
  • LockPersonality: Impide cambiar la personalidad del proceso (un mecanismo de compatibilidad de Linux que permite ejecutar binarios de otras arquitecturas). Otra superficie de ataque eliminada.
  • RemoveIPC: Limpia los recursos IPC (semáforos, colas de mensajes, memoria compartida) cuando el proceso termina. No queda basura en el kernel.
  • PrivateDevices: El proceso solo ve un /dev mínimo: null, zero, full, random, urandom, tty, ptmx. No tiene acceso a discos, particiones, dispositivos de audio, webcams, etc.

El impacto de todo esto es que crustaceo-monitor se ejecuta en una jaula virtual muy restrictiva. Si alguien comprometiera el binario (por ejemplo, mediante una vulnerabilidad de seguridad), el atacante tendría que saltarse todas estas capas para hacer algo útil. No podría leer /home, no podría escribir en /etc, no podría cargar módulos del kernel, no podría ejecutar binarios con setuid, y no podría usar syscalls peligrosas.

Puedes ver el nivel de seguridad asignado por systemd con:

systemd-analyze security crustaceo-monitor
# → Exposure: 1.2 SAFE 😊

La escala va de 0 (máxima seguridad) a 10 (ninguna protección). Un servicio sin hardening marca típicamente 8-9. Nuestro crustaceo-monitor, con todas las directivas, obtiene una puntuación cercana a 1.

Consejo: No copies y pegues el hardening sin entenderlo. Cada directiva puede romper algo. Si tu daemon necesita leer archivos de /etc, ProtectSystem=strict lo bloqueará. Si necesita crear sockets, PrivateDevices puede interferir. Siempre prueba el servicio después de aplicar hardening y mira los logs de systemd para ver si algo falla: journalctl -u crustaceo-monitor -xe.

Proyecto: crustaceo-monitor

Llegó la hora de construir. El proyecto completo:

crustaceo-monitor/
├── Cargo.toml
├── src/
│   ├── main.rs        # Punto de entrada, bucle principal, señales
│   ├── config.rs      # Carga de configuración TOML
│   └── checks.rs      # Comprobaciones: disco, memoria, CPU, servicios
├── crustaceo-monitor.service      # Unit file del servicio
├── crustaceo-monitor.timer        # Unit file del timer (opcional)
├── crustaceo-monitor.example.toml # Configuración de ejemplo
└── install.sh                     # Script de instalación

Dependencias (Cargo.toml)

[package]
name = "crustaceo-monitor"
version = "0.1.0"
edition = "2021"
description = "Daemon de monitoreo para systemd: disco, memoria, CPU y servicios"

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
anyhow = "1"
chrono = "0.4"
colored = "2"
tokio = { version = "1", features = ["full"] }
sysinfo = "0.32"

Usamos toml con serde para la configuración, sysinfo para las métricas del sistema, chrono para timestamps, colored para logs formateados en terminal, y tokio para el bucle asíncrono y el manejo de señales.

Estructura del código

src/config.rs — Carga la configuración desde un archivo TOML. Si el archivo no existe, usa valores por defecto con un mensaje de aviso. El struct Config tiene tres secciones:

  • [general]: interval_seconds (cada cuánto comprobar) y log_alerts_only (si solo mostrar alertas).
  • [thresholds]: disk_percent, memory_percent, cpu_percent — los umbrales de alerta.
  • [services]: lista opcional de servicios systemd a monitorear.

La función Config::load() es simple pero robusta: si el archivo no existe, no crashea, usa defaults. Esto es importante porque el daemon debe poder arrancar aunque la configuración no esté perfecta.

src/checks.rs — Cuatro funciones de comprobación que devuelven CheckResult:

  • check_disk(): Itera sobre los montajes con sysinfo::Disks, calcula el porcentaje de uso.
  • check_memory(): Lee memoria total y usada con sysinfo::System.
  • check_cpu(): Lee uso global de CPU.
  • check_service(): Ejecuta systemctl is-active <nombre> y parsea la salida.

Cada resultado tiene un campo CheckStatus que puede ser Ok, Warning o Critical. La función has_critical() permite detectar si hay alertas graves en un ciclo.

src/main.rs — El corazón del daemon:

  1. Parsea argumentos (--config para ruta alternativa).
  2. Carga la configuración.
  3. Inicializa sysinfo.
  4. Registra manejadores de señales con tokio::signal::unix.
  5. Entra en un bucle que cada interval_seconds ejecuta las comprobaciones.
  6. Cada segundo comprueba flags atómicos para SIGUSR1 (check inmediato) o SIGTERM (shutdown).
  7. Al recibir señal de parada, cierra ordenadamente.

El código completo de cada archivo no lo repetimos aquí porque está en el repositorio del tutorial, pero el flujo es el que ves.

Configuración de ejemplo

# ─────────────────────────────────────────────────────────
# crustaceo-monitor — Configuración de ejemplo
# Copiar a /etc/crustaceo-monitor/config.toml y ajustar
# ─────────────────────────────────────────────────────────

[general]
# Intervalo entre comprobaciones en segundos (por defecto: 60)
interval_seconds = 60

# Si true, solo muestra alertas (omite checks OK)
log_alerts_only = false

[thresholds]
# Umbral de uso de disco (%)
disk_percent = 90.0

# Umbral de uso de memoria (%)
memory_percent = 85.0

# Umbral de uso de CPU (%)
cpu_percent = 90.0

# Servicios systemd a monitorear (opcional)
services = ["sshd", "postgresql", "nginx"]

Unit file del servicio

[Unit]
Description=crustaceo-monitor — Daemon de monitoreo del sistema
Documentation=https://atareao.es/tutorial/rust/
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/crustaceo-monitor --config /etc/crustaceo-monitor/config.toml
User=crustaceo
Group=crustaceo
Restart=on-failure
RestartSec=5
TimeoutStopSec=10

# Variables de entorno
Environment=RUST_BACKTRACE=1
Environment=RUST_LOG=crustaceo_monitor=info

# ── Hardening del servicio ──────────────────────────────
CapabilityBoundingSet=CAP_DAC_OVERRIDE CAP_SYS_PTRACE
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallArchitectures=native
SystemCallFilter=@system-service
LockPersonality=true
RemoveIPC=true
PrivateDevices=true

[Install]
WantedBy=multi-user.target

Fíjate en los detalles:

  • Type=simple: Systemd no espera ningún mensaje de «ready». En cuanto ejecuta el binario, el servicio está «activo» para systemd.
  • User/Group crustaceo: El servicio corre con un usuario del sistema sin permisos de shell. Se crea durante la instalación.
  • Restart=on-failure: Si el binario crashea, systemd lo reinicia automáticamente tras 5 segundos.
  • TimeoutStopSec=10: Systemd envía SIGTERM y espera 10 segundos. Si no termina, envía SIGKILL.

Unit file del timer (opcional)

Si prefieres no tener el daemon corriendo permanentemente, puedes usar un timer systemd que ejecute el servicio periódicamente:

[Unit]
Description=crustaceo-monitor — Ejecución periódica opcional
Documentation=https://atareao.es/tutorial/rust/

[Timer]
OnCalendar=hourly
Persistent=true
RandomizedDelaySec=60

[Install]
WantedBy=timers.target

Con este timer, crustaceo-monitor se ejecuta cada hora (con un retardo aleatorio de hasta 60 segundos para evitar picos). Tras la ejecución, termina. No es un daemon persistente, sino una tarea periódica al estilo cron, pero gestionada por systemd.

¿Cuándo usar uno u otro?

  • Daemon permanente (el servicio sin timer): Para monitoreo en tiempo real. Te enteras de problemas en segundos.
  • Ejecución periódica (servicio + timer): Para tareas que no necesitan inmediatez. Menos recursos consumidos.

Puedes tener ambos instalados y activar/desactivar según necesites.

Script de instalación

El script install.sh automatiza todo el proceso:

#!/usr/bin/env bash
set -euo pipefail

BINARY="crustaceo-monitor"
BIN_SRC="target/release/${BINARY}"
BIN_DST="/usr/local/bin/${BINARY}"
CONFIG_DIR="/etc/${BINARY}"
SERVICE_DST="/etc/systemd/system/${BINARY}.service"
TIMER_DST="/etc/systemd/system/${BINARY}.timer"

# 1. Copiar binario
sudo cp "$BIN_SRC" "$BIN_DST"
sudo chmod 755 "$BIN_DST"

# 2. Crear usuario del servicio
if ! id -u crustaceo &>/dev/null; then
    sudo useradd --system --no-create-home --shell /usr/sbin/nologin crustaceo
fi

# 3. Copiar configuración de ejemplo
sudo mkdir -p "$CONFIG_DIR"
sudo cp "${BINARY}.example.toml" "${CONFIG_DIR}/config.toml"

# 4. Copiar unit files
sudo cp "${BINARY}.service" "$SERVICE_DST"
sudo cp "${BINARY}.timer" "$TIMER_DST"

# 5. Recargar y habilitar
sudo systemctl daemon-reload
sudo systemctl enable "${BINARY}.service"

echo "✅ Instalación completada."
echo "   sudo systemctl start crustaceo-monitor"
echo "   sudo journalctl -u crustaceo-monitor -f"

El script completo está en install.sh dentro del proyecto.

Compilar e instalar

El proceso completo de compilación e instalación:

# 1. Clonar o copiar el proyecto
cd crustaceo-monitor

# 2. Compilar en release (el binario pesa ~1.9 MB)
cargo build --release

# 3. Ejecutar el script de instalación
chmod +x install.sh
sudo ./install.sh

# 4. Editar la configuración (ajustar umbrales, servicios a monitorear)
sudo nano /etc/crustaceo-monitor/config.toml

# 5. Iniciar el servicio
sudo systemctl start crustaceo-monitor

# 6. Verificar que arrancó
sudo systemctl status crustaceo-monitor

Pruebas en desarrollo sin systemd

No necesitas instalar el servicio para probar el daemon durante el desarrollo. Puedes ejecutarlo directamente desde tu terminal:

# Probar la compilación
cargo build

# Ejecutar con un archivo de configuración local (si no existe, usa defaults)
cargo run -- --config ./crustaceo-monitor.example.toml

# El daemon arranca y empieza a mostrar las comprobaciones cada 60s
# Para salir: Ctrl+C (envía SIGINT, el daemon hace graceful shutdown)

# Para probar con un intervalo más corto, crea un config temporal
cat > /tmp/test-config.toml << 'EOF'
[general]
interval_seconds = 5
log_alerts_only = false

[thresholds]
disk_percent = 95.0
memory_percent = 95.0
cpu_percent = 95.0
EOF

cargo run -- --config /tmp/test-config.toml

Durante el desarrollo también puedes probar el envío de señales. Abre dos terminales:

# Terminal 1: ejecuta el daemon
cargo run -- --config /tmp/test-config.toml

# Terminal 2: envía señales
# Forzar comprobación inmediata (simula SIGUSR1)
kill -SIGUSR1 $(pgrep crustaceo-monitor)

# Detener graceful (simula systemctl stop)
kill -SIGTERM $(pgrep crustaceo-monitor)

Esta forma de trabajar acelera el desarrollo: compilas, ejecutas, ves los logs en tiempo real, y matas con Ctrl+C. Sin systemd, sin sudo, sin instalación.


## Verificación

Una vez instalado, verifica que todo funciona:

### Estado del servicio

```bash
sudo systemctl status crustaceo-monitor

La salida debería mostrar algo como:

● crustaceo-monitor.service - crustaceo-monitor — Daemon de monitoreo del sistema
     Loaded: loaded (/etc/systemd/system/crustaceo-monitor.service; enabled; preset: enabled)
     Active: active (running) since Wed 2026-06-24 10:00:00 CEST; 1min ago
   Main PID: 12345 (crustaceo-monito)
      Tasks: 4 (limit: 9382)
     Memory: 3.2M
        CPU: 12ms
     CGroup: /system.slice/crustaceo-monitor.service
             └─12345 /usr/local/bin/crustaceo-monitor --config /etc/crustaceo-monitor/config.toml

jun 24 10:00:00 servidor crustaceo-monitor[12345]: 🚀 crustaceo-monitor iniciado — PID 12345 — intervalo 60s — config: /etc/crustaceo-monitor/config.toml
jun 24 10:00:00 servidor crustaceo-monitor[12345]: ...comprobaciones iniciales...
jun 24 10:01:00 servidor crustaceo-monitor[12345]: [2026-06-24 10:01:00] OK | disk | Disco OK — pico de uso 45.2%
jun 24 10:01:00 servidor crustaceo-monitor[12345]: [2026-06-24 10:01:00] OK | cpu | CPU OK — 12.3% de uso
jun 24 10:01:00 servidor crustaceo-monitor[12345]: [2026-06-24 10:01:00] ALERT | memory | Memoria al 87.5% — usado: 6.3 GB / 7.2 GB

Logs en tiempo real

# Ver logs en tiempo real del servicio
sudo journalctl -u crustaceo-monitor -f

# Ver logs desde el arranque
sudo journalctl -u crustaceo-monitor --since "1 hour ago"

# Ver solo alertas (críticas)
sudo journalctl -u crustaceo-monitor -p err

Probar señales

# Forzar una comprobación inmediata con SIGUSR1
sudo kill -SIGUSR1 $(systemctl show -p MainPID --value crustaceo-monitor)

# Ver en los logs cómo responde
sudo journalctl -u crustaceo-monitor -n 5
# Deberías ver: "⚡ Recibida SIGUSR1 — forzando comprobación inmediata"

# Apagar el servicio graceful
sudo systemctl stop crustaceo-monitor

# Ver en los logs la parada ordenada
sudo journalctl -u crustaceo-monitor -n 5
# Deberías ver: "⏻  Apagando crustaceo-monitor..."

Probar el timer (opcional)

# Si instalaste el timer
sudo systemctl enable --now crustaceo-monitor.timer

# Ver timers activos
sudo systemctl list-timers --all | grep crustaceo

# Forzar ejecución del timer (sin esperar a la hora)
sudo systemctl start crustaceo-monitor.service

Probar la comprobación de servicios

Si configuraste servicios en el TOML:

# Ejemplo de salida si nginx está caído
sudo journalctl -u crustaceo-monitor | grep "service/"
# [2026-06-24 10:01:00] ALERT | service/nginx | Servicio nginx NO activo (estado: inactive)

Verificar el hardening

# Systemd permite ver qué protecciones están activas
systemd-analyze security crustaceo-monitor

La salida muestra una puntuación de 0 (máxima seguridad) a 10 (ninguna). Nuestro servicio debería obtener una puntuación excelente, cerca de 0-1, indicando que el hardening es muy estricto.

systemd-analyze security crustaceo-monitor
# → Exposure: 1.2 SAFE 😊

Conclusión

En este capítulo hemos visto,

  • Escribir un daemon de sistema en Rust que corre como servicio systemd, sin necesidad de daemonización manual (fork, setsid, etc.).
  • Manejar señales UNIX (SIGTERM, SIGINT, SIGHUP, SIGUSR1) con tokio::signal para graceful shutdown y acciones bajo demanda.
  • Escribir a journald simplemente usando println!() — systemd redirige stdout automáticamente.
  • Configurar un servicio systemd con unit file: Type=simple, ExecStart, User, Restart, variables de entorno.
  • Crear un timer systemd como alternativa ligera al daemon permanente, con OnCalendar=hourly.
  • Hardening de servicio con CapabilityBoundingSet, ProtectSystem=strict, PrivateTmp, NoNewPrivileges, y una docena de directivas más de seguridad.
  • Cargar configuración TOML con serde para umbrales, intervalos y listas de servicios a monitorear.
  • Construir un script de instalación que copia el binario, crea el usuario, instala los unit files y habilita el servicio.

Tu código Rust ya no es una herramienta que ejecutas desde la terminal. Es un ciudadano de primera clase en systemd: arranca con el sistema, escribe al log centralizado, respeta las señales del sistema operativo, y se comporta como cualquier otro servicio nativo de Linux.

# Resumen: tu daemon Rust, instalado y funcionando
sudo systemctl status crustaceo-monitor
# ● active (running) — PID 12345 — 3.2M RAM — monitoreando disco, CPU, memoria y servicios

Más información

Deja una respuesta