Las cinco lecciones anteriores han añadido medidas de seguridad de una en una: red controlada, SSH con claves, cortafuegos, detección de intrusos, secretos cifrados y TLS. Cada una resolvía un problema concreto. Esta lección hace algo distinto: las convierte en una postura de seguridad, que es un conjunto coherente de decisiones justificadas por un modelo de amenazas explícito, con constancia escrita de qué está protegido, qué se ha decidido asumir y qué queda fuera del alcance de un administrador.
La diferencia no es retórica. Sin modelo de amenazas, el endurecimiento degenera en cargo cult: se aplican recetas encontradas en guías porque «suenan seguras», se rompen cosas sin entender por qué, y se acaba con un sistema frágil y una falsa sensación de control. Con modelo de amenazas, cada medida tiene un porqué y cada omisión es una decisión consciente.
Además cierras aquí el tercer incidente pendiente —los servicios que siguen usando la libssl vulnerable cargada en memoria— y saldas la deuda que dejaste en 05-01: PAM, el subsistema que decide de verdad cómo se autentica alguien en este sistema.
Advertencia previa. Dos de los apartados de esta lección —PAM y los montajes seguros— pueden dejarte sin poder iniciar sesión en tu propio servidor si te equivocas. En ambos casos se indica el procedimiento seguro. No te lo salte: el laboratorio existe precisamente para que aprendas esto donde equivocarse no cuesta nada.
Contenido
- Los cinco principios que ordenan todo lo anterior
- El modelo de amenazas de Tramontana
- Reducción de la superficie de ataque
- Endurecimiento de cuentas y política de contraseñas
- PAM: cómo se autentica realmente este sistema
- Control de acceso obligatorio con AppArmor
- Endurecimiento del kernel con sysctl
- Montajes seguros
- Gestión de vulnerabilidades y cierre del incidente de openssl
- Endurecimiento de systemd revisitado
- Copias como último control frente al ransomware
- Checklist de endurecimiento y mantenimiento de la postura
Los cinco principios que ordenan todo lo anterior
Todo lo que has hecho en el módulo responde, sin que lo hayamos dicho, a cinco principios. Nombrarlos permite aplicarlos a situaciones nuevas en lugar de repetir recetas:
| Principio | Qué exige | Dónde lo has aplicado ya |
|---|---|---|
| Mínimo privilegio | Cada proceso y cada persona con los permisos justos, ni uno más | svc-tramontana sin shell, sudoers con comandos concretos, CapabilityBoundingSet= vacío |
| Superficie de ataque mínima | Lo que no existe no se puede atacar | Servidor sin escritorio, puerto 8080 cerrado al exterior, política de lista blanca en ufw |
| Defensa en profundidad | Varias capas independientes; que una falle no debe bastar | Firewall + SSH endurecido + AIDE + auditd + secretos cifrados |
| Fallo seguro | Cuando algo se rompe, debe quedar cerrado, no abierto | policy drop por defecto en nftables, set -euo pipefail en los scripts |
| Seguro por defecto | El estado inicial debe ser el restrictivo | umask 027, permisos 600 en los secretos, PasswordAuthentication no |
El más difícil de aplicar en la práctica es el segundo, porque exige quitar cosas, y quitar cosas da miedo. El más olvidado es el cuarto: mucha configuración «segura» falla abriéndose, y eso es peor que no tenerla, porque nadie se enterará.
El modelo de amenazas de Tramontana
Un modelo de amenazas responde a tres preguntas. Media página es suficiente, y es la media página que hace que todo lo demás tenga sentido.
¿Qué protegemos? Dos cosas, en este orden:
- Los datos personales de los huéspedes (nombres, fechas de estancia, importes en
reservas.csvy en la base de datos). Su compromiso es un daño a terceros, tiene consecuencias legales bajo el RGPD y es irreversible: una vez filtrados, no se pueden «recuperar». - La disponibilidad del servicio de reservas. Su interrupción tiene un coste económico directo y acotado en el tiempo.
¿De quién? Cuatro actores realistas, ordenados por probabilidad:
| Amenaza | Probabilidad | Capacidad | Controles que le oponemos |
|---|---|---|---|
| Escaneo automatizado de Internet | Constante | Baja: explota lo conocido y sin parchear | Firewall, fail2ban, actualizaciones de seguridad, SSH sin contraseña |
| Credencial filtrada o reutilizada | Media | Alta: entra como usuario legítimo | Claves en vez de contraseñas, secretos cifrados, rotación, auditd |
| Error propio de configuración | Alta | Variable, a veces total | --dry-run, copias antes de editar, netplan try, mount -a, AIDE |
| Abuso de acceso legítimo | Baja | Alta dentro de su ámbito | Mínimo privilegio en sudoers, ACL, auditoría de accesos |
Fíjate en la tercera fila. El actor más probable eres tú, y esa constatación explica por qué media docena de convenciones del curso —copiar antes de editar, verificar con diff -u, probar en seco, netplan try con reversión automática, mount -a antes de reiniciar— son controles de seguridad de pleno derecho, y no manías de estilo.
¿Qué asumimos fuera de alcance? Decirlo es tan importante como lo anterior, porque delimita qué no se está protegiendo:
- Atacante con recursos estatales o cadena de suministro comprometida. Está fuera del alcance de cualquier medida que un administrador pueda tomar en un servidor de una pequeña empresa.
- Acceso físico a la máquina. El cifrado LUKS mitiga el robo del disco, pero un atacante con acceso físico prolongado y la máquina encendida tiene el sistema.
- Vulnerabilidad en la propia aplicación (inyección SQL, lógica de negocio). Es responsabilidad del desarrollo, no de la administración, y el endurecimiento del sistema solo limita el daño posterior.
- Compromiso del proveedor de la nube o del hipervisor.
Y ahora el criterio de fondo: con este modelo, invertir en un NIDS aporta poco (ya lo razonaste en 06-04) e invertir en detección de cambios y en control de la autenticación aporta mucho. Eso es tomar decisiones, no seguir una lista.
Reducción de la superficie de ataque
Lo que no existe no se puede atacar, no necesita parches y no aparece en ningún CVE. Es el principio con mejor relación entre esfuerzo y resultado.
Qué escucha
$ sudo ss -tulpn
Netid State Local Address:Port Peer Address:Port Process
udp UNCONN 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=712,fd=17))
udp UNCONN 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=712,fd=13))
tcp LISTEN 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=712,fd=14))
tcp LISTEN 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=894,fd=3))
tcp LISTEN 0.0.0.0:8080 0.0.0.0:* users:(("tramontana",pid=4102,fd=6))
tcp LISTEN 10.0.2.15:5432 0.0.0.0:* users:(("postgres",pid=1105,fd=5))Lee cada línea preguntando «¿tiene que estar ahí, y en esa dirección?»:
systemd-resolveen127.0.0.53y127.0.0.54: local, correcto.sshden0.0.0.0:22: necesario, y ya endurecido.tramontanaen0.0.0.0:8080: escucha en todas las interfaces. El cortafuegos lo bloquea desde fuera, pero eso es defensa en una sola capa. Lo correcto es que la aplicación escuche solo en127.0.0.1, porque cuando llegue el proxy inverso de 08-01 será quien le hable, y así el firewall pasa a ser la segunda línea en lugar de la única.postgresen10.0.2.15:5432: ya restringido a la interfaz interna. Correcto.
El primer arreglo, entonces:
$ sudo cp -p /etc/tramontana/app.conf /etc/tramontana/app.conf.bak-$(date +%F)
$ sudo chattr -i /etc/tramontana/app.conf
$ echo 'escucha=127.0.0.1' | sudo tee -a /etc/tramontana/app.conf
$ sudo chattr +i /etc/tramontana/app.conf
$ sudo systemctl restart tramontana.service
$ sudo ss -tulpn | grep 8080
tcp LISTEN 127.0.0.1:8080 0.0.0.0:* users:(("tramontana",pid=4318,fd=6))Eso es defensa en profundidad aplicada: ahora hacen falta dos fallos —una regla de firewall mal puesta y una escucha demasiado amplia— para exponer el 8080. Antes bastaba uno.
Qué está habilitado
$ systemctl list-unit-files --state=enabled --type=service --no-pager
UNIT FILE STATE PRESET
apparmor.service enabled enabled
auditd.service enabled enabled
cron.service enabled enabled
fail2ban.service enabled enabled
ModemManager.service enabled enabled
multipathd.service enabled enabled
postgresql.service enabled enabled
ssh.service enabled enabled
tramontana.service enabled enabled
unattended-upgrades.service enabled enabledModemManager gestiona módems y banda ancha móvil. En una máquina virtual de servidor no tiene ningún sentido. Y aquí conviene entender la diferencia entre las dos formas de apagar algo, porque no son equivalentes:
| Operación | Qué hace | Puede reactivarse solo |
|---|---|---|
disable |
Borra los enlaces de [Install]; no arranca al inicio |
Sí: si otra unidad lo declara en Wants= |
mask |
Enlaza la unidad a /dev/null; es inarrancable |
No: ni a mano ni por dependencia |
$ sudo systemctl disable --now ModemManager.service
$ sudo systemctl mask ModemManager.service
Created symlink /etc/systemd/system/ModemManager.service → /dev/null.mask para lo que no debe arrancar nunca; disable para lo que podrías querer arrancar a mano algún día. Y el paso siguiente, cuando estés seguro, es desinstalar el paquete: un binario que no está en el disco no tiene vulnerabilidades.
Sobre el escritorio, para cerrar el apartado: un entorno gráfico añade cientos de paquetes, un servidor X o Wayland, un gestor de sesiones y un navegador — cada uno con su superficie y su calendario de parches. Que srv-tramontana no lo tenga no es austeridad, es la reducción de superficie más grande que se puede hacer en un servidor, y por eso el instalador de Ubuntu Server no lo ofrece por defecto.
Endurecimiento de cuentas y política de contraseñas
Tres auditorías que conviene tener guardadas en un script, porque detectan errores graves y son de una línea cada una:
# 1. Cuentas con UID 0: solo debe haber una, root
$ awk -F: '($3 == 0) {print $1}' /etc/passwd
root
# 2. Cuentas sin contraseña (segundo campo vacio en shadow): grave
$ sudo awk -F: '($2 == "") {print $1 " NO TIENE CONTRASENA"}' /etc/shadow
# 3. Cuentas de sistema (UID < 1000) con shell de inicio de sesion valida
$ awk -F: '($3 < 1000 && $7 !~ /(nologin|false|sync)$/) {print $1, $3, $7}' /etc/passwd
root 0 /bin/bash
sync 4 /bin/syncLos tres resultados son correctos: un único UID 0, ninguna cuenta sin contraseña, y las dos únicas cuentas de sistema con shell son root —necesario para la recuperación— y sync, que es un caso histórico inofensivo. Que svc-tramontana no aparezca en la tercera lista es la confirmación de que el -s /usr/sbin/nologin de 05-01 hizo su trabajo.
La política por defecto vive en /etc/login.defs, y afecta a las cuentas que se creen a partir de ahora:
# /etc/login.defs (valores modificados)
PASS_MAX_DAYS 365 # caducidad maxima
PASS_MIN_DAYS 1 # evita que se cambie varias veces seguidas para volver a la anterior
PASS_WARN_AGE 14 # dias de aviso antes de caducar
UMASK 027 # coherente con la convencion del curso
SHA_CRYPT_MIN_ROUNDS 5000
ENCRYPT_METHOD YESCRYPTPASS_MIN_DAYS 1 es menos obvio que los demás: sin él, alguien a quien se le exige cambiar la contraseña puede cambiarla cinco veces seguidas para agotar el historial y volver a la original. Es un truco viejo y sigue funcionando donde no se pone.
Y para las cuentas ya existentes, chage:
$ sudo chage -M 365 -m 1 -W 14 luis
$ sudo chage -l luis
Last password change : ago 04, 2026
Password expires : ago 04, 2027
Password inactive : never
Account expires : never
Minimum number of days between password change : 1
Maximum number of days between password change : 365
Number of days of warning before password expires : 14
# La cuenta del becario tiene fecha de fin de practicas: caduca automaticamente
$ sudo chage -E 2026-12-31 becario
$ sudo chage -l becario | grep 'Account expires'
Account expires : dic 31, 2026chage -E merece atención: es la forma correcta de gestionar una cuenta temporal. Caduca sola en la fecha prevista, sin depender de que alguien se acuerde de darla de baja — y las bajas de personal olvidadas son una de las vías de compromiso más comunes que existen.
PAM: cómo se autentica realmente este sistema
En 05-01 aplazamos PAM. Es el momento, porque es la pieza que decide de verdad quién entra en este sistema y con qué requisitos.
PAM (Pluggable Authentication Modules) es una capa de indirección: los programas que necesitan autenticar —login, sshd, sudo, su, passwd— no implementan la lógica, sino que preguntan a PAM. Eso permite cambiar la política (exigir contraseñas fuertes, bloquear tras fallos, añadir un segundo factor) sin modificar ni recompilar ningún programa. Es la razón por la que existe.
La estructura
Cada servicio tiene su fichero en /etc/pam.d/, y hay ficheros comunes que los demás incluyen:
$ ls /etc/pam.d/ | head -12
common-account
common-auth
common-password
common-session
cron
login
passwd
sshd
su
sudoCada línea tiene tres partes: tipo, control y módulo.
Los cuatro tipos, que son cuatro preguntas distintas y responden en momentos distintos:
| Tipo | Pregunta que responde |
|---|---|
auth |
¿Es quien dice ser? (contraseña, clave, segundo factor) |
account |
¿Se le permite entrar ahora? (cuenta caducada, horario, origen) |
password |
¿Es aceptable la contraseña nueva que quiere poner? |
session |
Qué hay que preparar y limpiar alrededor de la sesión (límites, registro, home) |
Confundir auth con account es el malentendido más habitual: una contraseña correcta en una cuenta caducada pasa auth y falla account.
Los controles determinan qué ocurre según el resultado del módulo:
| Control | Si el módulo falla | Si tiene éxito |
|---|---|---|
required |
Falla el conjunto, pero se siguen ejecutando los demás | Continúa |
requisite |
Falla y se detiene ahí mismo | Continúa |
sufficient |
Se ignora | Éxito inmediato, si nada required anterior falló |
optional |
Se ignora (salvo que sea el único) | Continúa |
required frente a requisite tiene una razón de ser que no es evidente: required sigue ejecutando la pila para no revelar en qué paso falló. Si un atacante pudiera distinguir «usuario inexistente» de «contraseña incorrecta» por el tiempo de respuesta o el mensaje, tendría un oráculo para enumerar usuarios.
La sintaxis moderna, más precisa, usa corchetes: [success=ok default=die] permite especificar la acción para cada código de retorno. Es lo que verás en los ficheros de Ubuntu.
El procedimiento para no dejarse fuera
Antes de tocar una sola línea. Un error de sintaxis en common-auth puede impedir todo inicio de sesión, incluida la consola local, y entonces la única salida es el modo de recuperación:
- Deja una sesión de root abierta en otra terminal, y no la cierres hasta haber terminado:
$ sudo -i # (no cerrar esta sesion) - Copia el fichero antes de editarlo, según la convención del curso:
$ sudo cp -p /etc/pam.d/common-password /etc/pam.d/common-password.bak-$(date +%F) - Prueba en una tercera terminal, nunca en la que tienes la sesión de root.
- Ten a mano la consola de la VM y sabe cómo entrar en modo de recuperación (se ve a fondo en 07-01).
Es exactamente la misma disciplina de netplan try y de la segunda sesión SSH: nunca cierres la puerta por la que estás entrando.
pam_pwquality: exigir contraseñas decentes
# /etc/security/pwquality.conf
minlen = 12 # longitud minima
minclass = 3 # al menos 3 de: minusculas, mayusculas, digitos, simbolos
maxrepeat = 3 # no mas de 3 caracteres iguales consecutivos
dictcheck = 1 # rechaza palabras de diccionario
usercheck = 1 # rechaza contrasenas que contengan el nombre de usuario
enforce_for_root = 1 # tambien a root: sin esto, root queda exento
retry = 3$ passwd luis
New password: tramontana2026
Bad password: it is based on a dictionary word.
New password: Kj8-mQ2vX9pL
passwd: password updated successfullyDos comentarios de fondo. El primero: minlen = 12 con minclass = 3 es más razonable que las políticas de «8 caracteres con mayúscula, número y símbolo» que producen Passw0rd! — la longitud aporta mucho más que la complejidad forzada, y así lo recogen las guías actuales (NIST SP 800-63B). El segundo: enforce_for_root = 1 es fácil de olvidar, y sin él la cuenta más importante del sistema es la única exenta de la política.
pam_faillock: bloquear tras intentos fallidos
Complementa a fail2ban, que actúa sobre la IP; faillock actúa sobre la cuenta, así que cubre también los intentos desde la consola o desde IP distintas.
# /etc/security/faillock.conf
deny = 5 # bloquea tras 5 fallos
fail_interval = 900 # que ocurran dentro de 15 minutos
unlock_time = 600 # desbloqueo automatico a los 10 minutos
even_deny_root # tambien a root
root_unlock_time = 60 # pero root se desbloquea en 1 minuto: evita el bloqueo total
audit # registra el intento
silent # no revela al atacante que la cuenta esta bloqueada# /etc/pam.d/common-auth
auth required pam_faillock.so preauth
auth [success=1 default=ignore] pam_unix.so nullok
auth [default=die] pam_faillock.so authfail
auth sufficient pam_faillock.so authsucc
auth requisite pam_deny.so
auth required pam_permit.soEl orden importa y no es intuitivo: preauth comprueba si la cuenta ya está bloqueada antes de pedir credenciales, authfail cuenta el fallo, y authsucc limpia el contador tras un acceso correcto. Faltar authsucc produce un bloqueo lento pero inevitable, porque el contador nunca se reinicia.
Y la combinación de even_deny_root con root_unlock_time = 60 es deliberada: proteger root sin crear la posibilidad de un bloqueo total del sistema.
$ faillock --user luis
luis:
When Type Source Valid
2026-08-18 14:22:11 RHOST 203.0.113.44 V
2026-08-18 14:22:14 RHOST 203.0.113.44 V
$ sudo faillock --user luis --reset # desbloquear a manopam_limits: límites de recursos
# /etc/security/limits.conf
# dominio tipo elemento valor
* hard nproc 4096 # frena una bomba de procesos
* hard core 0 # no generar volcados: pueden contener secretos
svc-tramontana soft nofile 8192
svc-tramontana hard nofile 16384Dos de estas líneas son medidas de seguridad más que de rendimiento. nproc limita el daño de un bucle que crea procesos sin control —accidental o no—. Y core 0 evita los volcados de memoria, que contienen todo lo que el proceso tenía en RAM en ese momento: incluida la contraseña que tanto trabajo te ha costado cifrar en 06-05.
Ojo con el alcance: pam_limits se aplica a sesiones que pasan por PAM. Para un servicio de systemd, los límites se ponen en la unidad (LimitNOFILE, LimitNPROC), como viste en 05-07.
Segundo factor, brevemente
libpam-google-authenticator añade un código TOTP a la autenticación SSH. Es eficaz contra el robo de credenciales, y en un servidor con acceso por clave el beneficio marginal es menor que en uno con contraseñas. Si se implanta, hay dos cosas que no se pueden olvidar: guardar los códigos de recuperación fuera del servidor, y decidir qué pasa si el dispositivo TOTP se pierde. Sin eso, es una forma elegante de quedarse fuera.
Cerrar el hallazgo: la excepción de PasswordAuthentication
En 06-04 los registros mostraron que luis entró con password, pese a que en 06-02 lo deshabilitaste. Hora de localizarlo, empezando por la fuente autorizada:
$ sudo sshd -T | grep -i -E 'passwordauth|kbdinteractive'
passwordauthentication no
kbdinteractiveauthentication noLa configuración global es correcta. Pero sshd -T sin argumentos muestra la configuración global; los bloques Match son condicionales y no aparecen ahí. Hay que preguntar por el caso concreto:
$ sudo grep -rn -A3 'Match' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/
/etc/ssh/sshd_config.d/60-tramontana.conf:12:Match User luis
/etc/ssh/sshd_config.d/60-tramontana.conf-13- PasswordAuthentication yes
$ sudo sshd -T -C user=luis,host=portatil-luis,addr=10.0.2.44 | grep -i passwordauth
passwordauthentication yesAhí está. Un bloque Match User luis con la excepción, puesta «temporalmente» mientras Luis generaba su clave, y nunca retirada. Es el actor más probable del modelo de amenazas —el error propio— en su forma más característica.
$ sudo cp -p /etc/ssh/sshd_config.d/60-tramontana.conf{,.bak-$(date +%F)}
$ sudo sed -i '/^Match User luis$/,+1d' /etc/ssh/sshd_config.d/60-tramontana.conf
$ sudo sshd -t && echo "sintaxis correcta"
sintaxis correcta
$ sudo sshd -T -C user=luis,host=portatil-luis,addr=10.0.2.44 | grep -i passwordauth
passwordauthentication no
$ sudo systemctl reload sshFíjate en sshd -T -C: la opción -C evalúa la configuración para un contexto concreto de usuario, host y dirección, resolviendo los bloques Match. Es la única forma fiable de saber qué configuración se aplica a alguien, y merece un sitio en tu memoria: la mitad de los problemas desconcertantes de SSH son un Match olvidado.
Antes de recargar, la disciplina de siempre: sshd -t valida, la segunda sesión está abierta, y se usa reload en lugar de restart.
Control de acceso obligatorio con AppArmor
Los permisos que conoces desde 02-07 son DAC (Discretionary Access Control): discrecionales porque el propietario de un recurso decide quién accede. Tienen un límite estructural: un proceso puede hacer todo lo que su usuario puede hacer. Si la aplicación de Tramontana se ve comprometida, el atacante puede leer cualquier cosa legible por svc-tramontana y escribir en cualquier sitio donde ese usuario escriba.
MAC (Mandatory Access Control) añade una capa que el propietario no puede relajar: una política, definida por el administrador y aplicada por el kernel, que dice qué puede hacer este programa, con independencia de quién lo ejecute.
| DAC (permisos Unix) | MAC (AppArmor / SELinux) | |
|---|---|---|
| Quién decide | El propietario del fichero | El administrador, en la política |
| Unidad de control | Usuario y grupo | Programa (perfil) |
| Puede el proceso relajarlo | Sí, dentro de lo de su usuario | No |
| Qué contiene | Un compromiso del proceso | Un compromiso del proceso y de su usuario |
$ sudo aa-status
apparmor module is loaded.
32 profiles are loaded.
28 profiles are in enforce mode.
/usr/bin/man
/usr/sbin/sshd
...
2 profiles are in complain mode.
2 processes have profiles defined.Los dos modos son la clave del método de trabajo:
complain: no bloquea, solo registra lo que habría bloqueado. Es el modo de aprendizaje.enforce: bloquea. Es el modo de producción.
Y el procedimiento profesional es siempre el mismo: escribir el perfil, ponerlo en complain, ejercitar la aplicación de verdad, recoger las denegaciones y ampliar el perfil, y solo entonces pasar a enforce. Empezar en enforce significa romper el servicio y descubrir los accesos que faltan de la peor manera.
Un perfil para la aplicación de Tramontana
# /etc/apparmor.d/opt.tramontana.app.tramontana
abi <abi/4.0>,
include <tunables/global>
/opt/tramontana/releases/*/tramontana {
include <abstractions/base>
include <abstractions/nameservice>
include <abstractions/openssl>
# El binario propio: leer y ejecutar
/opt/tramontana/releases/*/tramontana mr,
/opt/tramontana/releases/*/** r,
/opt/tramontana/app/** r,
# Configuracion: solo lectura. Aunque el proceso se vea comprometido,
# no puede modificar su propia configuracion.
/etc/tramontana/app.conf r,
# Credencial entregada por systemd (tmpfs privado del servicio)
/run/credentials/tramontana.service/* r,
# Registros propios: crear y anadir, nunca sobrescribir ni borrar
/var/log/tramontana/ r,
/var/log/tramontana/*.log rw,
# Datos subidos por los huespedes: lectura y escritura
/opt/tramontana/shared/uploads/ r,
/opt/tramontana/shared/uploads/** rwk,
# Red: solo TCP sobre IPv4/IPv6. No unix crudo, no paquetes crudos.
network inet stream,
network inet6 stream,
# Nada mas esta permitido: AppArmor deniega por defecto.
# En particular, quedan DENEGADOS de forma implicita:
# /etc/shadow, /home/**, /srv/tramontana/backups/**,
# /root/**, la ejecucion de /bin/sh y la carga de modulos.
}Ese último comentario es lo que hace valioso el perfil. Un atacante que ejecute código en el contexto de la aplicación no puede lanzar una shell, no puede leer las copias, no puede tocar los directorios personales y no puede modificar su propia configuración — cosas que el DAC sí le permitiría hacer en parte, y que son precisamente los primeros pasos de cualquier post-explotación.
# 1. Cargar en modo aprendizaje
$ sudo apparmor_parser -r /etc/apparmor.d/opt.tramontana.app.tramontana
$ sudo aa-complain /opt/tramontana/releases/*/tramontana
# 2. Ejercitar la aplicacion de verdad: peticiones, subida de fichero, despliegue
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/casas
200
$ ~/scripts/revision_salud.sh; echo "estado: $?"
estado: 0
# 3. Recoger las denegaciones registradas
$ sudo journalctl -k --since "10 min ago" | grep 'apparmor="ALLOWED"'
kernel: audit: type=1400 apparmor="ALLOWED" operation="open"
profile="/opt/tramontana/releases/*/tramontana" name="/opt/tramontana/shared/plantillas/factura.html"
pid=4318 comm="tramontana" requested_mask="r" denied_mask="r"En modo complain la marca es ALLOWED con un denied_mask: significa «esto lo habría bloqueado». Aquí ha aparecido un acceso legítimo que el perfil no contemplaba —las plantillas—, así que se añade y se repite el ciclo:
# 4. Ampliar de forma asistida si se prefiere, y pasar a enforce
$ sudo aa-logprof
$ sudo aa-enforce /opt/tramontana/releases/*/tramontana
Setting /opt/tramontana/releases/*/tramontana to enforce mode.
$ sudo systemctl restart tramontana.service
$ ~/scripts/revision_salud.sh; echo "estado: $?"
estado: 0A partir de aquí, una denegación real aparece como apparmor="DENIED", y es lo primero que hay que mirar cuando el servicio empieza a fallar tras un despliegue que cambia rutas:
SELinux, para quien vaya a RHEL:
| AppArmor | SELinux | |
|---|---|---|
| Distribuciones | Ubuntu, Debian, SUSE | RHEL, Fedora, CentOS Stream |
| Identifica objetos por | Ruta del fichero | Etiqueta en el inodo |
| Curva de aprendizaje | Suave; perfiles legibles | Pronunciada; más potente |
| Diagnóstico | journalctl + aa-logprof |
ausearch + audit2allow, restorecon |
| Modo permisivo | Por perfil (complain) |
Global o por dominio |
La diferencia por ruta frente a etiqueta tiene una consecuencia práctica: en SELinux, mover un fichero puede cambiar su contexto y romper el acceso (de ahí restorecon); en AppArmor, un enlace simbólico o un bind mount puede saltarse una regla basada en ruta. Cada modelo tiene su punto débil.
Endurecimiento del kernel con sysctl
En 06-03 aplicaste los sysctl de red. Estos son los del propio kernel, y son solo de seguridad: los de rendimiento se ven en 07-03.
# /etc/sysctl.d/60-endurecimiento.conf
# Solo root puede leer el buffer del kernel. Evita filtrar direcciones de
# memoria y detalles del hardware utiles para construir un exploit.
kernel.dmesg_restrict = 1
# Oculta los punteros del kernel en /proc y en los registros.
kernel.kptr_restrict = 2
# ASLR completo: aleatoriza el espacio de direcciones, incluido el heap.
# 2 es el valor por defecto en Ubuntu; se declara para que sea explicito.
kernel.randomize_va_space = 2
# Impide crear enlaces duros a ficheros de los que no eres propietario y
# seguir enlaces simbolicos ajenos en directorios con sticky bit.
# Cierra toda una familia de ataques de carrera en /tmp.
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
# La misma proteccion para FIFOs y ficheros regulares en directorios
# escribibles por todos.
fs.protected_fifos = 2
fs.protected_regular = 2
# Solo root puede usar BPF. Reduce mucho la superficie del subsistema.
kernel.unprivileged_bpf_disabled = 1
# Un proceso solo puede depurar a sus descendientes directos.
# Impide que un proceso comprometido lea la memoria de otro del mismo usuario
# (y con ella los secretos que ese otro tenga cargados).
kernel.yama.ptrace_scope = 1
# No permitir a usuarios sin privilegios crear espacios de nombres de usuario.
# ATENCION: rompe los contenedores sin privilegios. Comentado porque en
# 07-05 vas a necesitarlo; descomentar solo en servidores sin contenedores.
#kernel.unprivileged_userns_clone = 0$ sudo sysctl --system 2>&1 | grep -A9 '60-endurecimiento'
* Applying /etc/sysctl.d/60-endurecimiento.conf ...
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
kernel.randomize_va_space = 2
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
fs.protected_fifos = 2
fs.protected_regular = 2
kernel.unprivileged_bpf_disabled = 1
kernel.yama.ptrace_scope = 1
$ sysctl kernel.yama.ptrace_scope
kernel.yama.ptrace_scope = 1Dos avisos que debes anotar, porque son las dos formas de que esto te muerda más adelante:
kernel.yama.ptrace_scope = 1puede romper depuradores.straceogdbsobre un proceso que ya está en marcha y no es hijo tuyo dejará de funcionar sinsudo. En 07-02 vas a usar exactamente esas herramientas; recuerda que este parámetro es la explicación si te daOperation not permitted.kernel.unprivileged_userns_clone = 0rompe contenedores sin privilegios, y en 07-05 vas a montar Docker. Por eso queda comentado con la razón escrita al lado. Un parámetro de endurecimiento con una nota explicando por qué no está activado es documentación de calidad; borrarlo y olvidar el motivo no lo es.
Y el criterio general: cada línea de este fichero lleva su comentario. Un sysctl.d con quince valores sin explicación es imposible de mantener, porque nadie —tú incluido, en seis meses— sabrá si se puede quitar alguno.
Montajes seguros
Tres opciones de montaje que limitan el daño en los directorios donde cualquiera puede escribir:
| Opción | Efecto |
|---|---|
noexec |
No se puede ejecutar nada desde ahí |
nosuid |
Se ignoran los bits SUID/SGID |
nodev |
Se ignoran los ficheros de dispositivo |
El caso de uso es directo: un atacante que consigue escribir un fichero suele necesitar ejecutarlo, y /tmp es el sitio donde casi siempre puede escribir. Con noexec, ese paso falla.
Y aquí es donde la lección insiste en algo que las guías de endurecimiento suelen omitir: noexec en /tmp rompe cosas reales. Varios instaladores extraen a /tmp y ejecutan desde allí; algunos gestores de paquetes de lenguajes también. Hay que comprobarlo antes:
# 1. Comprobar el impacto en caliente, sin tocar fstab
$ sudo mount -o remount,noexec,nosuid,nodev /tmp
# 2. Ejercitar lo que podria romperse
$ sudo apt install --reinstall -y tree >/dev/null && echo "apt: correcto"
apt: correcto
$ ~/scripts/desplegar.sh --dry-run 3.2.1 && echo "despliegue: correcto"
despliegue: correcto
$ sudo needrestart -r l >/dev/null && echo "needrestart: correcto"
needrestart: correcto
# 3. Y comprobar que la restriccion realmente actua
$ printf '#!/bin/sh\necho hola\n' > /tmp/prueba.sh && chmod +x /tmp/prueba.sh
$ /tmp/prueba.sh
bash: /tmp/prueba.sh: Permiso denegadoEse último bloque es la verificación positiva: no basta con que nada se haya roto, hay que comprobar que la medida hace algo. Con las dos comprobaciones hechas, se hace persistente:
# /etc/fstab
UUID=3f8a... /tmp ext4 defaults,noatime,noexec,nosuid,nodev 0 2
/tmp /var/tmp none bind,noexec,nosuid,nodev 0 0
tmpfs /dev/shm tmpfs defaults,noexec,nosuid,nodev 0 0$ sudo systemctl daemon-reload
$ sudo mount -a && echo "fstab correcto"
fstab correcto
$ findmnt -o TARGET,OPTIONS /tmp /var/tmp /dev/shm
TARGET OPTIONS
/tmp rw,noexec,nosuid,nodev,relatime
/var/tmp rw,noexec,nosuid,nodev,relatime
/dev/shm rw,noexec,nosuid,nodevmount -a antes de reiniciar sigue siendo obligatorio, por lo que ya sabes desde 05-04: un fstab mal escrito impide arrancar el servidor.
Gestión de vulnerabilidades y cierre del incidente de openssl
Leer un CVE sin engañarse
Un CVE es el identificador único de una vulnerabilidad concreta. Su CVSS es una puntuación de 0 a 10 acompañada de un vector que describe cómo se explota:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H → 9.8 CRÍTICA
│ │ │ │ │ │ └─ Confidencialidad/Integridad/Disponibilidad: alta
│ │ │ │ │ └─ Alcance: sin cambio
│ │ │ │ └─ Interacción del usuario: ninguna
│ │ │ └─ Privilegios requeridos: ninguno
│ │ └─ Complejidad del ataque: baja
│ └─ Vector de ataque: redY el punto que hay que interiorizar: la puntuación base no es el riesgo real para ti. Un 9.8 en un componente que no tienes instalado, o que no está expuesto, o que ya está mitigado por otra capa, es menos urgente que un 6.5 en el servicio que da la cara a Internet. El vector es más informativo que el número: AV:N (explotable por red) sin privilegios ni interacción del usuario es lo que obliga a actuar hoy.
$ apt list --upgradable
$ pro security-status
1543 packages installed:
1489 packages from Ubuntu Main/Restricted repository
54 packages from Ubuntu Universe/Multiverse repository
Ubuntu Pro is not attached. Ubuntu Pro would provide security updates for
54 packages until 2034.
$ sudo apt install debsecan && debsecan --suite noble --format summary | head -5El cierre del incidente
El 18 de agosto a las 06:12, unattended-upgrades actualizó openssl y libssl3t64. El paquete está actualizado en disco, pero los procesos que arrancaron antes siguen con la biblioteca antigua cargada en memoria. Es una vulnerabilidad con papeleo: el informe dice que está parcheada y el servicio sigue expuesto.
$ grep -E 'openssl|libssl' /var/log/apt/history.log | tail -2
Start-Date: 2026-08-18 06:12:03
Upgrade: libssl3t64:amd64 (3.0.13-0ubuntu3.4, 3.0.13-0ubuntu3.5), openssl:amd64 (3.0.13-0ubuntu3.4, 3.0.13-0ubuntu3.5)
# La prueba directa: procesos con la biblioteca ANTIGUA (marcada DEL: borrada
# del disco pero aun mapeada en memoria)
$ sudo lsof 2>/dev/null | grep -E 'DEL.*libssl'
postgres 1105 postgres DEL REG 253,0 /usr/lib/x86_64-linux-gnu/libssl.so.3
sshd 894 root DEL REG 253,0 /usr/lib/x86_64-linux-gnu/libssl.so.3
$ sudo apt install needrestart
$ sudo needrestart -r l
Scanning processes...
Scanning candidates...
Scanning linux images...
Services to be restarted:
systemctl restart [email protected]
systemctl restart ssh.service
Service restarts being deferred:
(none)
No containers need to be restarted.
No user sessions are running outdated binaries.
No VM guests are running outdated hypervisor (qemu) binaries on this host.Dos servicios. El orden importa, y por dos razones distintas:
# 1. La base de datos primero, y con la aplicacion preparada para reconectar.
# Reiniciar postgresql corta las conexiones abiertas de tramontana.service.
$ sudo systemctl restart [email protected]
$ sudo systemctl restart tramontana.service
$ ~/scripts/revision_salud.sh; echo "estado: $?"
estado: 0
# 2. SSH al final, con RELOAD y no restart, y con la segunda sesion abierta.
# reload no corta las sesiones existentes; restart tampoco deberia, pero
# reload es la operacion correcta y no hay razon para arriesgarse.
$ sudo sshd -t && sudo systemctl reload sshUn matiz importante sobre ssh: reload recarga la configuración, pero no sustituye la biblioteca cargada en el proceso maestro. Para eso hace falta reiniciar el demonio — que no corta las sesiones ya establecidas, porque cada una vive en un proceso hijo propio. Con la segunda sesión abierta como red de seguridad:
$ sudo systemctl restart ssh
$ sudo systemctl is-active ssh
active
# Verificar desde la segunda sesion que se puede abrir una tercera ANTES de cerrar nadaY la verificación de cierre:
$ sudo lsof 2>/dev/null | grep -c -E 'DEL.*libssl'
0
$ sudo needrestart -r l
No services need to be restarted.
$ sudo ss -tulpn | grep -E ':22|:5432|:8080'
tcp LISTEN 0.0.0.0:22 users:(("sshd",pid=8841,fd=3))
tcp LISTEN 10.0.2.15:5432 users:(("postgres",pid=8902,fd=5))
tcp LISTEN 127.0.0.1:8080 users:(("tramontana",pid=8977,fd=6))Ese 0 es el cierre. El segundo incidente queda cerrado, y con él los tres.
Para que no vuelva a pasar, needrestart se configura en modo lista y se integra en la revisión:
# /etc/needrestart/needrestart.conf
$nrconf{restart} = 'l'; # solo listar, nunca reiniciar solo
$nrconf{kernelhints} = 1; # avisar si el kernel en uso no es el instalado# Anadir a revision_salud.sh: pendientes de reinicio tras actualizacion
$ sudo needrestart -r l -p >/dev/null; echo "codigo needrestart: $?"
codigo needrestart: 0La decisión de no reiniciar automáticamente es deliberada y coherente con el unattended-upgrades de 05-03: un reinicio no supervisado de la base de datos a las 6 de la mañana es una interrupción de servicio que Marta debe conocer, no un efecto secundario.
Endurecimiento de systemd revisitado
En 05-05 endureciste tramontana.service. Ahora se mide y se mejora:
$ systemd-analyze security tramontana.service | tail -12
→ Overall exposure level for tramontana.service: 3.4 MEDIUM 🙂3.4 MEDIUM para un servicio de red no está mal, y hay margen. Las directivas que faltan, cada una con su razón:
# /etc/systemd/system/tramontana.service.d/endurecimiento.conf
[Service]
# El servicio no necesita ajustar el kernel ni cargar modulos.
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
# No debe manipular la jerarquia de cgroups.
ProtectControlGroups=yes
# No debe crear ficheros SUID/SGID: cierra una via clasica de persistencia.
RestrictSUIDSGID=yes
# Impide cambiar la "personalidad" del proceso para emular otra arquitectura.
LockPersonality=yes
# Solo llamadas al sistema propias de un servicio normal. El resto se
# rechaza con EPERM en el kernel, antes de llegar al codigo del proceso.
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @obsolete
SystemCallArchitectures=native
# Solo las familias de sockets que necesita.
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
# Sin acceso a dispositivos ni a /home
PrivateDevices=yes
ProtectHome=yes
ProtectProc=invisible
ProcSubset=pid
# ATENCION: MemoryDenyWriteExecute rompe los tiempos de ejecucion con JIT
# (Java, Node.js, .NET, algunos Python). Solo si la aplicacion es nativa.
MemoryDenyWriteExecute=yesSystemCallFilter merece explicación porque es la directiva de más valor: instala un filtro seccomp en el kernel que rechaza las llamadas al sistema fuera del conjunto permitido. Un exploit que consiga ejecución de código dentro del proceso se encuentra con que mount, ptrace, init_module o setuid fallan con EPERM — no porque falten permisos de usuario, sino porque el kernel no las acepta de este proceso. Es contención real, no configuración cosmética.
Y el aviso de MemoryDenyWriteExecute es el tipo de detalle que separa una guía útil de una lista copiada: impide que una página de memoria sea escribible y ejecutable a la vez, lo que bloquea muchas técnicas de explotación, y rompe cualquier motor con compilación en tiempo de ejecución. Si la aplicación de Tramontana fuera Java o Node, esta línea la dejaría sin arrancar.
$ sudo systemctl daemon-reload && sudo systemctl restart tramontana.service
# Verificar que el servicio SIGUE FUNCIONANDO: endurecer y romper no es endurecer
$ ~/scripts/revision_salud.sh; echo "estado: $?"
estado: 0
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/casas
200
$ systemd-analyze security tramontana.service | tail -3
→ Overall exposure level for tramontana.service: 1.6 OK 🙂De 3.4 MEDIUM a 1.6 OK, con el servicio funcionando y verificado. El orden —aplicar, verificar, medir— es el mismo que en la rotación de credenciales y en el diagnóstico de rendimiento. Aplicar sin verificar es cómo se descubre a las 3 de la madrugada que el endurecimiento tumbó el servicio.
Copias como último control frente al ransomware
Cuando todo lo anterior falla, quedan las copias. Y frente al ransomware hay un detalle que suele pasarse por alto: una copia montada permanentemente es alcanzable por el ransomware, porque el proceso que cifra los ficheros del sistema cifra también los del volumen montado. Una copia cifrada por el atacante no es una copia.
Las tres propiedades que hacen que una copia resista:
| Propiedad | Cómo se consigue | Estado en Tramontana |
|---|---|---|
| Fuera de línea o inmutable | Medio desconectado, o almacenamiento con retención forzada | Pendiente para el destino externo del 3-2-1 |
| Credencial separada | La contraseña del repositorio no es accesible desde el servidor comprometido | Parcial: está en pass, pero el servidor puede leerla |
| Restauración probada | Prueba periódica documentada | Hecho: los tres escenarios de 05-08 |
La mejora concreta que se deduce: restic admite el modo append-only en el destino remoto, en el que el cliente puede escribir copias nuevas pero no borrar las existentes. Con eso, un atacante con acceso al servidor no puede destruir el histórico. Es la medida que hay que pedir al proveedor del almacenamiento externo, y queda anotada en la checklist.
Checklist de endurecimiento y mantenimiento de la postura
Esta es la tabla que se entrega, alineada con la estructura de los CIS Benchmarks y con lo que has hecho realmente en el curso. Las tres columnas son deliberadas: sin la de evidencia, una checklist es una declaración de intenciones.
| # | Control | Estado | Evidencia |
|---|---|---|---|
| 1 | Sistema de ficheros: noexec,nosuid,nodev en /tmp, /var/tmp, /dev/shm |
Hecho | findmnt -o TARGET,OPTIONS |
| 2 | Cifrado en reposo del volumen de copias | Hecho | cryptsetup luksDump, /etc/crypttab |
| 3 | Repositorios firmados; solo actualizaciones oficiales | Hecho | /etc/apt/sources.list.d/*.sources, /etc/apt/keyrings/ |
| 4 | Actualizaciones de seguridad automáticas | Hecho | unattended-upgrades, solo -security |
| 5 | Servicios pendientes de reinicio tras actualizar | Hecho | needrestart -r l sin pendientes; incidente cerrado |
| 6 | Superficie de escucha mínima | Hecho | ss -tulpn: 22, 5432 interno, 8080 en localhost |
| 7 | Servicios innecesarios deshabilitados y enmascarados | Hecho | ModemManager enmascarado y purgado |
| 8 | Sin entorno gráfico | Hecho | Instalación de Ubuntu Server |
| 9 | Cortafuegos con política de denegación por defecto | Hecho | ufw status verbose; ruleset nftables documentado |
| 10 | sysctl de red endurecidos |
Hecho | /etc/sysctl.d/ (06-03) |
| 11 | sysctl de kernel endurecidos |
Hecho | /etc/sysctl.d/60-endurecimiento.conf, comentado |
| 12 | SSH: sin root, sin contraseña, solo claves | Hecho | sshd -T -C user=...; excepción de Match eliminada |
| 13 | Bloqueo por intentos fallidos (IP y cuenta) | Hecho | fail2ban-client status sshd, faillock --user |
| 14 | Política de contraseñas y caducidad | Hecho | pwquality.conf, login.defs, chage -l |
| 15 | Un único UID 0; sin cuentas sin contraseña | Hecho | Los tres one-liners de auditoría |
| 16 | Mínimo privilegio en sudo |
Hecho | /etc/sudoers.d/tramontana con Cmnd_Alias |
| 17 | MAC: perfil de AppArmor en enforce |
Hecho | aa-status, perfil de tramontana |
| 18 | Servicio con endurecimiento de systemd | Hecho | systemd-analyze security: 1.6 OK |
| 19 | Secretos fuera de los ficheros de configuración | Hecho | LoadCredentialEncrypted, pass |
| 20 | TLS: material emitido, verificado y con renovación probada | Hecho | certbot renew --dry-run, revisar_certificado.sh |
| 21 | Registro persistente con rotación | Hecho | /var/log/journal, logrotate.d/tramontana |
| 22 | Integridad de ficheros vigilada, base de datos fuera | Hecho | tramontana-integridad.timer, suma en el portátil |
| 23 | Auditoría de accesos a configuración e identidades | Hecho | auditctl -l, claves tramontana_conf y privilegios |
| 24 | Copias 3-2-1, cifradas, con restauración probada | Hecho | restic snapshots, comprobar_copia.sh, runbook |
| 25 | Cifrado en tránsito en vigor | Pendiente (08-01) | Certificado listo; falta el proxy inverso. El 8080 no está expuesto |
| 26 | Copia externa en modo append-only |
Pendiente | Requiere decisión de proveedor; mitiga ransomware |
| 27 | authorized_keys2 sin explicar |
Abierto | Procedimiento de respuesta a incidentes de 06-04 en curso |
| 28 | Registros centralizados fuera del servidor | Asumido | Coste actual; primera inversión cuando haya presupuesto |
| 29 | NIDS | Asumido | Aporta poco con un solo servidor (razonado en 06-04) |
| 30 | Antivirus | Asumido | Servidor sin ficheros de terceros; consume memoria acotada |
| 31 | Segundo factor en SSH | Asumido | Acceso ya por clave; se revisa si crece el equipo |
| 32 | Seguridad de la aplicación (inyección, lógica) | Fuera de alcance | Responsabilidad de desarrollo |
| 33 | Seguridad física y del hipervisor | Fuera de alcance | Responsabilidad del proveedor |
| 34 | Auditoría formal de seguridad | Fuera de alcance | Requiere tercero independiente |
Las filas 25 a 27 son las importantes de verdad, porque son las que un informe honesto no oculta. Y las filas 32 a 34 delimitan lo que un administrador no puede resolver, que es información que la dirección necesita tener.
Mantener la postura
Un sistema endurecido se degrada solo: cada cambio, cada paquete nuevo, cada regla «temporal» lo aleja del estado que documentaste. Cuatro prácticas lo sostienen:
- Revisión periódica con métricas comparables. Lynis mensual, con su índice de endurecimiento como serie temporal junto a la línea base de rendimiento de 05-07:
Del 68 de 06-04 al 82 actual. Lo relevante no es el número: es que si baja, hay una explicación que buscar.$ sudo lynis audit system --quiet | grep 'Hardening index' Hardening index : 82 [################ ] - Gestión del cambio. Toda modificación de configuración con su copia previa, su
diff -u, su verificación y su anotación. Es la convención del curso, y es el control contra el actor más probable del modelo de amenazas. - Documentación viva en el runbook, fuera del servidor: el modelo de amenazas, esta checklist con fechas, el inventario de secretos con su última rotación, y el procedimiento de respuesta a incidentes.
- Revisión del propio modelo de amenazas cuando cambie la realidad: un servidor más, un servicio nuevo, un dato nuevo que se empieza a tratar. Un modelo de amenazas de hace dos años describe un sistema que ya no existe.
Advertencia de cumplimiento
srv-tramontana trata datos personales de huéspedes. El RGPD exige medidas técnicas y organizativas apropiadas al riesgo (art. 32), y varias de las decisiones de esta lección —la retención de registros, el alcance de la auditoría sobre la actividad de personas trabajadoras, el plazo de despliegue del cifrado en tránsito de la fila 25— tienen implicaciones legales que no corresponde decidir a un administrador. En un entorno real:
- El responsable de seguridad debe revisar el modelo de amenazas y la checklist, y validar los controles asumidos.
- El responsable de protección de datos debe validar el tratamiento, los plazos de retención y la evaluación del riesgo residual.
- Y esta checklist no sustituye a una auditoría formal. Es una autoevaluación honesta, hecha por quien administra el sistema, y por definición tiene el punto ciego de quien lo ha construido.
Errores Comunes y Consejos
- Endurecer sin modelo de amenazas. Es la causa de la mitad de los sistemas frágiles: medidas copiadas que rompen cosas y no protegen contra nada que fuera probable. Media página de modelo cambia todas las decisiones que vienen después.
- Tocar PAM sin una sesión de root abierta. Un error de sintaxis en
common-authpuede impedir todo inicio de sesión, incluida la consola. Sesión de root en otra terminal, copia previa, y prueba en una tercera. - Olvidar
pam_faillock.so authsucc. Sin esa línea el contador de fallos nunca se reinicia, y el bloqueo llega días después sin causa aparente. - Olvidar
enforce_for_root = 1enpwquality.conf: la cuenta más importante queda exenta de la política de contraseñas. - Poner un perfil de AppArmor directamente en
enforce. Rompe el servicio y te obliga a descubrir los accesos que faltan bajo presión. Siemprecomplainprimero, ejercitar de verdad, y luegoenforce. - Aplicar
noexeca/tmpsin comprobarlo. Hay instaladores que extraen y ejecutan ahí. Prueba conmount -o remounty ejercitaapty tus scripts antes de tocarfstab. - Editar
fstabsinmount -a. Unfstabroto impide arrancar el servidor. Es la red de seguridad de 05-04 y sigue siendo obligatoria. - Confundir «paquete actualizado» con «vulnerabilidad corregida». Mientras el proceso tenga la biblioteca vieja en memoria, sigue expuesto.
needrestart -r ldespués de cada actualización, ylsof | grep DELcomo confirmación. - Endurecer y no verificar.
systemd-analyze securitymejora la puntuación aunque hayas dejado el servicio sin arrancar. Aplicar, verificar conrevision_salud.sh, y luego medir. MemoryDenyWriteExecuteen una aplicación con JIT. Java, Node y .NET no arrancan. Lee lo que hace cada directiva antes de copiarla.- Una checklist sin columna de evidencia. «Firewall configurado» no dice nada;
ufw status verbosesí. Sin evidencia, la checklist es una declaración de intenciones. - Consejo de método. Convierte las auditorías de esta lección en
auditar_endurecimiento.shusandolib/comunes.sh, con un timer mensual y el principio de silencio si todo va bien. Lo que depende de tu memoria no es un control.
Ejercicios
Ejercicio 1
Tras aplicar el drop-in de endurecimiento, tramontana.service deja de arrancar:
$ sudo systemctl status tramontana.service --no-pager | head -6
● tramontana.service - Tramontana Reservas
Active: failed (Result: exit-code) since Tue 2026-08-18 16:04:11 CEST
Process: 9214 ExecStart=/opt/tramontana/app/tramontana ... (code=killed, signal=SYS)Describe el procedimiento de diagnóstico, identifica la causa más probable a partir de la información del mensaje, y explica cómo acotarías qué directiva concreta lo provoca sin quitarlas todas de golpe.
Ejercicio 2
Escribe el perfil de AppArmor para el envoltorio /home/operador/bin/desplegar-seguro, que se ejecuta vía sudo y llama a desplegar.sh. Razona qué debe permitir y —más importante— qué debe denegar, y explica qué ataque concreto contiene ese perfil que ni el DAC ni la regla de sudoers contienen.
Ejercicio 3
Marta te pide un informe de una página para la dirección: «¿estamos seguros?». Redáctalo apoyándote en el modelo de amenazas y en la checklist, sin tecnicismos innecesarios, diciendo con honestidad qué está protegido, qué no y qué decisiones necesitan presupuesto o autorización.
Soluciones
Solución 1
La pista está en el propio mensaje: code=killed, signal=SYS. SIGSYS es la señal que envía el kernel cuando un filtro seccomp rechaza una llamada al sistema. Eso apunta directamente a SystemCallFilter, y no a las demás directivas (que producirían típicamente EACCES, EPERM o un fallo de ruta).
Procedimiento, de lo más informativo a lo más costoso:
# 1. Confirmar la hipotesis y ver QUE llamada se rechazo
$ sudo journalctl -u tramontana.service -n 30 --no-pager | grep -iE 'seccomp|SYS|syscall'
audit: type=1326 audit(1755530651.882:88): auid=4294967295 uid=997 pid=9214
comm="tramontana" exe="/opt/tramontana/releases/3.2.1/tramontana"
syscall=318 compat=0 ip=0x7f2a1c4b3e2a code=0x80000000
# 2. Traducir el numero de llamada a su nombre
$ ausyscall 318
getrandomgetrandom es la llamada para obtener aleatoriedad del kernel — la aplicación la usa para TLS y para identificadores de sesión. Está en @system-service, pero la excluí al añadir SystemCallFilter=~@privileged @resources @obsolete, y getrandom pertenece al conjunto @resources en algunas versiones de systemd. La segunda línea, que parecía una mejora, es la que rompe el servicio.
Cómo acotar sin quitarlo todo, que es la parte de método del ejercicio. La clave es que los drop-ins se pueden apilar y anular por separado:
# a) Un drop-in temporal que solo neutraliza la directiva sospechosa.
# La linea vacia RESETEA la lista acumulada; luego se pone solo lo minimo.
$ sudo mkdir -p /etc/systemd/system/tramontana.service.d
$ sudo tee /etc/systemd/system/tramontana.service.d/99-diagnostico.conf >/dev/null <<'EOF'
[Service]
SystemCallFilter=
SystemCallFilter=@system-service
EOF
$ sudo systemctl daemon-reload && sudo systemctl restart tramontana.service
$ ~/scripts/revision_salud.sh; echo "estado: $?"
estado: 0Arranca. Con eso queda confirmado que el problema era la exclusión, no el conjunto base. Ahora se afina en lugar de renunciar al filtro:
# b) Recuperar la restriccion util, devolviendo solo lo necesario
$ sudo tee /etc/systemd/system/tramontana.service.d/99-diagnostico.conf >/dev/null <<'EOF'
[Service]
SystemCallFilter=
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @obsolete
SystemCallFilter=getrandom
EOF
$ sudo systemctl daemon-reload && sudo systemctl restart tramontana.service
$ ~/scripts/revision_salud.sh && systemd-analyze security tramontana.service | tail -1
→ Overall exposure level for tramontana.service: 1.7 OK 🙂Se mantiene ~@privileged @obsolete —que es donde está casi todo el valor defensivo— se abandona @resources, y se añade getrandom explícitamente. Exposición 1.7 en lugar de 1.6, con el servicio funcionando: un endurecimiento peor sobre el papel y infinitamente mejor en la práctica, porque el de 1.6 no arrancaba.
Método general aplicable a cualquier fallo de endurecimiento:
- Leer la señal o el código de error:
SIGSYS→ seccomp;EPERMen un fichero →ProtectSystem/ReadWritePaths;EACCESconapparmor="DENIED"→ AppArmor; fallo al mapear memoria →MemoryDenyWriteExecute. - Buscar la evidencia concreta en el journal (
syscall=,denied_mask=, la ruta). - Neutralizar una directiva con un drop-in de mayor prefijo numérico, nunca editando el original.
- Verificar que arranca, afinar hacia el mínimo necesario, y volver a medir.
- Consolidar el drop-in con un nombre definitivo y dejar escrito el motivo de la excepción — igual que el
unprivileged_userns_clonecomentado delsysctl.
Solución 2
# /etc/apparmor.d/home.operador.bin.desplegar-seguro
abi <abi/4.0>,
include <tunables/global>
/home/operador/bin/desplegar-seguro {
include <abstractions/base>
include <abstractions/bash>
# El envoltorio y el script que invoca
/home/operador/bin/desplegar-seguro r,
/home/operador/scripts/desplegar.sh rix,
/home/operador/scripts/lib/comunes.sh r,
/usr/bin/bash rix,
# Utilidades concretas que el script necesita, ENUMERADAS
/usr/bin/{tar,curl,ln,sha256sum,systemctl,flock,mktemp,rm,mv,date,logger} rix,
# Origen del paquete de release: solo lectura
/srv/tramontana/backups/envios/*.tar.gz r,
/srv/tramontana/backups/envios/*.sha256 r,
# Destino del despliegue: escritura acotada a releases y al enlace
/opt/tramontana/releases/ rw,
/opt/tramontana/releases/** rw,
/opt/tramontana/app rw,
/opt/tramontana/HISTORIAL rw,
# Registro y bloqueo
/var/log/tramontana/despliegue.log rw,
/var/lock/tramontana-despliegue.lock rwk,
/tmp/ r,
/tmp/** rw,
# Consultar el estado del servicio tras el despliegue
/run/systemd/private rw,
# DENEGADO de forma explicita, para que conste y quede registrado:
deny /etc/shadow rwklx,
deny /etc/tramontana/secretos/** rwklx,
deny /srv/tramontana/backups/restic/** rwklx,
deny /home/operador/.ssh/** rwklx,
deny /home/operador/.password-store/** rwklx,
deny /root/** rwklx,
deny /usr/bin/{nc,ncat,socat,ssh,scp,python3,perl} x,
}Qué permite: leer el paquete de release, escribir en releases/, cambiar el enlace app, escribir su registro y su bloqueo, y hablar con systemd para reiniciar el servicio. Nada más.
Qué deniega, y qué ataque contiene cada denegación:
| Denegación | Ataque que contiene |
|---|---|
/etc/tramontana/secretos/** |
El proceso corre como root vía sudo, así que el DAC le permitiría leer la credencial cifrada. AppArmor no |
/srv/tramontana/backups/restic/** |
Un despliegue no tiene ninguna razón para tocar las copias. Contiene el borrado de copias por ransomware |
/home/operador/.password-store/** |
Impide que el envoltorio acceda a la fuente de verdad de los secretos |
/home/operador/.ssh/** |
Bloquea añadir una clave autorizada — el mecanismo de persistencia que AIDE detectó en 06-04 |
x sobre nc, socat, python3, perl |
Bloquea la shell inversa, que es el primer paso tras conseguir ejecución con privilegios |
Y ahora el fondo del ejercicio: qué contiene este perfil que el DAC y sudoers no contienen.
La regla de sudoers permite a operador ejecutar desplegar-seguro como root. A partir de ese momento, el DAC no ofrece ninguna protección: root puede leer /etc/shadow, puede leer la credencial de la base de datos, puede borrar las copias, puede añadirse una clave SSH y puede lanzar nc para abrir una shell hacia el exterior. El envoltorio se escribió precisamente para evitar dar root completo, pero la contención depende por entero de que el contenido del script sea correcto y siga siéndolo.
Ahí está el hueco. Si desplegar.sh tiene un fallo —una variable sin entrecomillar que permita inyectar un comando, un tar que extraiga un fichero con ../ en su ruta— el atacante ejecuta código como root a través de una vía autorizada. Ni sudo ni los permisos Unix lo detienen: la ejecución es legítima desde su punto de vista.
El perfil de AppArmor es la capa que sí lo detiene, porque el control no depende del usuario sino del programa: sea cual sea el código que se acabe ejecutando dentro de este perfil, no podrá leer los secretos, no podrá tocar las copias, no podrá escribir en authorized_keys y no podrá lanzar un intérprete para salir. El daño queda acotado al despliegue, que es exactamente lo que se pretendía cuando se escribió el envoltorio y lo que hasta ahora era solo una intención.
Eso es defensa en profundidad en su forma más concreta: el sudoers limita quién y qué comando; AppArmor limita qué puede hacer ese comando, incluso cuando el comando se comporta mal.
Solución 3
Informe de seguridad —
srv-tramontana/ Tramontana Reservas Para: Dirección · De: Operaciones de sistemas · 18 de agosto de 2026En una frase. El servidor está razonablemente protegido frente a las amenazas que de verdad le afectan, con tres cuestiones pendientes que detallo al final, una de las cuales requiere una decisión de dirección.
Qué protegemos, por orden de importancia. Primero, los datos personales de nuestros huéspedes: nombres, fechas de estancia e importes. Su filtración sería irreversible, dañaría a terceros y tiene consecuencias legales. Segundo, la disponibilidad del servicio de reservas, cuyo coste es económico y acotado en el tiempo.
De quién nos protegemos, y qué hemos hecho.
- Ataques automatizados desde Internet, que son constantes. El servidor solo acepta conexiones por dos puertas: la de administración —que ya no admite contraseñas, solo claves criptográficas— y la del sitio web. Todo lo demás está cerrado por defecto, y quien insiste en probar contraseñas queda bloqueado automáticamente. En el último mes se han bloqueado 47 intentos desde una misma procedencia.
- Robo o filtración de una contraseña. Las credenciales del sistema ya no están escritas en ningún fichero legible: están cifradas y solo el programa que las necesita puede usarlas. Tenemos además un procedimiento escrito para cambiarlas sin interrumpir el servicio.
- Nuestros propios errores, que estadísticamente son el riesgo más probable. Toda modificación se hace con copia previa, se comprueba antes de aplicarse y queda registrada. Un sistema independiente vigila cada día que ningún fichero crítico haya cambiado sin explicación.
- Uso indebido de accesos legítimos. Cada persona tiene solo los permisos que su trabajo requiere, y los accesos a la configuración quedan registrados.
Qué hacemos si algo falla. Tenemos copias de seguridad cifradas, con la restauración probada —no solo configurada—, capaces de recuperar el servicio en 8 horas perdiendo como máximo 4 horas de reservas, plazos que se acordaron con Operaciones. Y un procedimiento escrito de respuesta a incidentes, guardado fuera del servidor.
Las tres cuestiones pendientes.
- El tráfico web todavía no viaja cifrado. El certificado está emitido y verificado, pero falta desplegar el componente que lo presentará, previsto en la siguiente fase. Mientras tanto el servicio no es accesible desde Internet, así que la exposición se limita a nuestra red interna. No requiere decisión: está planificado.
- Un hallazgo sin explicar. Nuestra vigilancia detectó un fichero de configuración de acceso que nadie creó. Está en investigación con el procedimiento previsto. Informaré del resultado; si se confirmara acceso a datos personales, hay obligación legal de notificarlo en 72 horas.
- Copia externa protegida contra borrado. Hoy, quien tuviera control del servidor podría destruir las copias. Existe una modalidad de almacenamiento que no permite borrar lo ya guardado, y es la mejor defensa disponible frente a un ataque de secuestro de datos. Requiere decisión de dirección: implica contratar almacenamiento externo.
Qué no está en nuestras manos. Tres cosas quedan fuera de lo que la administración de sistemas puede resolver, y conviene que consten: la seguridad interna de la propia aplicación, que corresponde a desarrollo; la seguridad física y del proveedor de infraestructura; y un atacante con recursos muy superiores a los de una empresa de nuestro tamaño.
Y una advertencia necesaria. Este informe es una autoevaluación hecha por quien administra el sistema, y por tanto tiene el punto ciego de quien lo ha construido. No sustituye a una auditoría independiente. Recomiendo, además, que el tratamiento de datos de huéspedes y los plazos de conservación de registros los revise el responsable de protección de datos.
En resumen: el trabajo está hecho, medido y documentado. La única decisión que necesito de la dirección es la del punto 3.
Conclusión
srv-tramontana tiene ahora una postura de seguridad, y eso es cualitativamente distinto de tener medidas de seguridad. Hay un modelo de amenazas escrito que dice qué protegemos, de quién y qué asumimos fuera de alcance, y cada decisión de esta lección se justifica en él. La superficie de ataque se ha reducido: la aplicación escucha solo en local, lo que no se usa está enmascarado y purgado. La autenticación pasa por PAM con política de contraseñas, bloqueo por cuenta y límites de recursos —y localizaste el Match User luis que llevaba semanas contradiciendo tu propia configuración—. AppArmor contiene la aplicación en enforce, con un perfil que impide lanzar una shell o leer las copias aunque el proceso se vea comprometido. Los sysctl de endurecimiento están aplicados y comentados, incluida la línea que dejaste desactivada con la razón escrita al lado. /tmp está sin permiso de ejecución, comprobado antes de hacerlo persistente. Y systemd-analyze security ha pasado de 3.4 a 1.6 con el servicio verificado y funcionando, que es la única mejora que cuenta.
Los tres incidentes están cerrados: los 47 intentos desde 203.0.113.44 con fail2ban, la db_password en claro con systemd-creds y pass, y hoy la libssl vulnerable que seguía cargada en memoria — porque un paquete actualizado no es una vulnerabilidad corregida hasta que los procesos lo saben. Queda una checklist de 34 filas con su columna de evidencia, tres cuestiones abiertas dichas sin adornos y un informe para la dirección que no promete lo que no puede cumplir. El índice de Lynis ha subido de 68 a 82, y lo importante de ese número no es su valor sino que ahora es una serie temporal que alguien vigila.
Y con esto se cierra el Módulo 6: configuraste la red de forma persistente con la reversión automática de netplan try; endureciste SSH con claves ed25519, sin root y sin contraseñas, y aprendiste a leer sshd -T -C para saber qué configuración se aplica realmente a alguien; levantaste un cortafuegos de lista blanca sobre nftables y supiste el orden en que se aplica para no quedarte fuera; montaste detección de intrusos con AIDE y auditd, con la base de datos protegida fuera del servidor, y un procedimiento de respuesta a incidentes con su regla incómoda; sacaste los secretos de los ficheros de configuración y preparaste el material criptográfico de TLS con renovación probada y vigilancia de la caducidad; y hoy has reunido todo eso en una postura coherente y documentada.
Hasta aquí has operado un servidor, y lo has operado a mano. Sabes usarlo, administrarlo y defenderlo, pero hay dos cosas que todavía no sabes hacer: mirar dentro y multiplicarlo. En el Módulo 7: Temas Avanzados haces las dos. Verás el proceso de arranque completo —de la UEFI a GRUB, al initramfs y a systemd— y aprenderás a recuperar un sistema que no arranca, que es la habilidad que separa a quien reinstala de quien arregla. Aprenderás diagnóstico avanzado con strace, perf y eBPF para responder a «¿qué está haciendo exactamente este proceso?» cuando los contadores no bastan (y descubrirás que el ptrace_scope que acabas de poner es parte del enunciado del problema). Ajustarás el kernel con sysctl, ahora sí por rendimiento. Y después empieza la multiplicación: virtualización con KVM y libvirt, contenedores con Docker y por qué son procesos aislados y no máquinas pequeñas, automatización con Ansible —que convertirá todo el trabajo manual de los módulos 5 y 6 en código versionado y reproducible, y las 8 horas de RTO en bastante menos—, y por fin alta disponibilidad y balanceo de carga, donde srv-tramontana deja de ser un único punto de fallo. Actualiza el snapshot de tu VM, guarda el runbook fuera de la máquina, y nos vemos en el Módulo 7.
Curso de Linux: De Principiante a Administrador de Sistemas
Módulo 1: Introducción a Linux
- ¿Qué es Linux?
- Historia de Linux
- Distribuciones de Linux
- Instalando Linux
- Primer Contacto con el Sistema
- Estructura del Sistema de Archivos de Linux
Módulo 2: Comandos Básicos de Linux
- Introducción a la Línea de Comandos
- Obtener Ayuda y Documentación del Sistema
- Navegando el Sistema de Archivos
- Operaciones con Archivos y Directorios
- Visualización y Edición de Archivos
- Enlaces Duros y Simbólicos
- Permisos y Propiedad de Archivos
Módulo 3: Habilidades Avanzadas en la Línea de Comandos
- El Entorno del Shell: Variables, Alias e Historial
- Uso de Comodines y Expresiones Regulares
- Búsqueda de Archivos y Contenido: find, locate y grep
- Tuberías y Redirección
- Procesamiento de Texto: cut, sort, uniq, sed y awk
- Gestión de Procesos
- Programación de Tareas con Cron
- Comandos de Redes
Módulo 4: Scripting en Shell
- Introducción al Scripting en Shell
- Variables y Tipos de Datos
- Entrada, Salida y Argumentos de un Script
- Estructuras de Control
- Funciones y Librerías
- Depuración y Manejo de Errores
- Scripts de Producción: Buenas Prácticas
Módulo 5: Administración del Sistema
- Gestión de Usuarios y Grupos
- sudo y Permisos Especiales
- Gestión de Paquetes
- Gestión de Discos
- systemd y la Gestión de Servicios
- Registros del Sistema: journald y syslog
- Monitoreo del Sistema y Optimización del Rendimiento
- Respaldo y Restauración
Módulo 6: Redes y Seguridad
- Configuración de Redes
- SSH y Acceso Remoto
- Firewall y Seguridad Perimetral
- Sistemas de Detección de Intrusos
- Gestión de Secretos y Certificados TLS
- Asegurando Sistemas Linux
Módulo 7: Temas Avanzados
- El Proceso de Arranque y la Recuperación del Sistema
- Diagnóstico Avanzado: strace, perf y eBPF
- Optimización del Kernel de Linux
- Virtualización con Linux
- Contenedores de Linux y Docker
- Automatización con Ansible
- Alta Disponibilidad y Balanceo de Carga
