Las dos lecciones anteriores han supuesto que el atacante entra por la puerta: que intenta autenticarse, que abusa de una regla de sudo, que usa una credencial que no le corresponde. Esta lección se ocupa de lo contrario: de lo que ocurre cuando nadie llama a la puerta porque el fallo está dentro, en el código que ya corre, y basta con enviarle unos bytes bien elegidos para que haga algo que nadie previó.

El escenario de Meteora es realista. meteo-api acepta peticiones HTTP de Internet; el ingestor acepta conexiones de estaciones meteorológicas en un puerto abierto; los tres procesos dependen de bibliotecas escritas por terceros que se actualizan solas cada pocas semanas. Ninguna credencial protege esas tres superficies: están abiertas por definición, porque su trabajo es aceptar entrada de fuera. La pregunta ya no es "¿quién puede entrar?", sino "¿qué pasa cuando la entrada es maliciosa y el programa que la procesa tiene un fallo?".

La lección tiene tres partes. Primero, conocer al adversario: un panorama ordenado de las amenazas reales de un servidor, cada una con el indicio observable que dejaría en meteo-01, y el modelo de amenazas de Meteora como ejercicio estructurado. Segundo, entender el fallo: las clases de vulnerabilidad de programa explicadas conceptualmente —qué se corrompe y por qué—, siempre cerradas con la forma correcta de escribir el código, y las defensas que el sistema operativo pone por debajo (ASLR, NX, canarios, PIE, RELRO) con el comando exacto para comprobar que están activas. Y tercero, endurecer: la lista completa y ordenada de medidas que convierten un Debian recién instalado en meteo-01, cada una con su comando y su justificación, desde minimizar paquetes hasta la prueba de restauración de una copia de seguridad.

Advertencia previa, que vale para toda la lección. Los mecanismos de ataque se describen a nivel conceptual y con fines defensivos: no encontrarás instrucciones de explotación ni código funcional de ataque, porque no hacen falta para defenderse. Cualquier prueba de seguridad —un escaneo de puertos, una auditoría con lynis, una comprobación de configuración— debe realizarse exclusivamente sobre sistemas propios o con autorización expresa y por escrito del responsable. Y las decisiones con implicaciones legales o regulatorias (RGPD, retención de datos, notificación de brechas) deben revisarse con un profesional de cumplimiento normativo o con asesoría jurídica.

Contenido

  1. Panorama de amenazas y su indicio observable
  2. El modelo de amenazas de Meteora
  3. Clases de vulnerabilidad de programa
  4. Las defensas del sistema operativo y cómo comprobarlas
  5. Minimizar la superficie: paquetes, servicios y puertos
  6. Actualizaciones de seguridad y su automatización
  7. Cortafuegos con nftables
  8. Endurecimiento del acceso remoto
  9. Aislamiento del servicio con systemd
  10. Opciones de montaje y límites de recursos
  11. Cifrado en reposo, en tránsito y gestión de secretos
  12. Copias de seguridad como control de seguridad
  13. Marcos de referencia y verificación
  14. Lista de comprobación de meteo-01

Panorama de amenazas y su indicio observable

Conocer la lista de amenazas sirve de poco si no sabes qué aspecto tiene cada una desde dentro del servidor. Por eso cada fila lleva su indicio: lo que verías en meteo-01 si estuviera ocurriendo.

Amenaza En qué consiste Indicio observable en meteo-01
Malware Código que ejecuta acciones no deseadas Binario desconocido en /tmp, /dev/shm o /var/tmp; proceso sin paquete que lo respalde
Ransomware Cifra los datos y exige un rescate Pico de escrituras en /var/lib/meteora; ficheros .dat con extensión nueva; nota de rescate; copias borradas
Rootkit de usuario Sustituye binarios (ps, ls, netstat) para ocultar actividad ps no muestra un proceso que sí aparece en /proc; dpkg --verify marca binarios alterados
Rootkit de núcleo Módulo que manipula el propio núcleo Módulo no firmado en lsmod; discrepancias entre herramientas; muy difícil de detectar desde el propio sistema
Puerta trasera Acceso persistente al margen de la autenticación Puerto en escucha inesperado; clave nueva en authorized_keys; usuario con UID 0; unidad de systemd desconocida
Criptominería Usa tu CPU para minar criptomonedas CPU al 100 % de forma sostenida sin carga que lo explique; conexiones salientes a puertos de pools
Botnet El servidor pasa a formar parte de una red de ataque Tráfico saliente masivo; conexiones a servidores de control; picos de red sin origen conocido
Denegación de servicio Agota un recurso hasta que el servicio deja de responder Descriptores agotados (EMFILE); memoria al límite; cola de conexiones llena; meteo-api sin responder
Robo de datos Extracción de la información Transferencia saliente grande e inusual; lecturas masivas de .dat; conexión a un destino nuevo
Cadena de suministro El código malicioso llega por una dependencia legítima Ninguno evidente: por eso es la más peligrosa. Solo se ve fijando versiones y verificando firmas

Tres observaciones que ordenan la tabla y que conviene tener claras antes de seguir.

Casi todas las amenazas comparten cuatro indicios. Un proceso que no deberías tener, un puerto que no deberías tener abierto, un fichero que no deberías tener y una conexión saliente que no deberías tener. Eso simplifica enormemente la detección: en lugar de buscar cada amenaza por separado, se vigilan esos cuatro ejes contra una línea base conocida. Es exactamente la misma estrategia de "detección por diferencia" de 05-02.

El rootkit de núcleo rompe la lógica del resto. Si el atacante controla el núcleo, controla también lo que ven ps, ls, netstat y cualquier herramienta que ejecutes: no puedes confiar en las respuestas del sistema que estás investigando. Es el argumento decisivo para la telemetría externa —enviar los registros fuera de la máquina— y para analizar en frío desde un medio de arranque limpio. Lo desarrollamos en Auditoría, Registros y Respuesta a Incidentes.

La cadena de suministro es la que no deja indicio. Cuando el código malicioso llega dentro de una actualización legítima de una biblioteca que usas, firmada correctamente por su repositorio, ninguna vigilancia del host lo distingue de una actualización normal. Las defensas son de otro tipo: fijar versiones exactas, verificar las firmas del repositorio, revisar qué dependencias entran realmente, minimizar el número de ellas, y reducir el daño posible con el confinamiento del apartado 9.

El modelo de amenazas de Meteora

Un modelo de amenazas es un ejercicio estructurado de media hora que responde a tres preguntas: qué proteges, de quién y por dónde puede entrar. Sin él, el endurecimiento se convierte en una lista de recetas sin criterio para priorizar.

Activos —lo que hay que proteger, con la razón—:

Activo Por qué importa Impacto si se pierde o se filtra
Las lecturas históricas (.dat) Es el producto de la empresa Pérdida irrecuperable si no hay copias; ventaja para un competidor
secrets.conf (claves de API) Da acceso a servicios de terceros Suplantación de Meteora ante el proveedor; coste económico
Disponibilidad de meteo-api Los clientes pagan por ella Incumplimiento de contrato; pérdida de clientes
Integridad de los datos Un dato falso es peor que ningún dato Decisiones erróneas de los clientes; pérdida de credibilidad
El propio servidor Recurso de cómputo y red Uso para minería o como origen de ataques a terceros, con responsabilidad legal

Adversarios —quién ataca, con qué motivación y qué recursos—:

