La lección anterior terminó señalando dos deudas que arrastras desde el Módulo 5, y las dos son criptográficas. La primera: db_password está escrita en claro dentro de /etc/tramontana/app.conf, y ya has visto que cualquier fallo de permisos convierte eso en una fuga de credenciales que ninguna vigilancia evita. La segunda: el tráfico de reservas —con los nombres de los huéspedes que tanto cuidado has puesto en proteger dentro de las copias— viaja sin cifrar por el puerto 8080. Esta lección resuelve ambas, porque son el mismo problema visto desde dos ángulos: guardar un secreto y demostrar una identidad.

Son dos partes claramente separadas. En la primera aprenderás a sacar un secreto de un fichero de configuración y a gestionarlo con las herramientas que el sistema ya te da: permisos estrictos, EnvironmentFile y LoadCredential de systemd, GPG, pass y LUKS. En la segunda, qué es realmente un certificado, cómo se genera, se lee y se verifica, cómo se obtiene uno gratuito y automáticamente renovado de Let's Encrypt, y cómo vigilar su caducidad — que es el fallo más tonto y más frecuente de producción.

Aviso de alcance. Aquí se obtiene, se instala y se verifica el material criptográfico de reservas.tramontana.example. El servidor web que lo servirá —Nginx como proxy inverso delante de la aplicación del puerto 8080— se monta en el proyecto 08-01. Es deliberado: el cifrado en tránsito se prepara y se prueba ahora para que en el Módulo 8 sea un detalle de configuración y no un tema nuevo.

Advertencia de cumplimiento. El cifrado de datos personales, en tránsito y en reposo, no es una mejora opcional: el RGPD lo exige como medida técnica apropiada (art. 32). reservas.csv contiene nombres de huéspedes. En un entorno real, el diseño de la gestión de claves y de su custodia debe revisarlo el responsable de seguridad, y las decisiones sobre datos personales el responsable de protección de datos.

Contenido

  1. Qué es un secreto y dónde no va nunca
  2. El mínimo decente: permisos, EnvironmentFile y LoadCredential
  3. Cifrado en reposo de secretos: GPG y pass
  4. Cifrado de disco con LUKS
  5. Gestores centralizados de secretos
  6. Rotación de credenciales
  7. Qué garantiza TLS y qué contiene un certificado
  8. Generar, leer y verificar material criptográfico con OpenSSL
  9. Let's Encrypt y la renovación automática
  10. Buenas prácticas de TLS, custodia de la clave y vigilancia de la caducidad

Qué es un secreto y dónde no va nunca

Un secreto es un dato cuyo valor depende de que solo lo conozca quien debe: una contraseña, una clave de API, un token, una clave privada, la frase de paso de un repositorio de copias. Se distingue de un dato sensible cualquiera en que su compromiso no es un problema de privacidad sino de control: quien lo tiene puede actuar en tu nombre.

La primera regla es negativa, y conviene entender el por qué de cada punto porque cada uno corresponde a una fuga real y frecuente:

Dónde NO va un secreto Por qué
En el código fuente Acaba en el repositorio, y en git el historial es permanente: borrar la línea no borra el commit
En la línea de comandos Cualquier usuario del sistema la ve con ps aux mientras el proceso corre
En una variable de entorno de un proceso Legible en /proc/<pid>/environ por su propietario y por root; y se hereda a los procesos hijos
En los registros Un set -x o un log "conectando con $PASS" lo escribe en el journal, que se conserva 30 días
En una copia de seguridad sin cifrar Es el incidente que ya viviste: la copia con permisos 644
En un fichero de configuración legible Es el estado actual de app.conf, y es lo que se corrige en esta lección

Los dos primeros merecen una demostración, porque la gente los subestima:

# Un secreto en la linea de comandos es publico mientras el proceso vive
$ ps aux | grep -m1 'psql'
luis    3417  0.0  0.1  ... psql -h 10.0.2.15 -U tramontana --password=Zx9K2pQ

# Y el entorno de un proceso es legible por su propietario
$ sudo tr '\0' '\n' < /proc/1284/environ | grep -i pass
TRAMONTANA_DB_PASSWORD=Zx9K2pQ

Sobre el segundo caso conviene ser preciso, porque hay un matiz que se malinterpreta a menudo: /proc/<pid>/environ tiene permisos 0400 y solo lo pueden leer el propietario del proceso y root. Eso hace que las variables de entorno sean mucho mejores que la línea de comandos, pero no las convierte en un buen sitio: se heredan a todo proceso hijo, aparecen en volcados de memoria y en trazas de error de muchos frameworks, y cualquier escalada a root las expone todas de golpe.

El mínimo decente: permisos, EnvironmentFile y LoadCredential

No hace falta infraestructura para mejorar sustancialmente. Con lo que ya sabes de permisos (02-07), usuarios de servicio (05-01) y unidades de systemd (05-05), hay tres niveles, cada uno mejor que el anterior.

Nivel 1: fichero de secretos con permisos estrictos

La idea es separar el secreto de la configuración: app.conf deja de contener la contraseña y pasa a ser un fichero legible por el grupo, mientras el secreto vive en un fichero aparte con permisos mínimos.

# El fichero de secretos: propiedad del usuario del servicio, solo el puede leerlo
$ sudo install -o svc-tramontana -g svc-tramontana -m 600 /dev/null \
       /etc/tramontana/secretos.env
$ sudo tee /etc/tramontana/secretos.env >/dev/null <<'EOF'
TRAMONTANA_DB_PASSWORD=Zx9K2pQ7vLm4RtWn
EOF
$ sudo ls -l /etc/tramontana/
total 8
-rw-r----- 1 root            tramontana        312 ago 18 12:04 app.conf
-rw------- 1 svc-tramontana  svc-tramontana     42 ago 18 12:06 secretos.env

Y la unidad lo carga en el entorno del servicio, sin que aparezca nunca en la línea de comandos:

# /etc/systemd/system/tramontana.service.d/override.conf
[Service]
EnvironmentFile=/etc/tramontana/secretos.env
$ sudo systemctl daemon-reload && sudo systemctl restart tramontana.service

Recuerda la convención del curso: app.conf está chattr +i, así que para editarlo hay que quitar el atributo, cambiar, y volver a ponerlo — con su copia previa:

$ sudo cp -p /etc/tramontana/app.conf /etc/tramontana/app.conf.bak-$(date +%F)
$ sudo chattr -i /etc/tramontana/app.conf
$ sudo sed -i.bak-$(date +%F) '/^db_password/d' /etc/tramontana/app.conf
$ sudo chattr +i /etc/tramontana/app.conf
$ sudo diff -u /etc/tramontana/app.conf.bak-$(date +%F) /etc/tramontana/app.conf
--- /etc/tramontana/app.conf.bak-2026-08-18
+++ /etc/tramontana/app.conf
@@ -3,7 +3,6 @@
 db_name=tramontana_reservas