Adversario Motivación Recursos Probabilidad
Automatizado (bots) Cualquier máquina vale Escaneo masivo, fallos conocidos Continua: es el 99 % del ruido
Oportunista Beneficio rápido Herramientas públicas Alta
Dirigido Los datos de Meteora en concreto Tiempo, dinero, quizá fallos desconocidos Baja, alto impacto
Interno Descontento, error, negligencia Acceso legítimo Media, y la más difícil de detectar

Superficies de entrada —por dónde se llega—:

Superficie Expuesta a Qué la protege hoy Riesgo residual
Puerto de estaciones (9010/TCP) Red de estaciones Cortafuegos por origen, TLS mutuo Estación comprometida enviando datos falsos
API HTTP (443/TCP) Internet TLS, autenticación, límite de peticiones Fallo en el análisis de peticiones
SSH (22/TCP) Red de gestión Claves, AllowGroups, sin contraseñas Clave privada robada del portátil de un administrador
Dependencias El repositorio y sus proveedores Firmas, versiones fijadas Cadena de suministro
Acceso físico y consola Centro de datos Control de acceso, LUKS Robo del disco

Esta tabla es la que decide dónde invertir el esfuerzo. Contra el adversario automatizado, que es el 99 % del tráfico hostil, la defensa efectiva y barata es no tener nada obvio abierto y estar al día de actualizaciones: cortafuegos, sin contraseñas en SSH, parches. Contra el dirigido, hace falta confinamiento y detección. Y contra el interno, mínimo privilegio, separación de funciones y auditoría, que ninguna otra capa cubre.

Clases de vulnerabilidad de programa

Aquí explicamos qué se corrompe y por qué, porque sin entender el mecanismo no se entiende la defensa. No hay código explotable: cada apartado termina con la forma correcta de escribir el programa.

Desbordamiento de búfer

Recuerda de 02-03 cómo está organizada la pila de una función: las variables locales, el puntero de marco guardado y la dirección de retorno, que es la posición del código a la que se volverá cuando la función termine. Están contiguos en memoria, y esa contigüidad es todo el problema.

/* ❌ VULNERABLE: el ingestor procesando el nombre de una estación */
void registrar_estacion(const char *entrada) {
    char nombre[32];              /* 32 bytes en la pila */
    strcpy(nombre, entrada);      /* copia HASTA EL '\0': no mira el tamaño */
    ...
}   /* al volver, la CPU salta a la dirección de retorno guardada */

Si entrada mide 200 bytes, strcpy escribe 200 bytes donde solo caben 32. Los 168 sobrantes no desaparecen: sobrescriben lo que sigue en la pila, incluida la dirección de retorno. Cuando la función termina, la CPU salta a donde diga esa dirección, que ahora contiene lo que el atacante puso. Ha logrado desviar el flujo de ejecución sin ninguna credencial. La causa raíz es siempre la misma: una copia cuyo tamaño lo decide el atacante y no el programa.

/* ✔ CORRECTO: el tamaño lo decide el programa, no la entrada */
void registrar_estacion(const char *entrada) {
    char nombre[32];
    if (snprintf(nombre, sizeof nombre, "%s", entrada) >= (int) sizeof nombre) {
        registrar_error("nombre de estacion demasiado largo");   /* rechazar */
        return;
    }
    ...
}

Las tres reglas que evitan la familia entera: usar funciones con límite (snprintf, strncat, memcpy con tamaño calculado) en vez de strcpy, strcat, sprintf o gets; derivar el límite de sizeof del destino, nunca de la longitud de la entrada; y comprobar el valor de retorno, porque truncar en silencio es un fallo distinto pero también un fallo. En Python, Go, Rust o Java esta clase no existe porque el lenguaje comprueba los límites; en C y C++ es responsabilidad del programador, y por eso es la vulnerabilidad más rentable de la historia del software.

Cadenas de formato

printf(mensaje_del_usuario);          /* ❌ la entrada ES la cadena de formato */
printf("%s", mensaje_del_usuario);    /* ✔ la entrada es un ARGUMENTO */

La diferencia parece cosmética y es abismal. Si el texto del usuario contiene especificadores como %x o %n, la primera versión los interpreta: printf lee argumentos que nadie pasó —revelando contenido de la pila, incluidos punteros y datos— y %n llega a escribir en memoria. La regla es absoluta y fácil de auditar: la cadena de formato siempre es una constante del programa; los datos externos van siempre como argumento. Compila con -Wformat -Wformat-security y el compilador te avisa.

Inyección de comandos

Es la clase más frecuente hoy, y la que más aparece en código de administración.

# ❌ VULNERABLE: construir una orden de shell concatenando datos externos
estacion = request.args.get("estacion")
os.system(f"grep {estacion} /var/lib/meteora/lecturas/2026-08-31.dat")

El problema no es grep: es que el texto se entrega a una shell, y la shell interpreta ;, |, &&, $(...) y comodines antes de ejecutar nada. Un valor como x; comando-arbitrario se convierte en dos órdenes, la segunda elegida por quien envió la petición, ejecutada con la identidad de meteora. La causa raíz: mezclar código y datos en la misma cadena, exactamente igual que en la inyección SQL.

# ✔ CORRECTO: sin shell, y con los argumentos SEPARADOS
import subprocess, re
if not re.fullmatch(r"[A-Za-z0-9_-]{1,32}", estacion):
    return "estacion no valida", 400
subprocess.run(["grep", "--", estacion, "/var/lib/meteora/lecturas/2026-08-31.dat"],
               shell=False, check=False, timeout=10)

Tres defensas independientes, y cada una tapa algo distinto. La lista de argumentos hace que no exista shell: el sistema ejecuta execve("/usr/bin/grep", ["grep", "--", "PATIO-01", ...]) y el contenido de estacion es un argumento, no código, por muchos ; que lleve. El -- impide que un valor que empiece por - se interprete como opción de grep. Y la validación por lista blanca rechaza lo que no tenga la forma esperada, en vez de intentar enumerar lo prohibido —que siempre olvida algo—. La regla general: nunca shell=True, nunca os.system, nunca concatenar; y en C, execve con argv separado en lugar de system().

TOCTOU: tiempo de comprobación frente a tiempo de uso

Ya apareció en 04-04 y en 05-01 como el enemigo de la mediación completa. El patrón vulnerable es siempre el mismo: comprobar algo sobre un nombre de fichero y luego actuar sobre ese nombre, porque entre las dos operaciones el atacante puede cambiar a qué apunta el nombre.

# ❌ VULNERABLE: comprueba el NOMBRE, actúa sobre el NOMBRE
if os.access(ruta, os.W_OK):          # 1. comprobación
    with open(ruta, "w") as f:        # 2. uso  ← la ventana está entre 1 y 2
        f.write(datos)

# ✔ CORRECTO: abrir primero, y actuar sobre el DESCRIPTOR
fd = os.open(ruta, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o640)
with os.fdopen(fd, "w") as f:
    f.write(datos)

La versión correcta aplica la lección de 05-01: un descriptor de fichero es una capacidad, una llave sobre un objeto concreto. Una vez abierto, el descriptor sigue apuntando al mismo inodo aunque alguien renombre el fichero o sustituya el nombre por un enlace, así que no hay ventana. O_EXCL hace fallar la operación si el destino ya existe y O_NOFOLLOW impide seguir un enlace simbólico. Es especialmente crítico en directorios compartidos y escribibles por varios, que es la razón de PrivateTmp=yes del apartado 9.

Desreferencia tras liberar

En C y C++, liberar memoria con free() no borra el puntero: sigue apuntando al mismo sitio, pero ese sitio ya no es tuyo. Si el programa lo usa después, lee o escribe en una zona que el asignador puede haber reutilizado para otra cosa —y si el atacante consigue que su dato acabe ahí, el programa opera sobre contenido que él controla. La disciplina que lo evita es sencilla y hay que aplicarla siempre: poner el puntero a NULL inmediatamente después de liberar, no liberar dos veces, y dejar claro en cada estructura quién es el dueño de cada bloque de memoria. Herramientas como valgrind y los sanitizers del compilador (-fsanitize=address) detectan estos fallos durante las pruebas, y usarlas en integración continua es de las inversiones más rentables que existen.

Las defensas del sistema operativo y cómo comprobarlas

El sistema operativo no puede impedir que un programa tenga fallos, pero sí encarecer mucho su aprovechamiento. Estas son las capas que aporta, y cómo comprobar que están activas en tu máquina.

Defensa Qué hace Qué ataque encarece
ASLR Coloca pila, montículo y bibliotecas en direcciones aleatorias en cada ejecución El atacante no sabe a qué dirección saltar
KASLR Lo mismo para el propio núcleo Ataques contra el núcleo
NX / W^X Marca la pila y los datos como no ejecutables Ejecutar código depositado en la pila
Canario de pila Valor aleatorio entre las variables y la dirección de retorno; se comprueba al salir Detecta el desbordamiento antes de saltar
PIE Binario reubicable, para que ASLR se aplique también al programa Reutilizar el código del propio binario
RELRO Marca de solo lectura las tablas de enlace tras cargarlas Sobrescribir punteros de funciones
seccomp Reduce las llamadas al sistema disponibles (05-01) Todo lo que venga después del control del flujo
# ASLR del sistema: 2 = completo (valor correcto)
cat /proc/sys/kernel/randomize_va_space          # → 2

# Comprobar que las direcciones cambian entre ejecuciones
for i in 1 2 3; do ldd /usr/bin/meteo-api | grep libc; done | awk '{print $4}'

# Protecciones compiladas en un binario concreto
checksec --file=/usr/bin/meteo-api
# RELRO: Full RELRO | Canary: found | NX: enabled | PIE: PIE enabled

Cómo se lee esa salida. Full RELRO significa que las tablas de enlace quedan de solo lectura tras la carga; Partial deja parte escribible y No RELRO es una señal de que el binario se compiló sin las protecciones habituales. Canary: found indica que el compilador insertó la comprobación de pila —se obtiene con -fstack-protector-strong, activo por defecto en Debian—. NX: enabled es la pila no ejecutable. Y PIE enabled permite que ASLR aleatorice también la posición del código del programa; sin PIE, el binario siempre está en la misma dirección y buena parte de ASLR se pierde. Si compilas software propio para meteo-01, las banderas mínimas son:

gcc -O2 -fstack-protector-strong -D_FORTIFY_SOURCE=2 \
    -fPIE -pie -Wl,-z,relro,-z,now -Wformat -Wformat-security \
    -o meteo-api meteo-api.c

-D_FORTIFY_SOURCE=2 sustituye funciones peligrosas por versiones que comprueban tamaños cuando el compilador los conoce; -Wl,-z,now fuerza la resolución completa de símbolos al cargar, que es lo que hace posible el RELRO total.

Y la advertencia que impide un exceso de confianza: son capas, no soluciones. Cada una encarece un paso, y las técnicas para sortear cada una individualmente son conocidas desde hace años. Su valor está en la combinación —y en que, junto con seccomp y AppArmor, convierten un fallo que antes daba el control de la máquina en, con suerte, un servicio que se cae y se reinicia—. Ninguna sustituye a corregir el fallo, y por eso el apartado siguiente empieza por las actualizaciones.

Minimizar la superficie: paquetes, servicios y puertos

El principio es de 05-01: el código que no está instalado no tiene vulnerabilidades. Un Debian mínimo trae unos 400 paquetes; una instalación con "entorno de escritorio" marcado por descuido, más de 1.500. Cada paquete de más es código que puede tener fallos, que hay que actualizar y que amplía lo que un atacante encuentra al llegar.

# ¿Qué está instalado y qué ocupa? (lo más grande primero)
dpkg-query -Wf '${Installed-Size}\t${Package}\n' | sort -rn | head -25

# ¿Qué servicios se ejecutan realmente?
systemctl list-units --type=service --state=running

# ¿Qué está en escucha, en qué interfaz y qué proceso lo abrió?
sudo ss -tulnp
# LISTEN 0 128 0.0.0.0:22    users:(("sshd",pid=812,fd=3))
# LISTEN 0 511 0.0.0.0:443   users:(("meteo-api",pid=1481,fd=6))
# LISTEN 0 128 10.0.5.11:9010 users:(("ingestor",pid=1402,fd=4))
# LISTEN 0 128 127.0.0.1:5432 users:(("postgres",pid=901,fd=5))

La salida de ss -tulnp es la superficie de red real del servidor, y hay que saber leer la columna de dirección, que es donde está la información importante. 0.0.0.0:443 significa "escucha en todas las interfaces", correcto para una API pública. 10.0.5.11:9010 escucha solo en la interfaz de la red de estaciones, de modo que la API pública no puede alcanzar ese puerto: es una restricción gratuita y muy efectiva. Y 127.0.0.1:5432 escucha solo en local, así que la base de datos es inalcanzable desde fuera aunque el cortafuegos fallara. La regla que se deduce: cada servicio debe escuchar en la interfaz mínima que necesite, y esa configuración es una capa independiente del cortafuegos.

# Desactivar un servicio que no se necesita (y que no vuelva al reiniciar)
sudo systemctl disable --now avahi-daemon
sudo apt purge avahi-daemon                       # mejor: quitarlo del disco
sudo apt autoremove --purge                       # dependencias huérfanas

disable --now para el servicio y evita que arranque; purge va más lejos y elimina el software y su configuración. La distinción importa: un servicio desactivado sigue en el disco y puede reactivarse por una actualización o por un error de configuración. Y apt autoremove --purge limpia dependencias que ya nadie usa, que suelen ser el grueso del exceso.

Actualizaciones de seguridad y su automatización

Es, con diferencia, la medida con mejor relación entre esfuerzo y protección. La inmensa mayoría de los compromisos reales de servidores no aprovechan fallos desconocidos, sino fallos publicados y corregidos hace semanas o meses en sistemas que nadie actualizó.

sudo apt install unattended-upgrades apt-listchanges
sudo dpkg-reconfigure -plow unattended-upgrades

# /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Origins-Pattern {
    "origin=Debian,codename=${distro_codename}-security";   # SOLO seguridad
};
Unattended-Upgrade::Mail "[email protected]";
Unattended-Upgrade::Automatic-Reboot "false";               # decidir a mano
Unattended-Upgrade::Remove-Unused-Dependencies "true";

# Verificar que funciona de verdad
sudo unattended-upgrade --dry-run --debug
grep -c . /var/log/unattended-upgrades/unattended-upgrades.log

Las tres decisiones de esa configuración. Solo el origen -security, no todas las actualizaciones: los parches de seguridad de Debian estable son mínimos y muy conservadores, mientras que actualizar todo automáticamente introduce cambios funcionales que pueden romper el servicio de madrugada. Automatic-Reboot "false" porque un reinicio no anunciado de meteo-01 es una interrupción del servicio; el reinicio se planifica, y /var/run/reboot-required indica cuándo hace falta. Y el correo de aviso, porque una automatización que nadie vigila acaba fallando en silencio: el fallo más común es un apt bloqueado por una actualización a medias que lleva meses sin aplicarse.