-db_password=Zx9K2pQ7vLm4RtWn
 max_conexiones=200

Esto ya cierra la exposición del último ejercicio de 06-04: un chmod 644 accidental sobre app.conf ya no filtra nada. Pero el secreto sigue en claro en disco y en el entorno del proceso.

Nivel 2: LoadCredential

Ubuntu 24.04 trae la mecánica de credenciales de systemd, que es claramente superior: el secreto se entrega al servicio en un fichero dentro de un tmpfs privado accesible solo por ese servicio, no se hereda a los hijos y no aparece en el entorno.

# /etc/systemd/system/tramontana.service.d/override.conf
[Service]
LoadCredential=db_password:/etc/tramontana/secretos/db_password
$ sudo install -d -o root -g root -m 700 /etc/tramontana/secretos
$ printf '%s' 'Zx9K2pQ7vLm4RtWn' | sudo tee /etc/tramontana/secretos/db_password >/dev/null
$ sudo chmod 600 /etc/tramontana/secretos/db_password

El servicio recibe la ruta en la variable CREDENTIALS_DIRECTORY y lee el fichero:

# Dentro del servicio, el secreto esta en:
#   ${CREDENTIALS_DIRECTORY}/db_password
$ systemd-run --property=LoadCredential=prueba:/etc/tramontana/secretos/db_password \
    --pty bash -c 'ls -l "$CREDENTIALS_DIRECTORY"; cat "$CREDENTIALS_DIRECTORY/prueba"'
total 0
-r--r----- 1 root root 16 ago 18 12:22 prueba
Zx9K2pQ7vLm4RtWn

La diferencia con EnvironmentFile en una tabla, porque decide la elección:

EnvironmentFile= LoadCredential=
Visible en /proc/<pid>/environ Sí No
Se hereda a procesos hijos Sí No
Aparece en systemctl show Sí (el nombre del fichero) Solo la ruta de origen
Funciona con ProtectSystem=strict Sí Sí, y el tmpfs es privado
Compatible con cualquier aplicación Sí, casi todas leen del entorno Requiere que la app lea de un fichero

Nivel 3: systemd-creds, secreto cifrado en disco

El paso que cierra el círculo: systemd-creds cifra el secreto con una clave derivada del sistema (y del TPM si existe), de modo que el fichero en disco ya no contiene el secreto en claro.

$ printf '%s' 'Zx9K2pQ7vLm4RtWn' \
    | sudo systemd-creds encrypt --name=db_password - /etc/tramontana/secretos/db_password.cred
$ sudo cat /etc/tramontana/secretos/db_password.cred | head -c 80
-----BEGIN CREDENTIAL-----
CqiVzUCu5c1TF4TBTdxr4hK+3XPqmn9lBTJmMWk0YjMwNzY2MzQ0ZTk...
# /etc/systemd/system/tramontana.service.d/override.conf
[Service]
LoadCredentialEncrypted=db_password:/etc/tramontana/secretos/db_password.cred
$ sudo systemctl daemon-reload && sudo systemctl restart tramontana.service
$ sudo systemctl status tramontana.service --no-pager | head -5
● tramontana.service - Tramontana Reservas
     Loaded: loaded (/etc/systemd/system/tramontana.service; enabled)
     Active: active (running) since Tue 2026-08-18 12:31:08 CEST; 4s ago
   Main PID: 4102 (tramontana)

# Verificacion: el secreto ya no esta en el entorno del proceso
$ sudo tr '\0' '\n' < /proc/4102/environ | grep -ci pass
0

Ese 0 es el resultado que buscabas. El tercer incidente abierto queda cerrado: la contraseña ya no está en claro en ningún fichero de configuración, no está en el entorno del proceso, no es legible por ningún usuario del sistema, y —consecuencia importante— tampoco viaja en claro dentro de la copia de restic, porque lo que se copia es el fichero cifrado.

Una limitación honesta: la clave de cifrado de systemd-creds está ligada a la máquina (/var/lib/systemd/credential.secret). Eso es exactamente lo que quieres frente a la fuga de un fichero, pero significa que el fichero .cred no se puede restaurar en un servidor reconstruido. El secreto original tiene que estar también en un sitio del que puedas recuperarlo: eso es la sección siguiente, y es lo que debe constar en el runbook de 05-08.

Cifrado en reposo de secretos: GPG y pass

Necesitas un lugar donde el secreto original viva cifrado, recuperable por una persona autorizada, con historial de cambios y fuera del servidor. La respuesta clásica en Linux es GPG, y pass construida sobre ella.

GPG en dos modos

# Simetrico: una frase de paso, sin claves. Simple y suficiente para un fichero puntual
$ gpg -c --cipher-algo AES256 runbook-credenciales.txt
$ ls runbook-credenciales.txt.gpg
$ gpg -d runbook-credenciales.txt.gpg > /dev/shm/recuperado.txt

# Asimetrico: cifrado para destinatarios concretos, sin compartir ninguna frase de paso
$ gpg --quick-generate-key "operador (Tramontana) <[email protected]>" ed25519 cert 2y
$ gpg -e -r [email protected] -r [email protected] secretos.txt
$ gpg -d secretos.txt.gpg

La diferencia práctica es de gestión: el modo simétrico obliga a transmitir la frase de paso por otro canal y a cambiarla cuando alguien deja el equipo; el asimétrico permite cifrar para varias personas y revocar el acceso de una sin volver a cifrar para las demás. En un equipo, siempre asimétrico.

Fíjate en el > /dev/shm/recuperado.txt del descifrado: /dev/shm es memoria, no disco. Escribir un secreto descifrado en el sistema de ficheros deja restos recuperables incluso después de borrarlo.

pass: el gestor construido sobre GPG

pass guarda cada secreto en un fichero cifrado con GPG dentro de un repositorio git. Es simple, auditable y no depende de ningún servicio.

$ sudo apt install pass
$ pass init [email protected]
mkdir: created directory '/home/operador/.password-store/'
Password store initialized for [email protected]

$ pass git init
$ pass insert tramontana/produccion/db_password
Enter password for tramontana/produccion/db_password:
Retype password for tramontana/produccion/db_password:
[master 8f2c1a4] Add given password for tramontana/produccion/db_password to store.

$ pass insert -m tramontana/produccion/restic
# (-m permite varias lineas: contrasena, repositorio, notas)

$ pass ls
Password Store
└── tramontana
    └── produccion
        ├── db_password
        └── restic

$ pass tramontana/produccion/db_password
Zx9K2pQ7vLm4RtWn

$ pass -c tramontana/produccion/db_password
Copied tramontana/produccion/db_password to clipboard. Will clear in 45 seconds.

Tres detalles que lo hacen apto para producción:

  • Historial completo con git. pass git log --oneline muestra cada alta, cambio y baja, con fecha y autor. Es la trazabilidad de la rotación.

  • Los ficheros están cifrados, así que el repositorio se puede sincronizar a un remoto sin exponer nada: pass git remote add origin ... y pass git push. Eso resuelve el «fuera del servidor».

  • Composición con scripts. pass escribe en stdout, así que encaja con lo del Módulo 4:

    # Generar el fichero .cred desde pass, sin que el secreto toque el disco en claro
    $ pass tramontana/produccion/db_password \\
        | sudo systemd-creds encrypt --name=db_password - \\
               /etc/tramontana/secretos/db_password.cred
    

    Esa tubería es la respuesta al problema de recuperabilidad del apartado anterior: pass es la fuente de verdad, systemd-creds la copia operativa ligada a la máquina, y reconstruir el servidor consiste en volver a ejecutar esta línea.

Y la advertencia obvia: la clave GPG privada y su frase de paso son ahora la llave maestra. Su copia de seguridad (gpg --export-secret-keys, cifrada, en un medio físico separado) y su custodia son parte del runbook, y su pérdida equivale a perder todos los secretos.

Cifrado de disco con LUKS

Los secretos ya están cifrados, pero /srv/tramontana/backups contiene reservas.csv con nombres de huéspedes. Si alguien se lleva el disco —o la imagen de la VM, o el disco de un servidor retirado sin borrar—, los permisos del sistema de ficheros no protegen nada: se montan en otra máquina y se lee todo.

LUKS es el estándar de cifrado de bloque en Linux. Cifra el dispositivo completo, por debajo del sistema de ficheros.

# Sobre un dispositivo NUEVO o cuyo contenido puedas perder: luksFormat DESTRUYE los datos
$ sudo cryptsetup luksFormat --type luks2 /dev/vg-datos/lv-backups
WARNING!
========
This will overwrite data on /dev/vg-datos/lv-backups irrevocably.
Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for /dev/vg-datos/lv-backups:

$ sudo cryptsetup luksOpen /dev/vg-datos/lv-backups backups-cifrado
$ sudo mkfs.ext4 -L backups /dev/mapper/backups-cifrado
$ sudo mount /dev/mapper/backups-cifrado /srv/tramontana/backups

$ sudo cryptsetup luksDump /dev/vg-datos/lv-backups | head -8
LUKS header information
Version:        2
Epoch:          3
Metadata area:  16384 [bytes]
Keyslots:
  0: luks2
        Key:        512 bits
        Cipher:     aes-xts-plain64

Y aquí está el problema real, que hay que plantear sin adornos: un volumen LUKS necesita una frase de paso para abrirse, y un servidor tiene que arrancar solo a las 3 de la madrugada. Las opciones, con su compromiso:

Opción Cómo Coste
Frase de paso manual Alguien la teclea en cada arranque Protección máxima; el servidor no arranca sin intervención
Fichero de clave en el disco raíz /etc/crypttab con keyfile Automático; protege solo contra el robo del disco de datos, no del raíz
TPM systemd-cryptenrol --tpm2-device=auto Automático y ligado al hardware; requiere TPM y una configuración cuidadosa
Desbloqueo remoto en el arranque dropbear-initramfs Automático con supervisión; más piezas que mantener

Para el laboratorio, la opción del fichero de clave con nofail es razonable, y es importante entender exactamente qué protege:

# /etc/crypttab
# nombre         dispositivo (por UUID)                          clave                  opciones
backups-cifrado  UUID=8f4c2a19-7d3e-4b1a-9c85-2e6f0a4d7b31      /etc/luks/backups.key  luks,nofail
$ sudo install -d -m 700 /etc/luks
$ sudo dd if=/dev/urandom of=/etc/luks/backups.key bs=512 count=1
$ sudo chmod 400 /etc/luks/backups.key
$ sudo cryptsetup luksAddKey /dev/vg-datos/lv-backups /etc/luks/backups.key
# /etc/fstab (la linea de backups pasa a apuntar al dispositivo desbloqueado)
/dev/mapper/backups-cifrado  /srv/tramontana/backups  ext4  defaults,noatime,nodev,nosuid,nofail  0  2
# La red de seguridad obligatoria antes de reiniciar, como en 05-04
$ sudo systemctl daemon-reload && sudo mount -a && findmnt /srv/tramontana/backups

Qué protege y qué no protege, dicho con precisión: protege contra el robo o la retirada del disco de datos, contra el acceso a la imagen de la VM parada, y contra la recuperación de datos de un disco desechado. No protege contra un atacante que compromete el servidor en marcha, porque para él el volumen está montado y legible como para cualquiera. Es cifrado en reposo, no cifrado en uso, y presentarlo como otra cosa ante Marta sería engañarla.

Fíjate además en la composición con lo anterior: restic ya cifra su repositorio con su propia contraseña —que ahora vive en pass—, así que las copias tienen dos capas independientes. Y para el destino externo de la regla 3-2-1, el cifrado de restic es el que de verdad importa, porque el medio remoto no lo controlas tú.

Gestores centralizados de secretos

Todo lo anterior es correcto para una máquina. Cuando hay varias, aparece un problema nuevo: distribuir y rotar secretos en N servidores sin copiarlos a mano.

Solución Modelo Cuándo compensa
pass + git Ficheros GPG en un repositorio 1-5 máquinas, un equipo pequeño, sin dependencias
sops + age Cifra solo los valores de un YAML/JSON, versionable en git Configuración como código; encaja muy bien con Ansible (07-06)
HashiCorp Vault Servicio con API, políticas, auditoría y secretos dinámicos de vida corta Decenas de máquinas, varios equipos, requisitos de auditoría
Secrets Manager de la nube (AWS/GCP/Azure) Servicio gestionado integrado con IAM Infraestructura ya en ese proveedor

El salto de rentabilidad tiene un umbral bastante identificable: cuando rotar una credencial deja de ser una tarea de cinco minutos. Con un servidor, rotar es cambiar pass, regenerar el .cred y reiniciar el servicio. Con veinte, sin un gestor central, es una operación manual propensa a dejar máquinas a medias — que es el peor de los mundos, porque tienes el coste de la rotación sin la garantía.

sops merece una mención concreta porque será relevante en 07-06: permite tener en git un fichero de variables de Ansible en el que las claves son legibles y los valores están cifrados, de modo que el diff de un cambio de configuración sigue siendo revisable sin exponer los secretos.

La otra ventaja de Vault que no tienen las demás son los secretos dinámicos: en lugar de una contraseña de base de datos fija, genera credenciales con validez de una hora, específicas de cada proceso. Un secreto que caduca solo no necesita rotación, y una fuga tiene una ventana de explotación mínima. Es el modelo al que tiende la industria.