Para el núcleo, además, existe la aplicación de parches en caliente (kpatch, livepatch), que evita reinicios; es útil en entornos donde la disponibilidad manda, y no sustituye a reiniciar de vez en cuando con un núcleo nuevo.

Cortafuegos con nftables

El cortafuegos aplica denegar por defecto al tráfico de red: se enumera lo permitido y se rechaza el resto, incluido lo que aún no existe. En Debian actual el motor es nftables; ufw es una interfaz sencilla sobre lo mismo y es perfectamente válida para casos simples.

#!/usr/sbin/nft -f
# /etc/nftables.conf — cortafuegos de meteo-01
flush ruleset

table inet filter {
  chain entrada {
    type filter hook input priority 0; policy drop;      # ← DENEGAR POR DEFECTO

    ct state established,related accept                  # respuestas a lo que pedimos
    ct state invalid drop
    iif lo accept                                        # tráfico local

    ip protocol icmp icmp type echo-request limit rate 5/second accept

    # SSH: SOLO desde la red de gestión, con límite de intentos nuevos
    ip saddr 10.0.1.0/24 tcp dport 22 ct state new limit rate 6/minute accept

    # API pública: abierta a Internet, con límite por origen
    tcp dport 443 ct state new limit rate 30/second accept

    # Estaciones: solo su red, y solo hacia el puerto del ingestor
    ip saddr 10.0.5.0/24 tcp dport 9010 ct state new accept

    counter comment "denegado por defecto"               # cuenta lo descartado
  }

  chain salida {
    type filter hook output priority 0; policy accept;   # ver comentario abajo
  }

  chain reenvio {
    type filter hook forward priority 0; policy drop;    # no somos un router
  }
}

Cada regla responde a una decisión del modelo de amenazas. policy drop en la cadena de entrada es el principio de valores por defecto seguros: si mañana alguien arranca un servicio nuevo, no queda expuesto por accidente. ct state established,related accept permite las respuestas al tráfico que nosotros iniciamos, y va la primera porque es la regla que más paquetes casa —el orden importa para el rendimiento—. SSH restringido por origen elimina de golpe el ruido de los escaneos de Internet, y el límite de 6 conexiones nuevas por minuto encarece cualquier intento repetitivo. El limit del 443 es una defensa básica frente a saturación. La regla de las estaciones aplica la superficie de la tabla de amenazas: solo esa red, solo ese puerto. Y el counter final es un detalle de diagnóstico muy útil: te dice cuánto tráfico se está descartando, que es lo primero que querrás saber cuando algo "no conecta".

La cadena de salida merece un comentario aparte. Dejarla en accept es lo habitual y lo cómodo. Ponerla en drop y enumerar los destinos permitidos es notablemente más seguro —corta la exfiltración de datos, las conexiones a servidores de control y la descarga de herramientas por parte de un proceso comprometido, tres filas enteras de la tabla de amenazas—, y es también bastante más trabajo de mantener: hay que permitir DNS, NTP, el repositorio de paquetes y las API de las que dependas. Para un servidor con datos valiosos como meteo-01, merece la pena.

sudo nft -c -f /etc/nftables.conf      # validar SIN aplicar
sudo systemctl enable --now nftables
sudo nft list ruleset                  # ver las reglas activas
sudo nft list ruleset | grep counter   # ¿cuánto se está descartando?

Antes de aplicar reglas en un servidor remoto, valida con nft -c, y protégete de un error con un mecanismo de reversión automática: por ejemplo, sudo sh -c 'sleep 300 && nft flush ruleset' & antes de aplicar, cancelándolo si todo va bien. Bloquearte a ti mismo con tu propio cortafuegos es un clásico.

Endurecimiento del acceso remoto

La configuración de SSH ya se justificó línea a línea en Usuarios, Autenticación y Escalada de Privilegios, así que aquí solo recogemos el conjunto mínimo y lo que añade a lo ya visto:

PermitRootLogin no                  # trazabilidad: cada acción, con nombre propio
PasswordAuthentication no           # elimina TODA la familia de ataques de contraseña
KbdInteractiveAuthentication no
AllowGroups admins                  # lista blanca de acceso
MaxAuthTries 3
AllowAgentForwarding no
X11Forwarding no

Lo que conviene añadir en el nivel de endurecimiento del sistema son dos capas complementarias, porque protegen de cosas distintas: la restricción por origen en el cortafuegos del apartado anterior, que hace que el servicio ni siquiera sea alcanzable desde Internet; y un bloqueo dinámico como fail2ban, que observa los registros y bloquea temporalmente los orígenes con muchos fallos. Con PasswordAuthentication no, fail2ban aporta poco contra el compromiso —no hay contraseña que probar— pero sigue siendo útil para reducir el ruido en los registros, que es un beneficio real: un registro lleno de miles de intentos automatizados esconde el intento que sí importa.

Aislamiento del servicio con systemd

Aquí es donde se materializa todo lo de 05-01. Systemd permite confinar un servicio con una docena de directivas, sin tocar el código de la aplicación y sin escribir una política de MAC completa. Esta es la unidad de meteo-api, comentada:

# /etc/systemd/system/meteo-api.service
[Unit]
Description=API de consultas meteorológicas de Meteora
After=network-online.target

[Service]
Type=notify
ExecStart=/usr/bin/meteo-api --config /etc/meteora/meteora.conf
Restart=on-failure
RestartSec=5

# --- IDENTIDAD (05-02) ---
User=meteora
Group=meteora
UMask=0027                          # los ficheros nacen 0640 (04-06)

# --- PRIVILEGIO (05-01) ---
NoNewPrivileges=yes                 # NUNCA puede ganar privilegio, ni por setuid
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE    # puerto 443 sin ser root

# --- SISTEMA DE FICHEROS ---
ProtectSystem=strict                # TODO el sistema, de solo lectura
ProtectHome=yes                     # /home, /root y /run/user: invisibles
PrivateTmp=yes                      # /tmp propio y aislado (mínimo mecanismo común)
ReadWritePaths=/var/log/meteora /run/meteora
ReadOnlyPaths=/var/lib/meteora/lecturas /etc/meteora
ProtectProc=invisible               # no ve los procesos de otros usuarios
PrivateDevices=yes                  # sin acceso a dispositivos físicos

# --- NÚCLEO Y MEMORIA ---
ProtectKernelTunables=yes           # /proc/sys y /sys, de solo lectura
ProtectKernelModules=yes            # no puede cargar ni descargar módulos
ProtectKernelLogs=yes
ProtectControlGroups=yes
MemoryDenyWriteExecute=yes          # ninguna página escribible Y ejecutable (W^X)
LockPersonality=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes                # no puede crear ficheros setuid

# --- RED ---
PrivateNetwork=no                   # necesita red
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
IPAddressDeny=any
IPAddressAllow=10.0.0.0/8 127.0.0.0/8    # y lo que la API deba alcanzar

# --- LLAMADAS AL SISTEMA (seccomp, 05-01) ---
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @mount @module @debug @reboot @swap @obsolete
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM

# --- RECURSOS (defensa frente a agotamiento) ---
LimitNOFILE=8192
LimitNPROC=64
MemoryMax=2G
TasksMax=128

[Install]
WantedBy=multi-user.target