Rotación de credenciales

Rotar es cambiar un secreto por otro nuevo. Es obligatorio porque la probabilidad de que un secreto esté comprometido solo crece con el tiempo: cada copia, cada registro, cada persona que lo vio, cada máquina donde estuvo. Rotar limita la ventana de explotación de una fuga que quizá ya ocurrió y no sabes.

Cuándo rotar, en orden de urgencia:

  1. Inmediatamente, ante cualquier sospecha de exposición. Es lo que hiciste con db_password cuando apareció en la copia con permisos 644, y lo que haría el desenlace B del último ejercicio de 06-04.
  2. Cuando alguien con acceso deja el equipo. No es desconfianza: es que el control sobre quién conoce el secreto se ha perdido.
  3. Periódicamente, con un plazo escrito. Para una credencial de servicio como esta, entre 90 días y un año es lo habitual, según la exposición.

El procedimiento sin cortar el servicio es la parte con contenido técnico, y depende de una propiedad del sistema de destino: que acepte dos credenciales válidas a la vez. PostgreSQL no lo hace con la misma cuenta, así que el patrón es de dos usuarios:

# 1. Crear la credencial nueva junto a la vieja (ambas validas)
$ sudo -u postgres psql -c \
    "CREATE ROLE tramontana_app2 LOGIN PASSWORD 'nueva-clave-generada';"
$ sudo -u postgres psql -c \
    "GRANT ALL PRIVILEGES ON DATABASE tramontana_reservas TO tramontana_app2;"

# 2. Guardarla en la fuente de verdad, con historial
$ pass insert tramontana/produccion/db_password
$ pass git log --oneline -1
a3f1c88 Add given password for tramontana/produccion/db_password to store.

# 3. Actualizar la copia operativa y aplicar
$ pass tramontana/produccion/db_password \
    | sudo systemd-creds encrypt --name=db_password - \
           /etc/tramontana/secretos/db_password.cred
$ sudo systemctl restart tramontana.service

# 4. VERIFICAR antes de retirar la vieja
$ ~/scripts/revision_salud.sh; echo "estado: $?"
estado: 0

# 5. Solo ahora, retirar la credencial antigua
$ sudo -u postgres psql -c "DROP ROLE tramontana_app;"

El orden es lo importante: crear, aplicar, verificar, retirar. Invertir los dos últimos pasos deja el servicio caído hasta que alguien se dé cuenta, y a las 3 de la madrugada eso son horas. Es la misma disciplina de «medir antes y después» que aplicaste en 05-07.

Una generación de contraseñas decente, para no ceder a la tentación de inventarlas:

$ pass generate -n tramontana/produccion/db_password 32
$ openssl rand -base64 24     # alternativa sin pass
Kj8mQ2vX9pLnR4tW7cYbF3sZ6dHa

Qué garantiza TLS y qué contiene un certificado

Segunda mitad de la lección. TLS (Transport Layer Security, el sucesor de SSL) da tres garantías, y conviene separarlas porque la tercera es la que casi todo el mundo olvida:

Garantía Qué significa Sin ella
Confidencialidad Nadie en el camino puede leer el contenido Quien esté en la red ve los datos de los huéspedes
Integridad Nadie puede modificar el contenido sin que se detecte Se puede alterar una reserva en tránsito
Autenticación Estás hablando con quien crees Cifras perfectamente... con el atacante

La tercera es la razón de que existan los certificados. El cifrado sin autenticación es inútil frente a un intermediario: si un atacante se interpone, negocia una conexión cifrada contigo y otra con el servidor real, lee todo. Es el mismo razonamiento del aviso de huella de SSH en 06-02.

Y lo que TLS no garantiza: que el servidor sea honesto, que la aplicación sea segura, que los datos estén cifrados en reposo, ni que quien se conecta sea quien dice (para eso hace falta autenticar al cliente).

La cadena de confianza

El problema a resolver es de bootstrapping: cómo confiar en la clave pública de un servidor que no habías visto nunca. La solución es delegar en un tercero en el que ya confías.

graph TD
    R["CA raiz<br/>(en el almacen del sistema<br/>y del navegador)"] -->|firma| I["CA intermedia<br/>(p. ej. Let's Encrypt E6)"]
    I -->|firma| H["Certificado hoja<br/>reservas.tramontana.example"]
    H -.->|contiene| K["Clave publica del servidor"]
    S["Clave privada<br/>(nunca sale del servidor)"] -.->|pareja de| K

La verificación consiste en comprobar, hacia arriba, que cada certificado está firmado por el siguiente, hasta llegar a una raíz que ya está en el almacén local (/etc/ssl/certs/, del paquete ca-certificates). La CA raíz firma poco y vive muy protegida; las intermedias hacen el trabajo del día a día y se pueden revocar sin invalidar la raíz.

Qué hay dentro de un certificado

Un certificado es la clave pública del servidor más un conjunto de afirmaciones, todo firmado por la CA:

Campo Contenido Nota
Subject A quién identifica El CN está obsoleto para nombres de host
SAN (Subject Alternative Name) Los nombres válidos Es el campo que se valida hoy
Issuer Quién lo firmó Enlaza con la cadena
Not Before / Not After Ventana de validez La causa del fallo más común
Public Key Clave pública y algoritmo ECDSA P-256 o RSA 2048+
Key Usage / Extended Key Usage Para qué se puede usar serverAuth, clientAuth

El punto sobre el SAN es práctico, no anecdótico: desde hace años los navegadores y las bibliotecas TLS ignoran el CN y validan exclusivamente contra el SAN. Un certificado con CN=reservas.tramontana.example y sin SAN es rechazado. Es una fuente clásica de horas perdidas al generar certificados a mano.

El handshake de TLS 1.3, en cinco líneas y sin entrar en criptografía:

  1. El cliente envía los algoritmos que soporta y su aportación al intercambio de claves.
  2. El servidor elige, envía la suya y su certificado.
  3. El cliente verifica el certificado contra su almacén de CA y comprueba que el nombre está en el SAN.
  4. Ambos derivan la misma clave de sesión sin haberla transmitido nunca.
  5. A partir de ahí, todo va cifrado con esa clave. Un solo viaje de ida y vuelta.

Generar, leer y verificar material criptográfico con OpenSSL

openssl es la navaja suiza del área. Cuatro operaciones cubren el 90 % del trabajo.

Generar una clave privada y una petición de firma

# Clave privada ECDSA P-256: mas corta y rapida que RSA, con seguridad equivalente a RSA 3072
$ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
    -out reservas.key
$ chmod 600 reservas.key