Las directivas que más valor aportan, y qué ataque concreto detiene cada una. ProtectSystem=strict deja todo el sistema de ficheros en solo lectura y obliga a declarar en ReadWritePaths exactamente dónde se escribe: es una lista blanca del sistema de ficheros que ningún permiso DAC puede contradecir, así que un proceso comprometido no puede modificar binarios ni dejar nada persistente fuera de dos rutas. NoNewPrivileges=yes impide toda ganancia de privilegio posterior, incluida la de ejecutar un binario setuid del sistema; corta la escalada por esa vía aunque exista un setuid vulnerable. MemoryDenyWriteExecute=yes impide que exista una página a la vez escribible y ejecutable, lo que bloquea la técnica habitual de generar código en memoria y saltar a él. PrivateTmp=yes da un /tmp propio, eliminando de un golpe los ataques por ficheros temporales y el TOCTOU en directorios compartidos. IPAddressDeny=any con lista blanca es el cortafuegos de salida aplicado a un solo servicio: aunque el proceso caiga, no puede llamar a casa. Y SystemCallFilter es seccomp sin escribir una línea de C.

sudo systemd-analyze security meteo-api      # puntuación 0 (mejor) a 10 (peor)
sudo systemctl daemon-reload && sudo systemctl restart meteo-api
sudo journalctl -u meteo-api -p err -n 50    # ¿algo dejó de funcionar?

systemd-analyze security es la herramienta clave de este apartado: analiza la unidad, puntúa su exposición y enumera exactamente qué directivas faltan, ordenadas por impacto. Una unidad sin endurecer saca alrededor de 9,5; con las directivas anteriores baja a la zona de 1,5 a 2,5. Aplícalas de forma incremental, comprobando el servicio tras cada bloque: si meteo-api deja de arrancar, casi siempre es porque necesita escribir en una ruta que falta en ReadWritePaths, y journalctl -p err lo dice.

Opciones de montaje y límites de recursos

Las opciones de montaje de 04-03 son endurecimiento puro, y aquí encajan en su sitio:

# /etc/fstab
UUID=a1b2...  /var/lib/meteora  ext4  defaults,noatime,nosuid,nodev,data=ordered  0 2
UUID=c3d4...  /var/log          ext4  defaults,nosuid,nodev,noexec               0 2
tmpfs         /dev/shm          tmpfs defaults,nosuid,nodev,noexec,size=512M     0 0
tmpfs         /tmp              tmpfs defaults,nosuid,nodev,noexec,size=1G       0 0

Las tres opciones cierran tres vectores por el precio de tres palabras. nosuid hace que el núcleo ignore los bits setuid y setgid en todo el volumen, de modo que un binario setuid depositado ahí no sirve de nada. nodev ignora los ficheros de dispositivo, impidiendo que alguien cree un /var/lib/meteora/disco que dé acceso directo al disco crudo. Y noexec impide ejecutar cualquier binario del volumen: es exactamente lo que quieres en /tmp, /dev/shm y /var/log, porque son los sitios donde un proceso comprometido puede escribir. Fíjate en que /var/lib/meteora no lleva noexec por coherencia con su uso —solo guarda datos—, aunque añadirlo también sería razonable; lo que no puede llevar es noatime fuera, porque esa opción es de rendimiento y ya está justificada desde 04-03.

Los límites de recursos son la defensa frente al agotamiento, que es la fila de "denegación de servicio" de la tabla de amenazas:

# Límites por servicio (preferible): en la unidad, como arriba
LimitNOFILE=8192 ; LimitNPROC=64 ; MemoryMax=2G ; TasksMax=128

# Límites por usuario: /etc/security/limits.conf (los aplica pam_limits, 05-02)
meteora  hard  nproc   128
meteora  hard  nofile  8192
*        hard  core    0        # sin volcados de memoria: pueden contener secretos

Por qué importan. Sin LimitNOFILE, un fallo que abra descriptores sin cerrarlos agota el límite global del sistema y deja sin descriptores a los demás servicios: un fallo local se convierte en una caída general. LimitNPROC y TasksMax frenan la creación descontrolada de procesos. MemoryMax evita que un consumo desbocado active el OOM killer de 02-04, que podría matar otro proceso distinto del culpable. Y core 0 merece atención especial: un volcado de memoria contiene todo lo que el proceso tenía en RAM, incluidas las claves de secrets.conf recién leídas, y suele acabar en un directorio poco protegido o en un sistema de análisis de fallos.

Cifrado en reposo, en tránsito y gestión de secretos

En reposo: LUKS. El cifrado de disco protege de un escenario muy concreto y muy real: que alguien obtenga el disco físico —robo, retirada de hardware, un disco defectuoso que se devuelve al fabricante sin borrar—.

sudo cryptsetup luksFormat --type luks2 /dev/md0
sudo cryptsetup open /dev/md0 meteora-datos
sudo mkfs.ext4 /dev/mapper/meteora-datos
sudo cryptsetup luksDump /dev/md0 | head -20      # ver cabecera y ranuras de clave

Lo importante es saber de qué no protege, para no crear falsa seguridad: con el sistema arrancado y el volumen abierto, los datos están accesibles como cualquier otro fichero, así que LUKS no protege en absoluto frente a un meteo-api comprometido ni frente a un atacante con acceso al sistema en marcha. Protege el disco apagado. Y trae una consecuencia operativa que hay que decidir de antemano: un volumen cifrado necesita la clave en cada arranque, así que o alguien la teclea —y el servidor no arranca solo tras un corte de luz— o se guarda en un TPM o en un servidor de claves, con sus propias implicaciones.

En tránsito: TLS. Todo lo que salga o entre por la red debe ir cifrado y autenticado: la API con TLS 1.2 como mínimo —preferiblemente 1.3—, certificados renovados automáticamente, y TLS mutuo en el puerto de las estaciones, que además autentica a la estación y ataja la fila "estación comprometida" del modelo de amenazas. Los protocolos en claro —telnet, FTP, HTTP para datos, SNMP v1 y v2— no tienen sitio en un servidor moderno.

Gestión de secretos. /etc/meteora/secrets.conf con modo 0600 (o 0640 con grupo meteora, como fijamos en 04-06) es el mínimo aceptable, no una buena solución. Sus límites son concretos: el secreto está en claro en el disco, entra en las copias de seguridad, cualquiera con root lo lee, y rotarlo exige tocar el servidor.

Enfoque Ventaja Inconveniente
Fichero 0600 Simple, sin dependencias En claro en disco y en las copias; rotación manual
LoadCredential= de systemd El secreto solo es visible para ese servicio Sigue estando en el disco
Gestor de secretos (Vault y similares) Rotación, auditoría de accesos, secretos temporales Infraestructura adicional; hay que protegerla igual
TPM o módulo de seguridad La clave no sale nunca del hardware Coste, complejidad, dependencia del equipo

Y tres reglas que valen siempre, sea cual sea el enfoque: nunca en el código ni en el repositorio —quedaría en el historial de Git para siempre, aunque lo borres después—; nunca en variables de entorno, porque son visibles en /proc/<pid>/environ y se filtran a los procesos hijos y a los volcados de error; y rotables, porque un secreto que no se puede rotar sin una parada planificada acaba no rotándose nunca.

Copias de seguridad como control de seguridad

Las copias no suelen aparecer en las listas de seguridad, y son la única defensa real contra el ransomware y contra el borrado destructivo. La regla clásica sigue siendo la mejor guía:

Regla 3-2-1: 3 copias de los datos, en 2 medios distintos, con 1 fuera del emplazamiento.

Aplicada a Meteora: el original en el RAID 1 de /var/lib/meteora; una copia diaria en un almacenamiento distinto del mismo centro, para restaurar rápido; y una copia remota, para el caso de incendio, inundación o compromiso del centro entero. Y el matiz que la actualiza a la era del ransomware:

Copias inmutables. Si el atacante llega con privilegios, buscará las copias antes de cifrar nada, porque unas copias accesibles hacen inútil su extorsión. De ahí las tres propiedades que debe tener el destino: credenciales distintas de las del servidor —nunca las mismas claves, ni un montaje permanente escribible—; modelo de solo añadir, de modo que el servidor pueda crear copias pero no borrar ni sobrescribir las anteriores; y retención forzada por el sistema de destino durante un plazo fijo, no por una política que el atacante pueda cambiar.

# Copia con verificación y retención (ejemplo con restic)
export RESTIC_REPOSITORY="s3:s3.example.com/meteora-backups"
restic backup /var/lib/meteora /etc/meteora --tag diaria
restic forget --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --prune
restic check --read-data-subset=5%       # verifica que los datos son legibles

# LA PRUEBA QUE CASI NADIE HACE: restaurar de verdad
restic restore latest --target /tmp/prueba-restauracion
diff <(sha256sum /var/lib/meteora/lecturas/2026-08-31.dat | cut -d' ' -f1) \
     <(sha256sum /tmp/prueba-restauracion/var/lib/meteora/lecturas/2026-08-31.dat | cut -d' ' -f1)

La prueba de restauración es la parte no negociable de todo esto. Una copia que nunca se ha restaurado no es una copia: es una suposición. Los fallos que aparecen en la primera restauración real son siempre los mismos y siempre en el peor momento —la clave de cifrado del repositorio no está documentada, faltaba un directorio en la lista de rutas, el proceso tarda catorce horas y nadie lo sabía, o los permisos y las ACL se perdieron porque la herramienta no los guardaba (04-06)—. Programa una restauración de prueba trimestral, con su tiempo medido y su verificación de sumas, y documenta el procedimiento paso a paso.

Marcos de referencia y verificación

No hace falta inventar el endurecimiento desde cero: existen guías públicas y revisadas por mucha gente.

Marco o herramienta Qué aporta Cómo se usa
CIS Benchmarks Guía detallada por sistema operativo, con justificación y comprobación de cada punto Como línea base; se adapta, no se aplica a ciegas
STIG (DISA) Equivalente de ámbito gubernamental, muy estricto En entornos regulados
lynis Auditoría automática del host con puntuación y sugerencias sudo lynis audit system
debsecan Vulnerabilidades conocidas de los paquetes instalados debsecan --suite bookworm --format detail
systemd-analyze security Exposición de cada unidad, con las directivas que faltan Por servicio
OpenSCAP Comprobación automatizada de perfiles de cumplimiento En entornos con auditoría formal
sudo apt install lynis debsecan
sudo lynis audit system            # informe con "warnings" y "suggestions"
sudo debsecan --suite bookworm --only-fixed --format detail

Dos advertencias sobre estas herramientas. Aplicar un benchmark completo a ciegas rompe servicios: los CIS Benchmarks incluyen recomendaciones pensadas para perfiles muy distintos, y algunas son incompatibles con lo que meteo-01 necesita hacer. Se leen, se decide punto por punto, y se documentan las excepciones con su justificación, que es lo que después permite defender la configuración ante una auditoría. Y una puntuación alta de lynis no es seguridad: mide la conformidad con una lista, no la resistencia a un adversario; sirve para encontrar lo que se te ha olvidado, no para declarar el sistema seguro.

Autorización previa. Ejecutar lynis o cualquier escáner sobre tu servidor es normal y recomendable. Ejecutar cualquier herramienta de análisis —incluido un simple escaneo de puertos— contra un sistema que no es tuyo requiere autorización expresa y por escrito del responsable, con un alcance y unas fechas definidos. Sin ese documento, la actuación puede constituir un delito con independencia de la intención, y la buena fe no es una defensa.

Lista de comprobación de meteo-01

# Medida Comprobación
1 Actualizaciones de seguridad automáticas unattended-upgrade --dry-run
2 Paquetes y servicios innecesarios eliminados systemctl list-units --state=running
3 Solo los puertos previstos en escucha, en la interfaz mínima ss -tulnp
4 Cortafuegos con policy drop de entrada nft list ruleset
5 SSH sin contraseñas, sin root, con AllowGroups sshd -T | grep -E 'permitroot|password'
6 Unidades endurecidas systemd-analyze security
7 nosuid,nodev,noexec donde procede findmnt -o TARGET,OPTIONS
8 ASLR completo y binarios con las protecciones checksec, /proc/sys/kernel/randomize_va_space
9 Permisos y propiedad correctos (04-06) Auditoría con find
10 Inventario de setuid y de capabilities al día find -perm -4000, getcap -r /
11 Secretos fuera del código y del repositorio Revisión del historial de Git
12 TLS en todo lo que sale y entra ss -tulnp, revisión de configuración
13 Copias 3-2-1 con destino inmutable Inventario de copias
14 Restauración probada en los últimos 3 meses Registro de la prueba
15 MAC activo (AppArmor) en modo obligatorio aa-status
16 Registro centralizado y auditoría Módulo 05-04

Errores Comunes y Consejos

Endurecer sin haber hecho el modelo de amenazas. Se acaba invirtiendo en lo que uno sabe hacer en lugar de en lo que protege. Media hora enumerando activos, adversarios y superficies cambia el orden de la lista de tareas.

Aplicar un benchmark completo de golpe en producción. Rompe servicios y genera desconfianza hacia el endurecimiento entero. Aplica por bloques, verifica el servicio tras cada uno y documenta las excepciones.

Confiar en ASLR, NX y canarios como si fueran una solución. Son capas que encarecen pasos concretos, y cada una tiene técnicas conocidas para sortearla. No sustituyen a corregir el fallo ni a actualizar.

Creer que LUKS protege de un servidor comprometido. Protege el disco apagado. Con el sistema arrancado y el volumen abierto, los datos son ficheros normales para cualquier proceso con permisos.

Poner secretos en variables de entorno. Son visibles en /proc/<pid>/environ, se heredan a los hijos y aparecen en volcados y en trazas de error. Fichero con permisos restrictivos, LoadCredential= o un gestor de secretos.

Tener copias y no haberlas restaurado nunca. Una copia no verificada es una suposición. Programa la restauración de prueba trimestral, mide cuánto tarda y comprueba las sumas.

Dejar un servicio escuchando en 0.0.0.0 cuando solo lo usa localhost. Es superficie gratuita. Mira la columna de dirección en ss -tulnp y restringe la interfaz: es una capa independiente del cortafuegos y sobrevive a un error en él.

Automatizar actualizaciones y no vigilar que funcionen. El fallo típico es un apt bloqueado por una actualización a medias que lleva meses sin aplicarse. Configura el aviso por correo y revísalo.

Aplicar reglas de cortafuegos en un servidor remoto sin red de seguridad. Valida con nft -c y deja programada una reversión automática que canceles si todo va bien.

Consejo: mide antes y después. systemd-analyze security, lynis y el inventario de puertos dan números concretos. Guarda el estado inicial, aplica y compara: convierte el endurecimiento en algo verificable y te permite justificar el tiempo invertido.

Ejercicios

Ejercicio 1: modelo de amenazas y superficie real

Sobre una máquina propia o una máquina virtual de pruebas: (a) enumera su superficie de red real con ss -tulnp, e indica para cada puerto quién lo abrió, en qué interfaz escucha y si esa exposición está justificada; (b) lista los servicios en ejecución y decide cuáles podrías desactivar, con la justificación; (c) construye la tabla de activos, adversarios y superficies del sistema; y (d) para las tres amenazas de la tabla del apartado 1 que consideres más probables en tu caso, indica qué indicio concreto buscarías y con qué comando.