# CSR con el SAN correctamente puesto (la parte que se olvida)
$ openssl req -new -key reservas.key -out reservas.csr \
    -subj "/CN=reservas.tramontana.example/O=Tramontana S.L./C=ES" \
    -addext "subjectAltName=DNS:reservas.tramontana.example,DNS:www.reservas.tramontana.example"

$ openssl req -in reservas.csr -noout -text | grep -A2 'Alternative'
            X509v3 Subject Alternative Name:
                DNS:reservas.tramontana.example, DNS:www.reservas.tramontana.example

La CSR (Certificate Signing Request) contiene la clave pública y los nombres solicitados, y va firmada con la clave privada para demostrar que la posees. La clave privada nunca sale del servidor.

Autofirmado para pruebas

$ openssl x509 -req -in reservas.csr -signkey reservas.key -days 365 \
    -copy_extensions copyext -out reservas-autofirmado.crt

Ese -copy_extensions copyext es imprescindible: sin él, el SAN de la CSR no se copia al certificado y obtienes uno inservible. Es el error más frecuente al generar certificados de prueba a mano.

Un autofirmado sirve para probar la configuración del servidor, y solo para eso: ninguna CA lo respalda, así que todo cliente avisará. Nunca en producción.

Leer y verificar

$ openssl x509 -in /etc/letsencrypt/live/reservas.tramontana.example/cert.pem \
    -noout -text | grep -A1 -E 'Issuer:|Not Before|Not After|Alternative'
        Issuer: C = US, O = Let's Encrypt, CN = E6
            Not Before: Aug 18 09:12:41 2026 GMT
            Not After : Nov 16 09:12:40 2026 GMT
            X509v3 Subject Alternative Name:
                DNS:reservas.tramontana.example

# Verificar la cadena completa contra el almacen del sistema
$ openssl verify -untrusted /etc/letsencrypt/live/reservas.tramontana.example/chain.pem \
    /etc/letsencrypt/live/reservas.tramontana.example/cert.pem
/etc/letsencrypt/live/reservas.tramontana.example/cert.pem: OK

# Comprobar que la clave privada corresponde al certificado (los modulos deben coincidir)
$ openssl pkey -in privkey.pem -pubout -outform DER | sha256sum
$ openssl x509 -in cert.pem -pubkey -noout -outform DER | sha256sum

Esa última comprobación resuelve un fallo desconcertante y frecuente: el servidor no arranca o rechaza las conexiones porque la clave y el certificado no son pareja, típicamente tras regenerar una y olvidar la otra. Si las dos sumas coinciden, son pareja.

Y la inspección de un servidor en marcha:

$ openssl s_client -connect reservas.tramontana.example:443 \
    -servername reservas.tramontana.example </dev/null 2>/dev/null \
    | openssl x509 -noout -dates -subject
notBefore=Aug 18 09:12:41 2026 GMT
notAfter=Nov 16 09:12:40 2026 GMT
subject=CN = reservas.tramontana.example

El -servername es obligatorio cuando hay varios sitios en la misma IP: es la extensión SNI, que indica qué certificado quieres. Sin él recibirás el certificado por defecto y creerás que hay un problema donde no lo hay.

Let's Encrypt y la renovación automática

Let's Encrypt es una CA gratuita y automatizada. Su protocolo, ACME, funciona sobre una idea simple: para demostrar que controlas un dominio, la CA te pide que hagas algo que solo su titular podría hacer.

Desafío Qué exige Cuándo usarlo
HTTP-01 Servir un fichero en http://dominio/.well-known/acme-challenge/<token> Por defecto; necesita el puerto 80 accesible desde Internet
DNS-01 Publicar un registro TXT _acme-challenge.dominio Cuando el 80 no es accesible, y obligatorio para comodines
TLS-ALPN-01 Responder en el 443 con una extensión ALPN Cuando el 80 está cerrado pero el 443 no

El DNS-01 es imprescindible en dos casos: certificados comodín (*.tramontana.example) y servidores que no exponen el puerto 80 — y requiere que el proveedor de DNS tenga API, o hacerlo a mano en cada renovación, que no es viable.

$ sudo apt install certbot python3-certbot-nginx

Y aquí aplica el aviso de alcance del principio: Nginx se monta en 08-01, así que ahora se usa el modo --standalone, en el que certbot levanta su propio servidor temporal en el puerto 80. Recuerda que en 06-03 dejaste el 443 abierto pero el 80 no: hay que abrirlo antes.

$ sudo ufw allow 80/tcp comment 'ACME HTTP-01'
$ sudo certbot certonly --standalone \
    -d reservas.tramontana.example \
    --email [email protected] \
    --agree-tos --no-eff-email

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/reservas.tramontana.example/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/reservas.tramontana.example/privkey.pem
This certificate expires on 2026-11-16.

Los cuatro ficheros que genera, porque cada servidor pide uno distinto:

Fichero Contenido Quién lo usa
privkey.pem Clave privada Siempre; permisos estrictos
cert.pem Solo el certificado hoja Configuraciones antiguas
chain.pem Solo las intermedias Grapado OCSP
fullchain.pem Hoja + intermedias Lo habitual: Nginx, Apache moderno

Servir cert.pem en lugar de fullchain.pem es un error clásico: funciona en el navegador de escritorio, que suele tener la intermedia en caché, y falla en clientes móviles y en curl. Se detecta con openssl s_client mirando si la cadena está completa.

La renovación, y por qué probarla

Los certificados de Let's Encrypt duran 90 días. Eso obliga a automatizar, que es intencionado: una renovación automatizada y probada falla menos que una anual hecha a mano.

$ systemctl list-timers certbot.timer --no-pager
NEXT                        LEFT      LAST  PASSED  UNIT           ACTIVATES
Tue 2026-08-18 22:47:19 CEST 10h left  -     -       certbot.timer  certbot.service

# La prueba, que hace TODO el proceso real contra el entorno de pruebas de la CA
$ sudo certbot renew --dry-run
Simulating renewal of an existing certificate for reservas.tramontana.example
Congratulations, all simulated renewals succeeded.

--dry-run es la línea más importante de esta sección. La renovación falla, típicamente, por el puerto 80 cerrado por una regla nueva del cortafuegos, por un servicio ocupando ese puerto, o por un --deploy-hook roto. Y falla en silencio, 60 días después de que introdujeras el cambio, cuando ya nadie recuerda haberlo hecho. Probar en seco después de cada cambio de red o de firewall es la disciplina que evita ese escenario.

El gancho de despliegue es lo que recarga el servicio tras renovar:

$ sudo tee /etc/letsencrypt/renewal-hooks/deploy/10-recargar >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
systemctl is-active --quiet nginx && systemctl reload nginx
logger -t certbot -p local0.notice "certificado renovado y servicio recargado"
EOF
$ sudo chmod 700 /etc/letsencrypt/renewal-hooks/deploy/10-recargar