Ejercicio 2: endurecer una unidad de systemd

Parte de esta unidad sin endurecer y llévala al nivel del apartado 9, añadiendo las directivas por bloques y verificando el servicio tras cada bloque. Para cada directiva que añadas, explica qué ataque concreto detiene. Mide la puntuación de systemd-analyze security antes y después, y explica por qué cada bloque la reduce. Indica también qué error verías en journalctl si ReadWritePaths se quedara corto y cómo lo diagnosticarías.

[Service]
ExecStart=/usr/bin/meteo-api --config /etc/meteora/meteora.conf
Restart=on-failure

Ejercicio 3: revisión defensiva de código

Para cada fragmento, identifica la clase de vulnerabilidad, explica qué se corrompe o qué se ejecuta de más y por qué, escribe la versión correcta y di qué defensa del sistema operativo (apartado 4) o directiva de systemd (apartado 9) reduciría el daño si el fallo llegara a producción.

/* A */ void guardar(const char *nombre) { char ruta[64]; strcpy(ruta, nombre); }
# B
os.system("gzip /var/lib/meteora/lecturas/" + request.args.get("fichero"))
# C
if os.path.exists(destino) == False:
    with open(destino, "w") as f: f.write(informe)
/* D */ fprintf(log, mensaje_recibido_de_la_estacion);

Soluciones

Solución 1

sudo ss -tulnp                                        # (a) superficie de red
systemctl list-units --type=service --state=running   # (b) servicios activos
sudo systemd-analyze security                         # exposición de cada unidad

(a) Cada línea de ss -tulnp se interpreta por la columna de dirección local. 127.0.0.1:puerto es local y no supone exposición externa; 0.0.0.0:puerto o [::]:puerto escucha en todas las interfaces y es exposición real; una IP concreta limita a esa red. Para cada puerto hay que responder tres preguntas: qué proceso lo abrió (columna users:), quién necesita alcanzarlo y si la interfaz es la mínima posible. Un hallazgo habitual y perfectamente evitable es una base de datos o un servidor de desarrollo escuchando en 0.0.0.0 cuando solo lo usa la propia máquina; se corrige en la configuración del servicio, y esa corrección es una capa independiente del cortafuegos que sigue protegiendo si las reglas fallan.

(b) Candidatos habituales a desactivar en un servidor: descubrimiento de red (avahi-daemon), impresión (cups), Bluetooth, servidores gráficos y servicios de gestión que ya no se usan. El criterio es doble: si no hay respuesta clara a "¿quién lo usa y para qué?", se desactiva; y purge es preferible a disable, porque un servicio desactivado sigue en el disco y puede reactivarse con una actualización.

(c) La tabla debe recoger, como mínimo: los activos con su impacto en caso de pérdida o filtración; los adversarios, sin olvidar que el automatizado es continuo y el interno es el más difícil de detectar; y las superficies con su protección actual y su riesgo residual. El resultado útil es la priorización: contra el adversario automatizado, que es casi todo el tráfico hostil, la defensa barata y efectiva es cortafuegos, sin contraseñas en SSH y actualizaciones al día.

(d) Indicios y comandos:

# Criptominería: CPU sostenida al 100 % sin carga que lo explique
top -b -n1 -o %CPU | head -12
# Puerta trasera: puertos y conexiones inesperados, y claves nuevas
sudo ss -tulnp ; sudo ss -tp state established
sudo find /home /root -name authorized_keys -newermt '-30 days' -ls
# Robo de datos: transferencia saliente inusual y lecturas masivas
sudo ss -tp state established ; vnstat -h 2>/dev/null || cat /proc/net/dev

Lo que convierte esto en detección de verdad no es ejecutar los comandos una vez, sino compararlos con una línea base conocida: la lista de puertos, la de procesos, la de ficheros setuid y la de claves autorizadas. Los cuatro ejes de los que hablaba el apartado 1.

Solución 2

systemd-analyze security meteo-api     # ANTES: ~9,5 de 10 ("UNSAFE")

Bloque 1 — identidad y privilegio. User=meteora, Group=meteora, UMask=0027, NoNewPrivileges=yes, CapabilityBoundingSet=CAP_NET_BIND_SERVICE. Detiene lo más grave: sin User=, el servicio corre como root y cualquier fallo en el análisis de peticiones da la máquina entera. NoNewPrivileges=yes corta la escalada por binarios setuid aunque exista uno vulnerable en el sistema, y el CapabilityBoundingSet impide para siempre que el proceso o sus hijos obtengan capabilities de montaje, de carga de módulos o de depuración.

Bloque 2 — sistema de ficheros. ProtectSystem=strict, ProtectHome=yes, PrivateTmp=yes, ReadWritePaths=, ReadOnlyPaths=, PrivateDevices=yes, ProtectProc=invisible. ProtectSystem=strict deja todo en solo lectura salvo lo declarado, así que un proceso comprometido no puede modificar binarios ni dejar persistencia; ProtectHome le oculta /home y /root, donde viven claves SSH y credenciales; y PrivateTmp elimina los ataques por ficheros temporales compartidos y el TOCTOU en /tmp.

Bloque 3 — núcleo, memoria y red. ProtectKernelTunables/Modules/Logs, MemoryDenyWriteExecute=yes, RestrictSUIDSGID=yes, RestrictAddressFamilies=, IPAddressDeny=any con IPAddressAllow=. MemoryDenyWriteExecute bloquea la generación de código en memoria; RestrictAddressFamilies corta familias de sockets que la API no necesita; y IPAddressDeny=any impide la exfiltración y la conexión a servidores de control, que son dos filas completas de la tabla de amenazas.

Bloque 4 — llamadas al sistema y recursos. SystemCallFilter=@system-service con las restas de @privileged @mount @module @debug, SystemCallArchitectures=native, y los límites LimitNOFILE, LimitNPROC, MemoryMax, TasksMax. El filtro reduce la superficie de ~350 llamadas a las que el servicio usa realmente (05-01); SystemCallArchitectures=native cierra el hueco de invocar por la ABI de 32 bits para esquivar el filtro; y los límites evitan que un fallo del servicio agote los descriptores o la memoria del sistema entero.

systemd-analyze security meteo-api     # DESPUÉS: ~1,5–2,5 ("OK"/"MEDIUM")

Por qué baja la puntuación: la herramienta pondera cada exposición no mitigada, y los dos bloques con más peso son el de privilegio y el de sistema de ficheros, precisamente porque son los que determinan si un compromiso queda contenido o se propaga.

Si ReadWritePaths se queda corto, el servicio arranca y falla al escribir. En journalctl -u meteo-api -p err aparecerá un error de tipo "Read-only file system" (EROFS) sobre una ruta concreta, que es exactamente la que falta declarar. La forma sistemática de diagnosticarlo es lanzar el binario bajo el mismo confinamiento con systemd-run y las mismas directivas, o revisar con strace -f -e trace=openat,write en un entorno de pruebas —nunca en producción— para ver qué rutas abre en modo escritura.

Solución 3

A — Desbordamiento de búfer. strcpy copia hasta el \0 sin mirar que ruta mide 64 bytes. Un nombre más largo sobrescribe lo que sigue en la pila, incluida la dirección de retorno, y al terminar la función la CPU salta a donde diga el atacante. Corrección: if (snprintf(ruta, sizeof ruta, "%s", nombre) >= (int) sizeof ruta) return -1;, con el límite derivado de sizeof del destino y el retorno comprobado. Mitigación del sistema: canario de pila (lo detecta y aborta), ASLR + PIE (el atacante no sabe a dónde saltar), NX (no puede ejecutar código en la pila) y MemoryDenyWriteExecute=yes en la unidad.