Un certificado renovado que el servicio no ha recargado no sirve: el proceso sigue presentando el viejo hasta que se reinicie. Es otro fallo silencioso clásico.

Sobre mTLS (autenticación mutua), brevemente: el servidor exige también un certificado de cliente. Es excelente para comunicación entre servicios —mucho mejor que una clave de API— y desaconsejable para personas, por el coste de distribuir y renovar certificados en cada dispositivo.

Buenas prácticas de TLS, custodia de la clave y vigilancia de la caducidad

Configuración

Los criterios vigentes, que se aplicarán en 08-01 al configurar Nginx:

  • Solo TLS 1.2 y TLS 1.3. SSL 3.0, TLS 1.0 y 1.1 están retirados por vulnerabilidades conocidas y ningún cliente actual los necesita.
  • Suites con confidencialidad persistente (ECDHE), para que el compromiso futuro de la clave privada no permita descifrar tráfico capturado en el pasado.
  • HSTS, la cabecera Strict-Transport-Security, que le dice al navegador que use HTTPS siempre para este dominio. Cuidado: es difícil de revertir, así que primero con max-age corto.
  • No inventes la lista de suites. Usa el generador de ssl-config.mozilla.org y su perfil intermediate, que es el equilibrio razonable entre seguridad y compatibilidad, y revísalo periódicamente.

Custodia de la clave privada

$ sudo ls -l /etc/letsencrypt/live/reservas.tramontana.example/privkey.pem
-rw-r----- 1 root ssl-cert 241 ago 18 11:12 privkey.pem

640 root:ssl-cert es el patrón: root escribe, el grupo ssl-cert lee, nadie más. El proceso que necesita la clave se añade a ese grupo — nunca se hace la clave legible por todos.

Si la clave privada se filtra, no basta con generar una nueva: hay que revocar la anterior, porque sigue siendo válida hasta su caducidad y permitiría a quien la tenga suplantar tu servicio:

$ sudo certbot revoke --cert-path /etc/letsencrypt/live/reservas.tramontana.example/cert.pem \
    --reason keyCompromise
$ sudo certbot certonly --standalone -d reservas.tramontana.example --force-renewal

Con honestidad sobre su eficacia: la revocación depende de que el cliente la compruebe, y muchos no lo hacen o fallan de forma permisiva. Es necesaria pero no suficiente; el plan de respuesta debe asumir que la clave filtrada sigue siendo utilizable un tiempo.

Vigilar la caducidad

El fallo más tonto y más frecuente de producción es un certificado caducado. Ocurre incluso con renovación automática, porque la automatización también se rompe. La medida es un control independiente que no confíe en el mecanismo de renovación, en la línea de comprobar_copia.sh de 05-08:

$ cat ~/scripts/revisar_certificado.sh
#!/usr/bin/env bash
# revisar_certificado.sh - Avisa si un certificado TLS caduca pronto.
# Uso: revisar_certificado.sh [-d dias] [-H host] [-p puerto]
# Salida: 0 correcto | 1 avisa (caduca pronto) | 2 critico (caducado o inalcanzable)
set -euo pipefail

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comunes.sh
source "${SCRIPT_DIR}/lib/comunes.sh"

readonly ETIQUETA_LOG="revisar-certificado"

main() {
    local dias_aviso="${TRAMONTANA_CERT_DIAS:-20}"
    local host="${TRAMONTANA_CERT_HOST:-reservas.tramontana.example}"
    local puerto="${TRAMONTANA_CERT_PUERTO:-443}"

    while getopts ":d:H:p:h" opcion; do
        case "$opcion" in
            d) dias_aviso="$OPTARG" ;;
            H) host="$OPTARG" ;;
            p) puerto="$OPTARG" ;;
            h) sed -n '2,4s/^# \?//p' "$0"; return 0 ;;
            *) morir 64 "opcion no valida: -$OPTARG" ;;
        esac
    done

    requiere_comando openssl
    es_numero "$dias_aviso" || morir 64 "los dias deben ser un numero: $dias_aviso"

    local fin_texto
    if ! fin_texto="$(timeout 10 openssl s_client -connect "${host}:${puerto}" \
            -servername "$host" </dev/null 2>/dev/null \
            | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)"; then
        error "no se pudo obtener el certificado de ${host}:${puerto}"
        return 2
    fi
    [[ -n "$fin_texto" ]] || { error "respuesta vacia de ${host}:${puerto}"; return 2; }

    local fin_epoch ahora_epoch dias_restantes
    fin_epoch="$(date -d "$fin_texto" +%s)"
    ahora_epoch="$(date +%s)"
    dias_restantes=$(( (fin_epoch - ahora_epoch) / 86400 ))

    if (( dias_restantes < 0 )); then
        error "CADUCADO hace $(( -dias_restantes )) dias: $host"
        return 2
    fi
    if (( dias_restantes <= dias_aviso )); then
        error "caduca en ${dias_restantes} dias (umbral ${dias_aviso}): $host"
        return 1
    fi

    log "certificado de $host correcto, caduca en ${dias_restantes} dias"
    return 0
}

main "$@"
$ chmod +x ~/scripts/revisar_certificado.sh
$ shellcheck ~/scripts/revisar_certificado.sh && echo "sin avisos"
sin avisos
$ ~/scripts/revisar_certificado.sh; echo "estado: $?"
certificado de reservas.tramontana.example correcto, caduca en 90 dias
estado: 0

Los 20 días de umbral no son arbitrarios: con certificados de 90 días, certbot renueva a los 60, así que quedan 30 días de margen. Un aviso a los 20 significa que la renovación automática ha fallado dos veces y aún hay casi tres semanas para arreglarlo con calma.

Y como el script devuelve 0/1/2 igual que revision_salud.sh, se integra sin fricción con la comprobación general y con la monitorización.

Errores Comunes y Consejos

  • Generar un certificado sin SAN. El CN ya no se valida. Sin -addext "subjectAltName=..." en la CSR y sin -copy_extensions copyext al autofirmar, obtienes un certificado que todo cliente rechaza.
  • Servir cert.pem en lugar de fullchain.pem. Funciona en tu navegador —tiene la intermedia en caché— y falla en móviles y en curl. Verifica siempre con openssl s_client desde otra máquina.
  • No probar la renovación en seco. certbot renew --dry-run después de cada cambio de cortafuegos o de servidor web. Si no, te enteras 60 días después, cuando el certificado caduca de verdad.
  • Olvidar el gancho de despliegue. Un certificado renovado que el servicio no ha recargado no sirve para nada: el proceso sigue presentando el viejo.
  • Pasar un secreto por la línea de comandos. --password=X es visible en ps para todo el sistema. Usa un fichero, la entrada estándar o el mecanismo de credenciales del programa.
  • Descifrar un secreto a un fichero en disco. Deja restos recuperables. Usa /dev/shm, una tubería, o pass -c con portapapeles temporal.
  • Creer que LUKS protege un servidor en marcha. Protege el disco robado o desechado, no el sistema comprometido, donde el volumen ya está montado. Decirlo mal en un informe es peor que no ponerlo.
  • Rotar retirando la credencial antigua antes de verificar la nueva. Crear, aplicar, verificar, retirar. Ese orden, siempre.
  • Perder la clave GPG de pass sin copia. Es la llave maestra de todos tus secretos. Su copia cifrada y su custodia son parte del runbook, no un detalle.
  • Consejo de método. Anota en el runbook, para cada secreto: dónde está la fuente de verdad, quién puede recuperarlo, cuándo se rotó por última vez y cada cuánto toca. Un inventario de secretos sin fechas es un inventario que nadie mantiene.

Ejercicios

Ejercicio 1

restic necesita su contraseña de repositorio para ejecutar la copia nocturna sin intervención humana. Hoy está en un fichero 600 propiedad de root leído por respaldo_tramontana.sh. Rediseña la solución usando pass como fuente de verdad y el mecanismo de credenciales de systemd, y explica el problema de recuperabilidad que aparece y cómo lo resuelves.

Ejercicio 2

Recibes este aviso y tienes que diagnosticarlo:

$ ~/scripts/revisar_certificado.sh
[2026-11-10 08:00:03] ERROR revisar-certificado: caduca en 6 dias (umbral 20): reservas.tramontana.example

certbot.timer está activo y certbot renew no ha dado ningún error visible en el journal. Enumera las causas posibles en orden de probabilidad, con el comando que confirma o descarta cada una.

Ejercicio 3

Marta te pide por escrito «que el cifrado proteja los datos de los huéspedes». Redacta el informe con las tres capas de cifrado que has montado —secretos con systemd-creds y pass, disco con LUKS, tránsito con TLS—, indicando para cada una qué protege y qué no protege, y qué riesgo sobre los datos de huéspedes sigue abierto pese a las tres.

Soluciones

Solución 1

# 1. La fuente de verdad, con historial y sincronizable fuera del servidor
$ pass insert -m tramontana/produccion/restic
Enter contents of tramontana/produccion/restic and press Ctrl+D when finished:

<contrasena-del-repositorio>
repositorio: /srv/tramontana/backups/restic
rotada: 2026-08-18

# 2. La copia operativa, cifrada y ligada a la maquina
$ pass tramontana/produccion/restic | head -1 \
    | sudo systemd-creds encrypt --name=restic_pass - \
           /etc/tramontana/secretos/restic_pass.cred
$ sudo chmod 600 /etc/tramontana/secretos/restic_pass.cred
# /etc/systemd/system/tramontana-respaldo.service.d/override.conf
[Service]
LoadCredentialEncrypted=restic_pass:/etc/tramontana/secretos/restic_pass.cred

Y en el script, restic lee la contraseña de un fichero, que es justo lo que la credencial le entrega:

# En respaldo_tramontana.sh
readonly RESTIC_PASSWORD_FILE="${CREDENTIALS_DIRECTORY:?este script se ejecuta via systemd}/restic_pass"
export RESTIC_PASSWORD_FILE
restic -r "$TRAMONTANA_REPO" backup /home/operador/datos /etc/tramontana

Tres decisiones que conviene justificar:

  • RESTIC_PASSWORD_FILE en lugar de RESTIC_PASSWORD: la contraseña no pasa por el entorno, solo la ruta al fichero.
  • ${CREDENTIALS_DIRECTORY:?...} con mensaje: si el script se ejecuta a mano en lugar de por systemd, falla de inmediato y con un mensaje claro, en vez de intentar copiar sin credencial. Es la expansión de 04-02 aplicada a algo real.
  • La credencial la monta systemd en un tmpfs privado del servicio: ni luis, ni becario, ni el propio svc-tramontana desde otro proceso pueden leerla.

El problema de recuperabilidad, que es el fondo del ejercicio: el fichero .cred está cifrado con una clave derivada de esta máquina (/var/lib/systemd/credential.secret). Si el servidor se pierde —el escenario (c) de restauración de 05-08, o una reinstalación tras compromiso—, ese fichero es indescifrable. Y sin la contraseña de restic no hay forma de leer las copias: tendrías las copias y no podrías restaurarlas, que es el peor de los fracasos posibles.

Se resuelve en dos frentes:

  1. pass es la fuente de verdad, y vive fuera. El repositorio de pass se sincroniza a un remoto (pass git push), y la clave GPG privada tiene copia cifrada en un medio físico separado. Reconstruir el servidor es: instalar, restaurar pass, y volver a ejecutar la tubería del paso 2.
  2. El runbook lo documenta explícitamente, con esa tubería escrita literalmente y la ubicación de la clave GPG. El runbook ya está fuera del servidor por la disciplina de 05-08.

La regla general que se extrae: un secreto ligado a la máquina nunca puede ser la única copia. La credencial de systemd es una caché operativa; la fuente de verdad está en otro sitio y es recuperable por una persona autorizada.

Solución 2

Que certbot renew no diera error es la pista principal: hay que distinguir «renovó y algo posterior falló» de «no renovó y no se enteró nadie». La primera comprobación separa las dos ramas:

$ sudo openssl x509 -in /etc/letsencrypt/live/reservas.tramontana.example/cert.pem \
    -noout -enddate

Rama A — el certificado en disco es nuevo (caduca en ~90 días). La renovación funcionó; el problema está después.

  1. El servicio no se recargó (lo más probable). El proceso sigue en memoria con el certificado viejo:
    $ ls -l /etc/letsencrypt/renewal-hooks/deploy/
    $ sudo journalctl -u certbot --since "40 days ago" | grep -iE 'hook|deploy|reload'
    $ systemctl show nginx -p ActiveEnterTimestamp
    
    Si la marca de arranque del servicio es anterior a la renovación, es esto. Se arregla recargando, y se corrige de raíz revisando el gancho de despliegue.
  2. El servidor apunta al fichero equivocado: a un cert.pem copiado a otra ruta en su día, en lugar de al enlace de live/ que certbot actualiza.
    $ grep -rE 'ssl_certificate' /etc/nginx/
    
  3. Estás mirando otro certificado: sin -servername, openssl s_client devuelve el sitio por defecto. El propio script lo pasa, pero compruébalo si has probado a mano.

Rama B — el certificado en disco también caduca en 6 días. No se ha renovado nada. certbot renew no falla ruidosamente cuando el desafío no se puede completar en una ejecución no interactiva, así que hay que provocar el error:

$ sudo certbot renew --dry-run     # el comando que revela la causa real