B — Inyección de comandos. El nombre de fichero llega de una petición HTTP y se concatena en una orden que ejecuta una shell, y la shell interpreta ;, |, && y $(...) antes de ejecutar. Un valor con ; produce una segunda orden elegida por quien envió la petición, con la identidad de meteora. Corrección: validar por lista blanca y ejecutar sin shell con argumentos separados, subprocess.run(["gzip", "--", ruta], shell=False), tras comprobar además con realpath que la ruta sigue dentro del directorio previsto —si no, es también un delegado confuso (05-01)—. Mitigación: SystemCallFilter sin execve, perfil de AppArmor sin reglas de ejecución, y ProtectSystem=strict.

C — TOCTOU. Entre os.path.exists(destino) y open(destino, "w") hay una ventana en la que el atacante puede crear ahí un enlace simbólico a otro fichero, y la escritura acabaría en el destino que él elija, con los permisos del servicio. Corrección: no comprobar el nombre y luego usarlo, sino abrir directamente con os.open(destino, O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW, 0o640), que falla si ya existe o si es un enlace, y operar sobre el descriptor. Mitigación: PrivateTmp=yes si es un directorio temporal, ReadWritePaths acotado, y los permisos de directorio de 04-06.

D — Cadena de formato. mensaje_recibido_de_la_estacion se usa como cadena de formato, así que fprintf interpreta los especificadores que contenga: %x revela contenido de la pila y %n permite escribir en memoria. Corrección: fprintf(log, "%s", mensaje_recibido_de_la_estacion);, con la cadena de formato siempre constante. Mitigación: compilar con -Wformat -Wformat-security —el compilador lo detecta en tiempo de compilación, que es donde hay que cazarlo—, -D_FORTIFY_SOURCE=2, y RELRO total, que impide sobrescribir las tablas de enlace.

La lección común a los cuatro: las mitigaciones del sistema encarecen el aprovechamiento, no arreglan el fallo. Son valiosas porque convierten un compromiso total en una caída del servicio, pero la corrección está siempre en el código, y por eso las revisiones, los sanitizers y las actualizaciones son la primera línea y no la última.

Conclusión

Un servidor real se enfrenta a un catálogo acotado de amenazas —malware, ransomware, rootkits de usuario y de núcleo, puertas traseras, criptominería, botnets, denegación de servicio, robo de datos y cadena de suministro— y casi todas dejan cuatro indicios comunes: un proceso, un puerto, un fichero o una conexión saliente que no deberías tener. Eso simplifica la detección a vigilar esos cuatro ejes contra una línea base. Dos amenazas rompen el esquema y conviene tenerlas presentes: el rootkit de núcleo, que corrompe las respuestas de las propias herramientas con las que investigas —de ahí la necesidad de telemetría externa—, y la cadena de suministro, que no deja indicio porque llega firmada dentro de una actualización legítima. El modelo de amenazas —activos, adversarios, superficies— es lo que da criterio para priorizar: contra el adversario automatizado, que es el 99 % del ruido, basta con no tener nada obvio abierto y estar al día de parches; contra el dirigido hacen falta confinamiento y detección; y contra el interno, mínimo privilegio y auditoría.

Las clases de vulnerabilidad de programa se entienden mejor por su causa raíz que por su nombre. El desbordamiento de búfer ocurre cuando el tamaño de una copia lo decide el atacante y no el programa, y lo que corrompe es la dirección de retorno que está contigua en la pila. Las cadenas de formato aparecen cuando datos externos ocupan el lugar de la cadena de formato, que siempre debe ser una constante. La inyección de comandos nace de mezclar código y datos en una cadena que interpreta una shell, y desaparece al ejecutar con execve y argumentos separados, sin shell, con validación por lista blanca. El TOCTOU vive en la ventana entre comprobar un nombre y usarlo, y se cierra operando sobre el descriptor, que es una capacidad. Y la desreferencia tras liberar se evita con disciplina de propiedad de la memoria y herramientas de detección en las pruebas. El sistema aporta capas que encarecen el aprovechamiento —ASLR y KASLR, NX, canarios, PIE, RELRO, seccomp—, comprobables con checksec y /proc/sys/kernel/randomize_va_space; son capas, no soluciones, y ninguna sustituye a corregir el fallo.

El endurecimiento de meteo-01 tiene un orden que refleja la relación entre esfuerzo y protección. Primero, actualizaciones automáticas de seguridad, porque la mayoría de los compromisos reales aprovechan fallos ya corregidos. Después, minimizar la superficie: purgar paquetes, apagar servicios y hacer que cada uno escuche en la interfaz mínima, algo que ss -tulnp revela en un segundo. Luego el cortafuegos con policy drop, que aplica denegar por defecto también a lo que se instale mañana, con la cadena de salida restringida si los datos lo merecen. El acceso remoto ya endurecido en 05-02, con restricción por origen y reducción de ruido en los registros. Y el bloque de más valor por línea escrita: el aislamiento con systemd, donde NoNewPrivileges, ProtectSystem=strict, PrivateTmp, CapabilityBoundingSet, MemoryDenyWriteExecute, IPAddressDeny y SystemCallFilter implementan sin tocar el código todo lo que 05-01 planteaba en teoría —y systemd-analyze security lo mide—. A eso se suman las opciones de montaje nosuid,nodev,noexec, los límites de recursos como defensa frente al agotamiento, el cifrado en reposo con LUKS —que protege el disco apagado, no un sistema comprometido— y en tránsito con TLS, y una gestión de secretos en la que el fichero 0600 es el mínimo y nunca el ideal. Cierran el conjunto las copias 3-2-1 con destino inmutable y credenciales separadas, única defensa real contra el ransomware, cuya parte no negociable es la prueba de restauración; y los marcos de verificación —CIS, lynis, debsecan—, que se adaptan y se documentan, nunca se aplican a ciegas, y cuyo uso sobre sistemas ajenos exige autorización expresa por escrito.

Con esto, meteo-01 está razonablemente protegido. Pero fíjate en lo que sigue faltando, y es lo más incómodo: todo lo anterior es prevención. Ninguna de estas medidas te dice si algo ha ocurrido. Si mañana a las 03:12 hay un pico de escrituras en /var/lib/meteora y aparece un proceso que nadie reconoce, ¿cómo lo sabes? ¿Qué registros existen, dónde están y qué contienen? ¿Cómo se investiga en el orden correcto, sin destruir la evidencia por el camino? ¿Y qué valor tiene un registro escrito por un sistema que el atacante controla?

Eso es Auditoría, Registros y Respuesta a Incidentes, donde veremos syslog y journald con sus filtros, cómo se diseña el registro de una aplicación y qué nunca debe contener, la rotación y la retención, el subsistema auditd del núcleo, la integridad de los registros frente a un atacante con privilegios, la detección de cambios con AIDE, y el ciclo completo de respuesta a incidentes aplicado a ese pico de las 03:12.

Fundamentos de Sistemas Operativos

Módulo 1: Introducción a los Sistemas Operativos

Módulo 2: Gestión de Recursos

Módulo 3: Concurrencia

Módulo 4: Estructuras de Archivos

Módulo 5: Protección y Seguridad del Sistema

Módulo 6: Virtualización y Contenedores

Módulo 7: Administración y Diagnóstico en la Práctica

© Copyright 2026. Todos los derechos reservados