Causas en orden de probabilidad:

  1. El puerto 80 está cerrado. Es la causa número uno, porque cualquier revisión del cortafuegos se lleva por delante esa regla «que no parecía necesaria»:
    $ sudo ufw status verbose | grep -E '^80|http'
    $ sudo nft list ruleset | grep -E 'dport 80|dport { .*80'
    
  2. Otro proceso ocupa el 80 y --standalone no puede levantar su servidor:
    $ sudo ss -tulpn | grep ':80 '
    
  3. El timer no se ejecuta de verdad, aunque esté active:
    $ systemctl list-timers certbot.timer --no-pager
    $ sudo journalctl -u certbot.service --since "70 days ago" | tail -20
    
    Un LAST vacío con el timer activo significa que nunca ha disparado.
  4. DNS: el nombre ya no resuelve a esta máquina, así que el desafío HTTP-01 llega a otro sitio:
    $ dig +short reservas.tramontana.example
    $ getent hosts reservas.tramontana.example
    
  5. Límite de peticiones de la CA, si hubo muchos intentos fallidos. La salida de --dry-run lo dice, y se distingue porque usa el entorno de pruebas y no consume cuota.

La lección de método: el aviso llegó con 6 días de margen porque el umbral era 20 y el control es independiente del mecanismo de renovación. Si la vigilancia hubiera consistido en confiar en certbot, te habrías enterado con el servicio caído. Por eso los controles de verificación no deben compartir mecanismo con lo que verifican.

Solución 3

Informe para Marta Vidal — Cifrado de los datos de huéspedes en srv-tramontana 18 de agosto de 2026

Se han implantado tres capas de cifrado. Cada una cubre un riesgo distinto y ninguna sustituye a las otras. Detallo qué protege y qué no protege cada una, y el riesgo que sigue abierto.

Capa 1 — Secretos (systemd-creds + pass) La contraseña de la base de datos y la del repositorio de copias ya no están en claro en ningún fichero de configuración ni en el entorno de ningún proceso. Están cifradas en disco y systemd las entrega al servicio en un espacio privado en memoria. La fuente de verdad está en un gestor con historial, fuera del servidor. Protege: la fuga de credenciales por un error de permisos, por una copia mal hecha, por un registro o por la lectura del entorno de un proceso. Cierra el incidente abierto desde la copia con permisos 644. No protege: contra un atacante con privilegios de root en la máquina en marcha, que puede leer la credencial del mismo modo que el servicio.

Capa 2 — Disco de copias (LUKS) El volumen /srv/tramontana/backups está cifrado a nivel de bloque. Además, el repositorio de restic tiene su propio cifrado, así que las copias tienen dos capas independientes. Protege: el robo o la pérdida física del disco, el acceso a la imagen de la máquina apagada, y la recuperación de datos de un disco retirado sin borrado seguro. No protege: contra un atacante que compromete el servidor en marcha. Para él, el volumen está montado y legible. Es cifrado en reposo, no en uso.

Capa 3 — Tránsito (TLS) El material criptográfico de reservas.tramontana.example está emitido por Let's Encrypt, verificado, con renovación automática probada y vigilancia independiente de la caducidad con aviso a 20 días. Protege: la lectura y la modificación de los datos de reserva en su camino por la red, y la suplantación del servicio. No protege: los datos una vez llegan al servidor, ni la seguridad de la propia aplicación.

Riesgo abierto, y es importante que conste Los datos de huéspedes viajan hoy sin cifrar: la aplicación escucha en HTTP en el puerto 8080. El certificado está listo y verificado, pero el componente que lo presentará —el proxy inverso delante de la aplicación— está previsto en la siguiente fase del proyecto. Hasta que se despliegue, el cifrado en tránsito no está en vigor, aunque el material esté preparado. Mitigación provisional: el 8080 no es accesible desde Internet, así que la exposición se limita a la red interna.

Riesgo residual común a las tres capas Ninguna capa protege contra el compromiso del servidor en marcha con privilegios de root. Contra ese escenario actúan los controles de la fase anterior: detección de cambios con AIDE, auditoría de accesos con auditd, y el procedimiento de respuesta a incidentes con reconstrucción.

Cumplimiento. El cifrado de datos personales en tránsito y en reposo es una obligación del RGPD (art. 32), no una mejora opcional, y el riesgo abierto del punto anterior es relevante a esos efectos. Recomiendo que el diseño de la custodia de claves y la decisión sobre el plazo de despliegue del proxy inverso los revise el responsable de protección de datos.

Conclusión

Las dos deudas criptográficas del curso están saldadas, cada una a su manera. La db_password ya no existe como texto en ningún fichero de configuración: vive cifrada en un .cred que solo systemd puede descifrar y que solo el servicio puede leer, con pass como fuente de verdad versionada y sincronizada fuera del servidor. El volumen de copias está cifrado con LUKS, sabiendo con precisión qué protege eso y qué no. Y el material de reservas.tramontana.example está emitido, verificado con openssl, con renovación automática probada en seco y con un control de caducidad independiente que avisa con veinte días de margen. El tercer incidente abierto queda cerrado, y sabes por qué LoadCredentialEncrypted es mejor que EnvironmentFile, por qué el SAN manda sobre el CN, y por qué el orden de una rotación es crear, aplicar, verificar y retirar.

Queda uno. El 18 de agosto a las 06:12, unattended-upgrades aplicó la corrección de un CVE de TLS sobre openssl y libssl3t64 — la biblioteca que sostiene todo lo que acabas de montar en la segunda mitad de esta lección — y dejó servicios en ejecución que siguen usando la versión antigua cargada en memoria. Un parche instalado que nadie ha aplicado de verdad es una vulnerabilidad con papeleo. En la lección 06-06: Asegurando Sistemas Linux se cierra ese incidente con needrestart y se hace algo más ambicioso: reunir todo lo que has aprendido en un modelo de amenazas explícito y una postura de seguridad coherente. Reducirás la superficie de ataque, endurecerás la autenticación con PAM —la deuda que dejaste pendiente en 05-01—, escribirás un perfil de AppArmor para la aplicación, aplicarás los sysctl de endurecimiento del kernel y los montajes con noexec, subirás la puntuación de systemd-analyze security con criterio, y cerrarás con una checklist de endurecimiento de srv-tramontana que diga con honestidad qué está hecho, qué se ha decidido asumir y qué excede el alcance de un administrador.

Curso de Linux: De Principiante a Administrador de Sistemas

Módulo 1: Introducción a Linux

Módulo 2: Comandos Básicos de Linux

Módulo 3: Habilidades Avanzadas en la Línea de Comandos

Módulo 4: Scripting en Shell

Módulo 5: Administración del Sistema

Módulo 6: Redes y Seguridad

Módulo 7: Temas Avanzados

Módulo 8: Proyectos Prácticos

© Copyright 2026. Todos los derechos reservados