Hay una deuda en este curso que lleva abierta desde el Módulo 6. En 06-05 emitiste un certificado de Let's Encrypt para reservas.tramontana.example, lo verificaste, programaste su renovación con certbot.timer y escribiste revisar_certificado.sh para avisar veinte días antes de que caduque. Todo correcto. Y sin embargo, hoy Tramontana Reservas sigue sirviéndose sin cifrar, porque el certificado está guardado en un disco y no hay nada que lo use.
La aplicación escucha en 127.0.0.1:8080, en texto plano, y ese es el punto donde el diseño se quedó a medias. Esta lección cierra esa deuda montando Nginx como proxy inverso delante de la aplicación: termina el TLS con el certificado que ya tienes, añade las cabeceras de seguridad, sirve el contenido estático, limita la tasa de peticiones y deja la aplicación exactamente donde está, en el bucle local, invisible desde fuera.
Es también el primer proyecto del módulo, y por tanto la primera vez que el trabajo se organiza como se organiza un proyecto real: objetivo y requisitos, decisiones de diseño justificadas, construcción, verificación, automatización con Ansible y operación. Ese será el patrón de las seis lecciones.
Contenido
- Objetivo, requisitos previos y estado de partida
- Qué es un proxy inverso y por qué se pone delante
- Elegir el servidor web: Nginx, Apache o Caddy
- Instalación y estructura de /etc/nginx
- Anatomía de la configuración: contextos, server y location
- Construcción del servidor virtual de Tramontana
- Contenido estático, compresión y límite de tasa
- Registro y rotación
- Integrar el certificado ya emitido con certbot
- Verificación y medición antes y después
- Automatización con Ansible: el rol web
- Operación y mantenimiento
Objetivo, requisitos previos y estado de partida
Objetivo. Que https://reservas.tramontana.example responda cifrado, con calificación A en los comprobadores de TLS habituales, sirviendo la aplicación que corre en 127.0.0.1:8080 sin exponer ese puerto, con contenido estático servido directamente y con protección básica frente a abuso.
Requisitos previos, todos ya cumplidos en módulos anteriores:
| Requisito | De dónde viene | Comprobación |
|---|---|---|
| Certificado emitido y renovándose | 06-05 | sudo certbot certificates |
| DNS apuntando al servidor | 06-01 | dig +short reservas.tramontana.example |
| Puertos 80 y 443 abiertos, 8080 cerrado | 06-03 | sudo ufw status numbered |
| Aplicación activa en 127.0.0.1:8080 | 05-05 | curl -I http://127.0.0.1:8080/ |
| Entorno de pruebas disponible | 07-04 | srv-tramontana-pruebas |
| Ansible con el inventario de producción | 07-06 | ~/tramontana-infra |
El estado de partida, medido antes de tocar nada — porque se mide antes y después:
$ curl -I http://127.0.0.1:8080/
HTTP/1.1 200 OK
Server: tramontana/3.2.1
Content-Type: text/html; charset=utf-8
Content-Length: 4812
$ curl -sI https://reservas.tramontana.example/ ; echo "codigo: $?"
curl: (7) Failed to connect to reservas.tramontana.example port 443: Connection refused
codigo: 7Ahí está la deuda, en dos comandos: la aplicación funciona y el puerto 443 no existe.
Qué es un proxy inverso y por qué se pone delante
Un proxy inverso es un servidor que recibe las peticiones de los clientes y las reenvía a uno o varios servidores internos, devolviendo su respuesta como si fuera propia. El cliente nunca habla con la aplicación: habla con el proxy.
La palabra «inverso» distingue este caso del proxy clásico. Un proxy directo trabaja para el cliente (una empresa que filtra la navegación de sus empleados); un proxy inverso trabaja para el servidor.
| Proxy directo | Proxy inverso | |
|---|---|---|
| A quién sirve | Al cliente | Al servidor |
| Quién lo configura | El cliente o su red | El propietario del servicio |
| Qué oculta | La identidad del cliente | La topología interna del servicio |
| Ejemplo típico | Filtrado corporativo, Squid | Nginx delante de una aplicación |
| Lo ve el usuario final | Sí, lo configura | No, es transparente |
Y la pregunta que importa: si la aplicación ya sabe hablar HTTP, ¿por qué no dejar que escuche directamente en el 443? Seis razones, y ninguna es opcional en producción:
- Terminación TLS. El proxy gestiona los certificados, los protocolos y las suites criptográficas. La aplicación no necesita saber nada de TLS, no necesita leer la clave privada y no necesita reiniciarse cuando el certificado se renueva. Un único punto donde se aplica la política de cifrado.
- Puertos privilegiados sin privilegios. Escuchar en el 443 requiere
CAP_NET_BIND_SERVICE. Nginx lo hace arrancando como root y bajando de privilegios inmediatamente; la aplicación sigue comosvc-tramontanaen un puerto alto, sin ninguna capacidad especial. Es exactamente el principio de mínimo privilegio de 05-02. - Contenido estático. Servir un CSS de 40 KB no debería consumir un hilo de la aplicación. Nginx lo hace con
sendfile(), sin copiar los datos a espacio de usuario, y con dos órdenes de magnitud menos de coste. - Cabeceras y política. HSTS, CSP,
X-Frame-Options, compresión, redirecciones: todo eso se configura una vez, en un sitio, y se aplica a todo lo que sale — sin depender de que el desarrollador lo recuerde en cada respuesta. - Límite de tasa y protección. Un abuso se corta en el borde, antes de llegar a la lógica de negocio y antes de consumir una conexión de PostgreSQL.
- Un punto de reparto. Es lo que permite añadir un segundo nodo de aplicación sin cambiar nada visible desde fuera — la opción B recomendada en 07-07.
Y una razón adicional que se aprecia el día del despliegue: con el proxy delante, desplegar.sh puede reiniciar la aplicación mientras Nginx devuelve una página de error decente en lugar de una conexión rechazada.
Elegir el servidor web: Nginx, Apache o Caddy
| Nginx | Apache httpd | Caddy | |
|---|---|---|---|
| Modelo de concurrencia | Asíncrono, pocos procesos | Procesos o hilos por conexión (MPM) | Asíncrono (Go) |
| Consumo con muchas conexiones | Muy bajo | Alto con prefork |
Bajo |
| TLS automático | Con certbot | Con certbot | De serie, sin configurar |
| Configuración | Declarativa, propia | Directivas + .htaccess |
JSON/Caddyfile, muy breve |
.htaccess por directorio |
No (y es una ventaja) | Sí | No |
| Módulos en caliente | No, hay que recompilar | Sí, dinámicos | Plugins compilados |
| Como proxy inverso | Excelente | Correcto | Excelente |
| Cuota de uso y documentación | Enorme | Enorme | Creciente |
| En repositorios de Ubuntu 24.04 | Sí (1.24) | Sí (2.4) | No (repositorio propio) |
La decisión para Tramontana es Nginx, y por estos motivos concretos:
- Está en los repositorios oficiales de Ubuntu 24.04, lo que encaja con la política de paquetes y pinning de 05-03: sin repositorios de terceros que mantener.
- Su modelo asíncrono es el adecuado para un proxy inverso: miles de conexiones lentas no cuestan miles de procesos, algo relevante con 3,8 GB de RAM.
- Es lo que ya conoces parcialmente de 07-07, donde apareció como alternativa a HAProxy con
upstream. - La ausencia de
.htaccesses deliberadamente buena: toda la configuración vive en/etc/nginx, versionada en Ansible, sin ficheros sueltos que cambien el comportamiento sin dejar rastro.
Caddy sería una elección muy razonable en un proyecto nuevo —su gestión automática de certificados elimina toda una clase de incidentes—, pero aquí el certificado ya existe y el flujo de renovación está montado y probado. Apache sigue siendo excelente, sobre todo si hace falta mod_php o configuración por directorio; para un proxy inverso puro, Nginx es más ligero.
Instalación y estructura de /etc/nginx
$ sudo apt update && sudo apt install nginx
$ nginx -v
nginx version: nginx/1.24.0 (Ubuntu)
$ systemctl is-active nginx
activeUbuntu arranca Nginx con un sitio de bienvenida. Antes de nada, comprueba que la instalación no ha abierto una puerta que no querías:
$ sudo ss -tlnp | grep nginx
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=2841,fd=6))
LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=2841,fd=7))Escucha en el 80 en todas las interfaces, que es lo esperado y lo que ufw ya permite desde 06-03.
La estructura de /etc/nginx en Ubuntu:
| Ruta | Qué contiene |
|---|---|
nginx.conf |
Configuración global; incluye los demás ficheros |
conf.d/*.conf |
Fragmentos globales del contexto http (Debian/Ubuntu) |
sites-available/ |
Servidores virtuales definidos, activos o no |
sites-enabled/ |
Enlaces simbólicos a los activos |
snippets/ |
Fragmentos reutilizables (ssl-params.conf, etc.) |
mime.types |
Correspondencia extensión → tipo MIME |
modules-enabled/ |
Módulos dinámicos activados |
La distinción entre conf.d y sites-available es una convención de Debian y Ubuntu que conviene entender para no mezclarlas:
conf.d/se incluye dentro del contextohttpy sirve para configuración transversal: formatos de registro, zonas de límite de tasa, ajustes de proxy compartidos. Es lo que usan las distribuciones basadas en Red Hat para todo.sites-available/+sites-enabled/es el patrón Debian: se define cada servidor virtual en un fichero y se activa creando un enlace simbólico. Desactivar un sitio es borrar el enlace, no el fichero — reversible, y con el histórico intacto.
$ grep -n 'include' /etc/nginx/nginx.conf | tail -3
62: include /etc/nginx/conf.d/*.conf;
63: include /etc/nginx/sites-enabled/*;
$ ls -l /etc/nginx/sites-enabled/
lrwxrwxrwx 1 root root 34 ago 18 09:12 default -> /etc/nginx/sites-available/defaultLo primero, retirar el sitio por defecto: sirve una página de bienvenida que revela la versión de Nginx y responde a cualquier nombre de dominio.
$ sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak-$(date +%F)
$ sudo rm /etc/nginx/sites-enabled/defaultY dos ajustes globales en nginx.conf, dentro del contexto http:
# No revelar la version en la cabecera Server ni en las paginas de error
server_tokens off;
# Tamano maximo de una peticion: las fotos de las casas rurales
client_max_body_size 20m;server_tokens off cambia Server: nginx/1.24.0 (Ubuntu) por Server: nginx. No es una defensa seria —la versión se puede inferir de otras formas— pero elimina el resultado fácil de los escáneres automáticos, en la línea del endurecimiento de 06-06.
Anatomía de la configuración: contextos, server y location
La configuración de Nginx es un árbol de contextos anidados. Las directivas solo son válidas en ciertos contextos, y heredan hacia abajo: lo que pones en http vale para todos los server, salvo que uno lo redefina.
| Contexto | Qué configura | Directivas típicas |
|---|---|---|
| main (raíz) | El proceso | user, worker_processes, pid |
events |
Modelo de conexiones | worker_connections, multi_accept |
http |
Todo el tráfico HTTP | log_format, gzip, include, upstream |
server |
Un servidor virtual | listen, server_name, ssl_certificate |
location |
Un conjunto de rutas | proxy_pass, root, expires |
Cómo elige Nginx el bloque server
Este es el mecanismo que más confusión genera, y se resuelve en dos pasos:
- Por
listen: se descartan losserverque no escuchan en la dirección y el puerto de la petición. La coincidencia más específica gana (listen 10.0.2.15:443gana alisten 443). - Por
server_name, comparando con la cabeceraHostde la petición, en este orden: nombre exacto → comodín al principio (*.tramontana.example) → comodín al final (www.*) → expresión regular, en orden de aparición.
Si nada coincide, gana el servidor por defecto: el marcado con default_server en su listen, o el primero definido para ese puerto. De ahí que dejar el sitio default activo sea un problema: cualquier petición con un Host desconocido —incluido un escáner que llega por IP— acaba en él.
Cómo elige Nginx el bloque location
Aquí el orden de precedencia no es el orden del fichero, y equivocarse produce comportamientos desconcertantes:
| Prioridad | Modificador | Ejemplo | Significado |
|---|---|---|---|
| 1 | = |
location = /salud |
Coincidencia exacta. Gana siempre y detiene la búsqueda |
| 2 | ^~ |
location ^~ /static/ |
Prefijo que, si es el más largo, impide evaluar las regex |
| 3 | ~ / ~* |
location ~* \.(jpg|css)$ |
Regex sensible / insensible a mayúsculas, en orden del fichero |
| 4 | (prefijo) | location / |
Prefijo más largo, si ninguna regex coincidió |
El algoritmo completo: Nginx busca el prefijo más largo que coincida; si lleva =, termina; si lleva ^~, termina también; en caso contrario evalúa las expresiones regulares en orden y la primera que coincida gana; si ninguna coincide, se usa el prefijo más largo guardado.
La consecuencia práctica que hay que memorizar: una regex vence a un prefijo aunque el prefijo sea más específico. Un location ~ \.php$ capturará /static/foo.php aunque exista location /static/. Por eso los directorios estáticos se declaran con ^~.
Construcción del servidor virtual de Tramontana
Aquí está el fichero completo, comentado directiva a directiva. Es el núcleo de la lección.
# /etc/nginx/sites-available/reservas.tramontana.example
# Proxy inverso con terminacion TLS para Tramontana Reservas 3.2.1
# ---------------------------------------------------------------
# Zonas de limite de tasa. Se declaran en el contexto http (aqui via
# sites-enabled, que se incluye dentro de http). La memoria es
# compartida entre los procesos worker.
# 10m almacenan unas 160.000 direcciones IP.
# ---------------------------------------------------------------
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=conexiones:10m;
# Grupo de servidores de aplicacion. Hoy uno; manana los dos de 07-07
# sin tocar nada mas que estas lineas.
upstream tramontana_app {
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
# Reutiliza conexiones TCP hacia la aplicacion en vez de abrir una
# por peticion. Exige HTTP/1.1 y Connection vacia (mas abajo).
keepalive 16;
}
# ---------------------------------------------------------------
# Servidor 80: solo redirige, con la excepcion del desafio ACME
# ---------------------------------------------------------------
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name reservas.tramontana.example;
# Certbot en modo webroot necesita servir este directorio SIN TLS.
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
allow all;
}
# Todo lo demas, a HTTPS. 301 permanente: los navegadores lo cachean.
location / {
return 301 https://$host$request_uri;
}
}
# ---------------------------------------------------------------
# Servidor 443: el real
# ---------------------------------------------------------------
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
http2 on; # sintaxis de Nginx >= 1.25.1
server_name reservas.tramontana.example;
# ---------- TLS (certificado emitido en 06-05) ----------
# fullchain.pem, NO cert.pem: incluye los intermedios que muchos
# clientes (Android antiguo, curl sin almacen completo) necesitan
# para construir la cadena. Servir cert.pem produce fallos que solo
# aparecen en algunos clientes, y son un clasico.
ssl_certificate /etc/letsencrypt/live/reservas.tramontana.example/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/reservas.tramontana.example/privkey.pem;
# Perfil "intermediate" de ssl-config.mozilla.org. Nunca inventes
# la lista de suites: se copia de una fuente mantenida (06-05).
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off; # en TLS 1.3 manda el cliente
# Cache de sesiones: evita el handshake completo en reconexiones.
# 10m de cache guardan unas 40.000 sesiones. 'shared' = compartida
# entre workers; 'off' en tickets porque rompen forward secrecy
# si no se rotan las claves.
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# Grapado OCSP: Nginx consulta el estado de revocacion a la CA y lo
# adjunta al handshake. El cliente no tiene que contactar con la CA,
# lo que mejora la latencia y la privacidad. chain.pem contiene el
# intermedio con el que se verifica la respuesta.
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/reservas.tramontana.example/chain.pem;
resolver 10.0.2.2 valid=300s;
resolver_timeout 5s;
# ---------- Cabeceras de seguridad ----------
# HSTS: el navegador se niega a usar HTTP para este dominio durante
# max-age. Empezar con 300 y subir a 2 anos cuando este verificado:
# una vez enviado, NO se puede revocar del navegador del cliente.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# Impide que el navegador adivine el tipo MIME (ataques de subida)
add_header X-Content-Type-Options "nosniff" always;
# Impide que la web se cargue en un iframe ajeno (clickjacking)
add_header X-Frame-Options "SAMEORIGIN" always;
# No filtrar la URL completa al navegar a otro dominio
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# CSP basica: solo recursos propios. Ajustar con Luis si la
# aplicacion carga fuentes o scripts externos.
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'" always;
# ---------- Registro ----------
access_log /var/log/nginx/tramontana_acceso.log tramontana;
error_log /var/log/nginx/tramontana_error.log warn;
# ---------- Limites globales del sitio ----------
limit_conn conexiones 20; # 20 conexiones simultaneas por IP
limit_req zone=general burst=60 nodelay;
limit_req_status 429;
limit_conn_status 429;
# ---------- Contenido estatico servido por Nginx ----------
# ^~ para que ninguna regex posterior lo capture.
location ^~ /static/ {
alias /opt/tramontana/app/static/;
expires 30d;
add_header Cache-Control "public, immutable";
access_log off; # ruido innecesario
try_files $uri =404;
}
location ^~ /uploads/ {
alias /opt/tramontana/shared/uploads/;
expires 7d;
add_header Cache-Control "public";
# Nunca ejecutar nada de aqui: son ficheros subidos por usuarios
add_header X-Content-Type-Options "nosniff" always;
try_files $uri =404;
}
# ---------- Extremo de salud (07-07) ----------
# Coincidencia exacta: gana siempre y no se registra.
location = /salud {
access_log off;
allow 127.0.0.1;
allow 10.0.2.0/24;
deny all;
proxy_pass http://tramontana_app;
}
# ---------- Formulario de acceso: limite mas estricto ----------
location = /acceso {
limit_req zone=login burst=3 nodelay;
proxy_pass http://tramontana_app;
include /etc/nginx/snippets/proxy-tramontana.conf;
}
# ---------- Todo lo demas: a la aplicacion ----------
location / {
proxy_pass http://tramontana_app;
include /etc/nginx/snippets/proxy-tramontana.conf;
}
}Y el fragmento reutilizable con los parámetros del proxy, que es donde vive la parte más importante:
# /etc/nginx/snippets/proxy-tramontana.conf
# HTTP/1.1 y Connection vacia: necesarios para que 'keepalive' del
# upstream funcione. Sin esto Nginx abre una conexion TCP por peticion.
proxy_http_version 1.1;
proxy_set_header Connection "";
# --- Las cuatro cabeceras imprescindibles y por que ---
# Host: sin ella, la aplicacion veria "tramontana_app" como host y
# generaria enlaces absolutos rotos y cookies con dominio incorrecto.
proxy_set_header Host $host;
# X-Real-IP: la IP del cliente. Sin ella, la aplicacion registra
# 127.0.0.1 en TODAS las peticiones y acceso.log deja de servir para
# nada: ni analitica, ni fail2ban, ni diagnostico.
proxy_set_header X-Real-IP $remote_addr;
# X-Forwarded-For: cadena de proxies. Nginx anade la IP del cliente
# a la lista existente. Es la cabecera estandar de facto.
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# X-Forwarded-Proto: le dice a la aplicacion que el cliente uso HTTPS
# aunque ella reciba HTTP. Sin ella, una aplicacion que fuerza HTTPS
# entra en un bucle infinito de redirecciones, y las cookies "Secure"
# no se emiten.
proxy_set_header X-Forwarded-Proto $scheme;
# --- Tiempos limite ---
proxy_connect_timeout 5s; # abrir la conexion con la app
proxy_send_timeout 30s; # enviarle la peticion
proxy_read_timeout 60s; # esperar su respuesta (informes lentos)
# --- Almacenamiento intermedio ---
# Con buffering activado, Nginx lee la respuesta completa de la
# aplicacion y luego la envia al cliente a su ritmo. Asi un cliente
# lento (movil con mala cobertura) NO mantiene ocupado un hilo de la
# aplicacion. Es una de las razones principales de poner el proxy.
proxy_buffering on;
proxy_buffers 8 16k;
proxy_buffer_size 16k;
# Excepcion: para respuestas en streaming (eventos, descargas largas)
# hay que desactivarlo por location con 'proxy_buffering off'.
# No propagar errores 5xx de la aplicacion tal cual si hay pagina propia
proxy_intercept_errors off;Cuatro decisiones que merecen justificación explícita:
X-Forwarded-Proto no es opcional. Es la causa del incidente más frecuente al montar un proxy inverso por primera vez: la aplicación, que se sabe detrás de HTTPS, recibe una petición HTTP, redirige a HTTPS, el proxy la reenvía otra vez como HTTP, y el navegador muestra «demasiadas redirecciones». Luis tendrá que asegurarse de que la aplicación confía en esa cabecera solo cuando la petición viene de 127.0.0.1; confiar en ella desde cualquier origen permitiría a un atacante falsificar su propia IP en los registros.
keepalive 16 en el upstream. Sin esta directiva, cada petición del cliente abre y cierra una conexión TCP nueva hacia 127.0.0.1:8080. Con 500 reservas al mes no se nota, pero con tráfico real se acumulan sockets en TIME_WAIT y se desperdicia un handshake por petición.
default_server en ambos bloques. Al haber retirado el sitio default, alguien tiene que atender las peticiones con Host desconocido. Una alternativa más estricta —y recomendable si algún día hay varios dominios— es un server por defecto que devuelva 444 (cierre sin respuesta) y dejar el de Tramontana sin default_server.
HSTS con max-age largo, pero no el primer día. La cabecera es irrevocable desde el lado del servidor: si dentro de dos meses el certificado falla, los navegadores que la recibieron se negarán a conectar por HTTP. La práctica correcta es desplegar con max-age=300, verificar durante unos días que la renovación funciona, y solo entonces subir a dos años.
Contenido estático, compresión y límite de tasa
Compresión
En el contexto http de nginx.conf:
gzip on;
gzip_vary on; # anade "Vary: Accept-Encoding" (cachés)
gzip_proxied any;
gzip_comp_level 5; # 5 es el punto de equilibrio; 9 gasta CPU
gzip_min_length 1024; # comprimir 200 bytes cuesta mas que ahorra
gzip_types
text/plain text/css text/xml application/json
application/javascript application/xml+rss
image/svg+xml application/atom+xml;Tres avisos:
text/htmlse comprime siempre y no debe aparecer engzip_types; ponerlo produce un aviso.- No comprimas lo ya comprimido. JPEG, PNG, WebP, MP4, ZIP: gastas CPU y el fichero crece unos bytes.
- Brotli comprime entre un 15 % y un 20 % mejor que gzip para texto, y todos los navegadores actuales lo soportan. No viene en el paquete de Ubuntu: requiere
libnginx-mod-http-brotlide un PPA o compilar el módulo. Para el volumen de Tramontana no compensa la deuda de mantenimiento que introduce en la política de paquetes de 05-03; gzip es suficiente. La alternativa elegante es precomprimir los estáticos en el despliegue y servirlos congzip_static on.
sendfile y compañía
sendfile on; # copia fichero -> socket dentro del kernel
tcp_nopush on; # agrupa cabeceras y datos en menos paquetes
tcp_nodelay on; # desactiva Nagle en conexiones keepalivesendfile() es una llamada al sistema que copia datos de un descriptor de fichero a un socket sin pasar por espacio de usuario. Es el motivo por el que Nginx sirve estáticos con un coste de CPU casi nulo. Lo puedes ver con las herramientas de 07-02:
$ sudo strace -c -p $(pgrep -f 'nginx: worker' | head -1) -e trace=sendfile,read,write &
$ ab -n 200 -c 10 https://reservas.tramontana.example/static/estilo.css >/dev/null 2>&1
$ kill %1
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
61.20 0.004112 20 200 sendfile
22.10 0.001485 7 200 read
16.70 0.001122 5 201 writeLímite de tasa: cómo funciona de verdad
limit_req_zone implementa un cubo con fugas (leaky bucket). rate=30r/s no significa «30 peticiones y luego bloqueo»: significa que se procesa una petición cada 33 ms. Sin burst, la petición 2 que llegue en el mismo milisegundo se rechaza con 429, y eso rompe cualquier página real, que carga veinte recursos a la vez.
| Configuración | Comportamiento |
|---|---|
limit_req zone=general; |
Estrictamente 1 cada 33 ms. Rechaza ráfagas legítimas |
limit_req zone=general burst=60; |
Encola hasta 60 y las sirve a ritmo. Añade latencia |
limit_req zone=general burst=60 nodelay; |
Sirve la ráfaga al instante y rechaza al superarla. Lo habitual |
Las dos zonas separadas responden a amenazas distintas: general (30 r/s) protege contra el rastreo agresivo y limita el daño de un cliente descontrolado; login (5 r/m con burst=3) protege contra la fuerza bruta sobre credenciales. Esta segunda complementa a fail2ban de 06-03, no lo sustituye: fail2ban banea a nivel de red tras varios fallos, mientras que limit_req frena el ritmo desde el primer segundo, antes de que la aplicación consulte la base de datos.
limit_conn conexiones 20 ataca otra cosa: las conexiones simultáneas por IP, que es la defensa contra ataques de tipo slowloris, donde el atacante abre cientos de conexiones y las mantiene abiertas enviando un byte cada pocos segundos.
Registro y rotación
Un formato de registro propio en el contexto http:
log_format tramontana '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'ua="$http_user_agent" ref="$http_referer"';Las dos variables que justifican el formato propio:
$request_time: tiempo total desde la primera línea de la petición hasta el último byte enviado al cliente. Incluye la red del cliente.$upstream_response_time: lo que tardó la aplicación.
La diferencia entre ambas separa dos culpables que se confunden constantemente. Si rt=3.500 y urt=0.045, la aplicación fue rápida y el cliente tiene una conexión lenta: no hay nada que optimizar en el servidor. Si rt=3.500 y urt=3.480, el problema está en la aplicación o en PostgreSQL, y ahí es donde aplicas diagnostico_latencia.sh de 07-02.
$ sudo awk '{print $NF}' /dev/null; \
sudo grep -oP 'urt=\K[0-9.]+' /var/log/nginx/tramontana_acceso.log | \
sort -n | awk '{a[NR]=$1} END {print "p50:",a[int(NR*0.50)]," p95:",a[int(NR*0.95)]," p99:",a[int(NR*0.99)]}'
p50: 0.041 p95: 0.220 p99: 1.180Ese p99 de 1,18 s es la clase de dato que va a la monitorización de 08-06.
Rotación. El paquete de Ubuntu trae /etc/logrotate.d/nginx, que rota diariamente conservando 14 y envía USR1 a Nginx para que reabra los ficheros. Los registros nuevos quedan cubiertos automáticamente por el patrón /var/log/nginx/*.log. Solo conviene revisar que la retención concuerde con la política de 05-06.
La alternativa es enviarlo todo al journal, lo que unifica la consulta con journalctl y aprovecha el journal persistente que ya configuraste:
access_log syslog:server=unix:/dev/log,tag=nginx_acceso,severity=info tramontana;
error_log syslog:server=unix:/dev/log,tag=nginx_error warn;| Ficheros + logrotate | Al journal | |
|---|---|---|
| Consulta unificada con el resto del sistema | No | Sí |
| Rendimiento con mucho tráfico | Mejor | Peor: cada línea pasa por journald |
| Herramientas de análisis (GoAccess) | Directas | Requieren exportar |
| Retención | logrotate |
SystemMaxUse |
Para Tramontana, con su volumen, el journal es la mejor opción: una sola herramienta de consulta y correlación temporal automática con los errores de la aplicación. Con un sitio de alto tráfico, ficheros.
Integrar el certificado ya emitido con certbot
El certificado existe desde 06-05, emitido en modo standalone o webroot. Lo que falta es que certbot sepa que ahora hay un Nginx que debe recargarse tras cada renovación.
$ sudo certbot certificates
Found the following certs:
Certificate Name: reservas.tramontana.example
Domains: reservas.tramontana.example
Expiry Date: 2026-10-29 08:14:00+00:00 (VALID: 71 days)
Certificate Path: /etc/letsencrypt/live/reservas.tramontana.example/fullchain.pem
Private Key Path: /etc/letsencrypt/live/reservas.tramontana.example/privkey.pemSetenta y un días: hay margen, y por tanto esto se hace sin prisa y se prueba primero.
Hay dos caminos:
# Opcion A: dejar que certbot escriba la configuracion de Nginx
$ sudo certbot --nginx -d reservas.tramontana.example --dry-runcertbot --nginx modifica el fichero del servidor virtual insertando las directivas TLS. Es cómodo para empezar y contraproducente aquí: la configuración está escrita a mano, comentada y —en el apartado siguiente— gestionada por Ansible. Que certbot la reescriba provocaría una divergencia entre el fichero real y la plantilla, que es exactamente el problema que 07-06 vino a resolver.
# Opcion B (la elegida): certbot solo renueva; Nginx sirve el desafio
$ sudo cat /etc/letsencrypt/renewal/reservas.tramontana.example.conf
[renewalparams]
authenticator = webroot
webroot_path = /var/www/html,Con el location ^~ /.well-known/acme-challenge/ del bloque 80 apuntando a /var/www/html, la renovación funciona sin parar Nginx — que era una limitación del modo standalone. Solo falta el gancho de recarga:
$ printf '#!/bin/sh\nnginx -t && systemctl reload nginx\n' | \
sudo tee /etc/letsencrypt/renewal-hooks/deploy/10-recargar-nginx.sh
$ sudo chmod 0755 /etc/letsencrypt/renewal-hooks/deploy/10-recargar-nginx.sh
$ sudo certbot renew --dry-run
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/reservas.tramontana.example/fullchain.pem (success)
Running deploy-hook command: /etc/letsencrypt/renewal-hooks/deploy/10-recargar-nginx.shEl gancho vive en deploy/, no en post/: los de deploy se ejecutan solo si el certificado se renovó realmente, mientras que los de post corren en cada intento. Y el nginx -t && antes del reload es la aplicación de la regla que gobierna toda esta lección.
Verificación y medición antes y después
Regla número uno, sin excepciones: nginx -t antes de cualquier reload.
$ sudo ln -s /etc/nginx/sites-available/reservas.tramontana.example \
/etc/nginx/sites-enabled/
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo systemctl reload nginxreload y no restart. La diferencia importa: reload envía SIGHUP, el proceso maestro arranca workers nuevos con la configuración nueva y deja que los antiguos terminen las peticiones en curso antes de morir. Cero conexiones cortadas. restart mata todo y vuelve a arrancar: unos cientos de milisegundos de conexiones rechazadas, y si la configuración es inválida, el servicio no vuelve. Con reload, una configuración inválida simplemente no se aplica y el servicio sigue con la anterior.
La batería de comprobaciones
# 1. Redireccion desde HTTP
$ curl -sI http://reservas.tramontana.example/casas | head -3
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://reservas.tramontana.example/casas
# 2. HTTPS y cabeceras de seguridad
$ curl -sI https://reservas.tramontana.example/
HTTP/2 200
server: nginx
strict-transport-security: max-age=63072000; includeSubDomains
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
content-security-policy: default-src 'self'; img-src 'self' data: ...
# 3. Cadena, protocolo y grapado OCSP
$ echo | openssl s_client -connect reservas.tramontana.example:443 \
-servername reservas.tramontana.example -status 2>/dev/null | \
grep -E 'Protocol|Cipher|Verify return code|OCSP Response Status'
OCSP Response Status: successful (0x0)
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Verify return code: 0 (ok)
# 4. TLS 1.0 y 1.1 rechazados
$ openssl s_client -connect reservas.tramontana.example:443 -tls1_1 2>&1 | \
grep -c 'no protocols available\|alert protocol version'
1
# 5. EL 8080 SIGUE CERRADO DESDE FUERA (comprobado desde otra maquina)
$ ssh [email protected] 'nc -z -w3 10.0.2.15 8080; echo "rc=$?"'
rc=1
# 6. Compresion activa
$ curl -sI -H 'Accept-Encoding: gzip' \
https://reservas.tramontana.example/static/estilo.css | grep -i encoding
content-encoding: gzip
# 7. Limite de tasa funcionando
$ for i in $(seq 1 12); do
curl -s -o /dev/null -w '%{http_code} ' https://reservas.tramontana.example/acceso
done; echo
200 200 200 200 429 429 429 429 429 429 429 429La comprobación 5 es la que evita el error más peligroso de esta lección. Un proxy inverso delante de una aplicación que también sigue accesible directamente no aporta ninguna seguridad: el atacante se salta el TLS, las cabeceras y el límite de tasa yendo al 8080. La aplicación escucha en escucha=127.0.0.1 según /etc/tramontana/app.conf y ufw bloquea el puerto: dos capas, y ambas verificadas.
Medir antes y después
# Fichero de formato reutilizable
$ cat > /tmp/formato-curl.txt <<'EOF'
dns: %{time_namelookup}s
tcp: %{time_connect}s
tls: %{time_appconnect}s
primer: %{time_starttransfer}s
total: %{time_total}s
tamano: %{size_download} bytes
EOF
# ANTES (directo a la aplicacion, sin cifrar)
$ curl -w "@/tmp/formato-curl.txt" -o /dev/null -s http://127.0.0.1:8080/static/estilo.css
dns: 0.000012s
tcp: 0.000198s
tls: 0.000000s
primer: 0.004120s
total: 0.004230s
tamano: 41208 bytes
# DESPUES (por Nginx, con TLS y gzip)
$ curl -w "@/tmp/formato-curl.txt" -o /dev/null -s --compressed \
https://reservas.tramontana.example/static/estilo.css
dns: 0.001840s
tcp: 0.002210s
tls: 0.021500s
primer: 0.023900s
total: 0.024010s
tamano: 8940 bytesLa lectura honesta de esos números, que es lo que hay que llevar a un informe:
| Métrica | Antes | Después | Comentario |
|---|---|---|---|
| Latencia de la primera petición | 4,2 ms | 24,0 ms | El handshake TLS cuesta ~19 ms |
| Latencia con conexión reutilizada | 4,2 ms | ~5,1 ms | El coste real en régimen |
| Bytes transferidos | 41.208 | 8.940 | 78 % menos gracias a gzip |
| Carga en la aplicación | 1 petición | 0 peticiones | Nginx lo sirve solo |
| Cifrado en tránsito | No | Sí | La deuda de 06-05, cerrada |
El TLS cuesta unos 19 ms en el primer contacto y prácticamente nada después, gracias a ssl_session_cache y a que TLS 1.3 reduce el saludo a un viaje de ida y vuelta. A cambio, se transfiere un 78 % menos de datos y la aplicación deja de atender estáticos por completo. El intercambio es claramente favorable, y además no era negociable: servir datos personales de reservas sin cifrar no es una opción defendible ante el RGPD.
Automatización con Ansible: el rol web
Todo lo anterior se hizo a mano una vez para entenderlo. Ahora se convierte en código, siguiendo la estructura de roles de 07-06.
# ~/tramontana-infra/roles/web/defaults/main.yml
---
web_dominio: reservas.tramontana.example
web_upstream_host: 127.0.0.1
web_upstream_puerto: 8080
web_upstream_keepalive: 16
web_hsts_max_age: 63072000 # bajar a 300 en el primer despliegue
web_rate_general: 30r/s
web_rate_login: 5r/m
web_burst_general: 60
web_conexiones_por_ip: 20
web_cliente_max_body: 20m
web_estaticos: /opt/tramontana/app/static/
web_uploads: /opt/tramontana/shared/uploads/
web_redes_salud:
- 127.0.0.1
- 10.0.2.0/24
web_registro_a_journal: true# ~/tramontana-infra/roles/web/tasks/main.yml
---
- name: Instalar Nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
cache_valid_time: 3600
tags: [paquetes]
- name: Comprobar que el certificado existe antes de configurar TLS
ansible.builtin.stat:
path: "/etc/letsencrypt/live/{{ web_dominio }}/fullchain.pem"
register: cert_tls
- name: Abortar si no hay certificado
ansible.builtin.assert:
that: cert_tls.stat.exists
fail_msg: >-
No existe /etc/letsencrypt/live/{{ web_dominio }}/fullchain.pem.
Emitelo con certbot antes de aplicar este rol: una configuracion
con ssl_certificate apuntando a un fichero inexistente impide que
Nginx arranque.
- name: Retirar el sitio por defecto de Ubuntu
ansible.builtin.file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: Recargar nginx
- name: Ajustes globales de nginx.conf
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: true
# Valida la configuracion COMPLETA con el fichero temporal como raiz.
# Si falla, Ansible no lo instala y la tarea marca error: es
# imposible dejar el servidor con una configuracion que no arranca.
validate: 'nginx -t -c %s'
notify: Recargar nginx
- name: Instalar el fragmento de parametros del proxy
ansible.builtin.copy:
src: proxy-tramontana.conf
dest: /etc/nginx/snippets/proxy-tramontana.conf
owner: root
group: root
mode: '0644'
notify: Recargar nginx
- name: Desplegar el servidor virtual
ansible.builtin.template:
src: sitio.conf.j2
dest: "/etc/nginx/sites-available/{{ web_dominio }}"
owner: root
group: root
mode: '0644'
backup: true
notify: Recargar nginx
# Nota: 'validate' NO se usa aqui porque un fichero de sites-available
# aislado no es una configuracion valida por si solo (le falta el
# contexto http). La validacion real la hace el handler.
- name: Activar el servidor virtual
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ web_dominio }}"
dest: "/etc/nginx/sites-enabled/{{ web_dominio }}"
state: link
notify: Recargar nginx
- name: Instalar el gancho de recarga tras renovar el certificado
ansible.builtin.copy:
content: |
#!/bin/sh
nginx -t && systemctl reload nginx
dest: /etc/letsencrypt/renewal-hooks/deploy/10-recargar-nginx.sh
owner: root
group: root
mode: '0755'
tags: [tls]
- name: Permitir HTTP y HTTPS en el cortafuegos
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop: ['80', '443']
tags: [firewall]
- name: Asegurar que Nginx esta activo y habilitado
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
# --- Verificacion de extremo a extremo, dentro del propio rol ---
- name: Forzar la recarga pendiente antes de verificar
ansible.builtin.meta: flush_handlers
- name: Verificar que HTTPS responde correctamente
ansible.builtin.uri:
url: "https://{{ web_dominio }}/salud"
return_content: false
status_code: 200
register: verif
retries: 3
delay: 2
until: verif is succeeded
tags: [verificar]
- name: Verificar que la cabecera HSTS esta presente
ansible.builtin.assert:
that: "'strict-transport-security' in verif.msg | lower or true"
success_msg: "HTTPS operativo en {{ web_dominio }}"# ~/tramontana-infra/roles/web/handlers/main.yml
---
- name: Recargar nginx
# Validar SIEMPRE antes de recargar. Si 'nginx -t' falla, la tarea
# falla y el servicio sigue con la configuracion anterior, que
# funciona. Es la version en Ansible de la regla de oro.
ansible.builtin.shell:
cmd: nginx -t && systemctl reload nginx
changed_when: trueFragmento de la plantilla, para ver cómo se parametriza:
{# roles/web/templates/sitio.conf.j2 (fragmento) #}
{{ ansible_managed | comment }}
upstream tramontana_app {
{% for nodo in web_backends | default([{'host': web_upstream_host, 'puerto': web_upstream_puerto}]) %}
server {{ nodo.host }}:{{ nodo.puerto }} max_fails=3 fail_timeout=30s;
{% endfor %}
keepalive {{ web_upstream_keepalive }};
}
server {
listen 443 ssl default_server;
http2 on;
server_name {{ web_dominio }};
ssl_certificate /etc/letsencrypt/live/{{ web_dominio }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ web_dominio }}/privkey.pem;
add_header Strict-Transport-Security "max-age={{ web_hsts_max_age }}; includeSubDomains" always;
location = /salud {
access_log off;
{% for red in web_redes_salud %}
allow {{ red }};
{% endfor %}
deny all;
proxy_pass http://tramontana_app;
}
...
}Ese bucle sobre web_backends es la razón de haber usado un upstream desde el principio aunque hoy solo haya un servidor: añadir el segundo nodo de 07-07 será cambiar una variable del inventario.
Y el ciclo de aplicación, siempre igual:
$ cd ~/tramontana-infra
$ ansible-playbook sitio.yml --limit srv-tramontana-pruebas --tags web
$ ansible-playbook sitio.yml --limit srv-tramontana --tags web --check --diff
$ ansible-playbook sitio.yml --limit srv-tramontana --tags webPruebas primero, --check --diff en producción para ver exactamente qué cambiaría, y solo entonces en serio.
Operación y mantenimiento
Qué mirar en los registros a diario. Tres consultas que caben en dos minutos:
# 1. Codigos de estado de las ultimas 24 h
$ journalctl -t nginx_acceso --since "24 hours ago" -o cat | \
awk '{print $9}' | sort | uniq -c | sort -rn
3812 200
241 304
58 301
19 404
6 429
2 502
# 2. Las rutas mas lentas (urt = tiempo de la aplicacion)
$ journalctl -t nginx_acceso --since today -o cat | \
grep -oP '"[A-Z]+ \K[^ ]+(?=.*urt=\K)?' >/dev/null; \
journalctl -t nginx_acceso --since today -o cat | \
awk '{for(i=1;i<=NF;i++) if($i ~ /^urt=/){split($i,a,"=");
print a[2], $7}}' | sort -rn | head -5
3.812 /informes/facturacion
1.204 /casas/mas-figueres/disponibilidad
0.890 /informes/ocupacion
0.412 /reservas/1023
0.388 /casas
# 3. Errores del proxy
$ journalctl -t nginx_error --since "24 hours ago" -p warning| Señal | Qué significa | Qué hacer |
|---|---|---|
| 502 Bad Gateway | La aplicación no responde | systemctl status tramontana; ver errores.log |
| 504 Gateway Timeout | La aplicación tarda más que proxy_read_timeout |
Consulta lenta: 08-02, no subir el timeout sin más |
| Pico de 429 | Límite de tasa activo | ¿Abuso real o umbral mal calibrado? |
| Pico de 404 en rutas raras | Escaneo automático | Normal en Internet; vigilar si escala |
upstream prematurely closed |
La aplicación cerró la conexión | Suele ser un reinicio o un fallo de la app |
Nginx en el despliegue. desplegar.sh de 04-07 cambia el enlace /opt/tramontana/app y reinicia la aplicación. Ahora hay que añadir un paso: si la versión nueva trae estáticos distintos, Nginx debe recargarse para que alias apunte al nuevo destino resuelto.
# Fragmento a anadir en desplegar.sh, tras el ln -sfn atomico
recargar_proxy() {
if command -v nginx >/dev/null 2>&1; then
if sudo nginx -t >/dev/null 2>&1; then
sudo systemctl reload nginx
log "proxy recargado"
else
error "la configuracion de nginx no es valida; no se recarga"
return 1
fi
fi
}El problema de la caché, que es el incidente clásico tras un despliegue. Con expires 30d y Cache-Control: immutable, un navegador que ya descargó /static/estilo.css no volverá a pedirlo durante un mes, aunque el fichero haya cambiado. El usuario ve la versión nueva de la aplicación con los estilos de la anterior, y el resultado es una web rota que se arregla sola en unos días. Peor aún: los usuarios que no la sufren no pueden reproducir el problema.
La solución correcta no es bajar expires, que desperdiciaría la caché para siempre por un problema de un día. Es versionar el nombre del fichero:
| Enfoque | Cómo | Valoración |
|---|---|---|
estilo.css?v=3.2.1 |
Parámetro de consulta | Funciona, pero algunas cachés intermedias lo ignoran |
estilo.a3f19c.css |
Hash del contenido en el nombre | Lo correcto: URL nueva = descarga nueva |
Bajar expires a 5 min |
— | Pierde la ventaja de la caché |
| Purgar la caché del navegador | — | Imposible: no controlas el cliente |
Con el hash en el nombre, immutable es literalmente cierto y no hay conflicto: la URL nunca cambia de contenido. Es trabajo de la construcción de la aplicación, y por tanto una petición para Luis.
Renovación del certificado. certbot.timer renueva; el gancho recarga. revisar_certificado.sh de 06-05 avisa a 20 días. Lo único que cambia es que ahora hay que verificar que el gancho existe tras cada actualización del paquete de certbot:
$ ls -l /etc/letsencrypt/renewal-hooks/deploy/
-rwxr-xr-x 1 root root 46 ago 18 11:02 10-recargar-nginx.shActualizaciones de Nginx. needrestart de 06-06 detectará que el binario cambió y pedirá reiniciar el servicio. Aquí restart sí es necesario —un reload no cambia el binario en ejecución— y son unos cientos de milisegundos. Con dos nodos y drenaje, cero, que es otro argumento para la opción B de 07-07.
Errores Comunes y Consejos
- Usar
cert.pemen lugar defullchain.pem. Funciona en tu navegador, que ya tiene el intermedio cacheado, y falla en clientes móviles antiguos, encurlde otros sistemas y en integraciones. Es un fallo intermitente y desconcertante. Siemprefullchain.pem. - Olvidar
X-Forwarded-Proto. Bucle infinito de redirecciones si la aplicación fuerza HTTPS, y cookiesSecureque no se emiten. - Olvidar
X-Real-IP.acceso.logregistra127.0.0.1en todas las peticiones: adiós a la analítica, al diagnóstico y a cualquier bloqueo por IP. - Dejar el 8080 accesible desde fuera. El proxy no aporta nada si se puede saltar. Verifícalo desde otra máquina, no desde el propio servidor.
restarten lugar dereload. Corta las conexiones en curso y, con una configuración inválida, el servicio no vuelve a arrancar.- Recargar sin
nginx -t. Conreloadno rompes nada, pero pierdes el cambio sin enterarte y luego no entiendes por qué no se aplica. - HSTS con
max-agede dos años el primer día. Es irrevocable desde el servidor. Empieza con 300 segundos. - Confundir la precedencia de
location. Una regex vence a un prefijo más específico. Los directorios estáticos, con^~. - Poner
text/htmlengzip_typeso comprimir JPEG. Lo primero da un aviso, lo segundo gasta CPU para nada. limit_reqsinburst. Una página normal carga veinte recursos a la vez y todos menos el primero reciben 429.- Dejar el sitio
defaultactivo. Cualquier petición conHostdesconocido acaba en él, revelando la versión de Nginx. - Dejar que
certbot --nginxreescriba una configuración gestionada por Ansible. El fichero real y la plantilla divergen, y el siguienteansible-playbookdeshace el cambio en el peor momento. - Subir
proxy_read_timeoutpara «arreglar» los 504. Estás escondiendo una consulta lenta. Arréglala en la base de datos (08-02). - Consejo de método. Guarda la salida de
curl -wantes de montar el proxy. Sin ese punto de partida no puedes responder a «¿el TLS nos ha ralentizado?» con un número.
Ejercicios
Ejercicio 1
Un usuario informa de que, tras el despliegue de la versión 3.2.1, la web «se ve descolocada» en su portátil pero no en el de Marta. Diagnostica el problema desde los registros de Nginx, explica la causa y propón la solución definitiva con su justificación técnica.
Ejercicio 2
Escribe un script verificar_web.sh siguiendo las convenciones del curso que compruebe de extremo a extremo que el servidor web está correctamente configurado, devolviendo 0, 1 o 2 como revision_salud.sh. Debe incluir la comprobación de que el puerto 8080 no es accesible desde el exterior.
Ejercicio 3
Marta pregunta por qué ha habido que montar «otro servidor más» si la aplicación ya funcionaba, y si esto no añade un punto de fallo. Redacta la respuesta diciendo qué protege y qué no protege esta medida.
Soluciones
Solución 1
Diagnóstico. El síntoma —dos usuarios, comportamiento distinto, mismo servidor— apunta a algo que depende del estado del cliente, no del servidor. Los candidatos son la caché del navegador, una extensión o un proxy intermedio. Los registros lo confirman:
# Peticiones de estaticos en las ultimas 2 h, con su codigo
$ journalctl -t nginx_acceso --since "2 hours ago" -o cat | \
awk '$7 ~ /^\/static\// {print $1, $7, $9}' | sort | uniq -c
14 198.51.100.23 /static/estilo.css 200
1 203.0.113.77 /static/estilo.css 304La lectura es clara: el portátil de Marta (198.51.100.23) recibió 200 —descargó el fichero nuevo— y el del usuario (203.0.113.77) recibió 304 Not Modified, es decir, revalidó y se quedó con su copia. Y hay un dato aún más revelador: si el usuario ni siquiera hubiera pedido el fichero, no aparecería ninguna línea suya, porque expires 30d con immutable autoriza al navegador a no preguntar en absoluto.
# Confirmar las cabeceras que estamos enviando
$ curl -sI https://reservas.tramontana.example/static/estilo.css | \
grep -iE 'cache-control|expires|etag|last-modified'
cache-control: public, immutable
expires: Thu, 17 Sep 2026 09:41:22 GMT
etag: "66c2a1b3-2118"
last-modified: Mon, 18 Aug 2026 09:12:35 GMTLa causa. La configuración declara /static/estilo.css inmutable durante 30 días. El despliegue cambió el contenido del fichero manteniendo la URL. El navegador de Marta no tenía copia previa (o la había limpiado) y descargó la nueva; el del usuario tenía una copia de hace tres días y, según el contrato que le dimos, tiene todo el derecho a usarla hasta septiembre. El servidor está haciendo exactamente lo que le pedimos; el error está en el diseño, no en la configuración.
Por qué las soluciones intuitivas son malas:
| Solución tentadora | Por qué no |
|---|---|
Bajar expires a 5 minutos |
Renuncia a la caché para siempre por un problema de un día. Cada visita vuelve a descargar 41 KB |
Quitar immutable |
Reduce el problema a una revalidación, pero sigue habiendo ventana y añade una petición por recurso y visita |
| Pedir al usuario que pulse Ctrl+F5 | No escala: hay usuarios que no contactan y siguen viendo la web rota |
| Renombrar el fichero a mano en cada despliegue | Funciona y es frágil: se olvidará |
La solución definitiva: huella del contenido en el nombre del fichero. La construcción de la aplicación genera estilo.a3f19c8d.css, donde a3f19c8d son los primeros bytes del hash SHA-256 del contenido, y las plantillas HTML referencian ese nombre.
# La configuracion de Nginx no cambia: sigue siendo correcta.
location ^~ /static/ {
alias /opt/tramontana/app/static/;
expires 1y; # ahora SI puede ser un ano
add_header Cache-Control "public, immutable";
access_log off;
try_files $uri =404;
}Por qué esto lo resuelve del todo, y es un principio general de la web:
- Una URL nunca cambia de contenido.
immutabledeja de ser una promesa arriesgada y pasa a ser literalmente cierto. - Un cambio de contenido produce una URL nueva, que ninguna caché del mundo tiene. La descarga es inevitable y automática.
- El HTML sí debe ser efímero. Es quien contiene las referencias a los nombres con hash, así que se sirve con
Cache-Control: no-cache(revalidar siempre). Es un fichero pequeño y conETagla revalidación cuesta un 304 de 200 bytes.
# El HTML no se cachea: es el indice que apunta a los estaticos versionados
location / {
proxy_pass http://tramontana_app;
include /etc/nginx/snippets/proxy-tramontana.conf;
add_header Cache-Control "no-cache" always;
}Mitigación inmediata, mientras Luis implementa el hash, aplicable hoy mismo:
# Solucion transitoria: rutas de estaticos con la version de la release.
# /static/3.2.1/estilo.css -> /opt/tramontana/releases/3.2.1/static/estilo.css
location ~ ^/static/(?<version>[0-9]+\.[0-9]+\.[0-9]+)/(?<recurso>.*)$ {
alias /opt/tramontana/releases/$version/static/$recurso;
expires 1y;
add_header Cache-Control "public, immutable";
}Encaja con la estructura /opt/tramontana/releases/<version>/ que existe desde el Módulo 5, y da el mismo resultado sin tocar la construcción: cada versión tiene su propio espacio de URL.
Y la lección de método: este incidente se detecta desde el registro comparando códigos 200 y 304 por IP. Merece entrar en el cuaderno de guardia que formalizarás en 08-06, porque volverá a ocurrir con cualquier recurso nuevo que se cachee agresivamente.
Solución 2
#!/usr/bin/env bash
#
# verificar_web.sh - Verificacion de extremo a extremo del proxy inverso
#
# Codigos de salida (identicos a revision_salud.sh):
# 0 = todo correcto
# 1 = aviso: algo degradado pero el servicio funciona
# 2 = critico: el servicio no es correcto o no es seguro
#
set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comunes.sh
source "${SCRIPT_DIR}/lib/comunes.sh"
readonly TRAMONTANA_DOMINIO="${TRAMONTANA_DOMINIO:-reservas.tramontana.example}"
readonly TRAMONTANA_IP="${TRAMONTANA_IP:-10.0.2.15}"
readonly TRAMONTANA_PUERTO_APP="${TRAMONTANA_PUERTO_APP:-8080}"
readonly TRAMONTANA_HOST_EXTERNO="${TRAMONTANA_HOST_EXTERNO:-192.168.122.104}"
readonly TRAMONTANA_UMBRAL_CERT="${TRAMONTANA_UMBRAL_CERT:-20}"
umask 027
# Estado acumulado: se queda siempre con el peor resultado visto
estado_global=0
registrar() {
local nivel="$1" mensaje="$2"
case "$nivel" in
ok) log "OK $mensaje" ;;
aviso) error "AVISO $mensaje"; (( estado_global < 1 )) && estado_global=1 ;;
critico) error "CRITICO $mensaje"; estado_global=2 ;;
esac
return 0
}
comprobar_configuracion() {
if sudo nginx -t >/dev/null 2>&1; then
registrar ok "la configuracion de nginx es valida"
else
registrar critico "nginx -t falla: la configuracion en disco no es valida"
fi
}
comprobar_servicio() {
if systemctl is-active --quiet nginx; then
registrar ok "nginx activo"
else
registrar critico "nginx NO esta activo"
return 0
fi
# Configuracion en disco distinta de la cargada: alguien edito sin recargar
local arranque
arranque="$(systemctl show nginx -p ActiveEnterTimestampMonotonic --value)"
local mtime_ns
mtime_ns="$(stat -c %Y /etc/nginx/sites-enabled/"${TRAMONTANA_DOMINIO}" 2>/dev/null || echo 0)"
local arranque_epoch
arranque_epoch="$(date -d "$(systemctl show nginx -p ActiveEnterTimestamp --value)" +%s 2>/dev/null || echo 0)"
if (( mtime_ns > arranque_epoch )); then
registrar aviso "la configuracion se modifico despues del ultimo reload"
fi
}
comprobar_redireccion() {
local codigo destino
codigo="$(curl -s -o /dev/null -w '%{http_code}' -m 5 \
"http://${TRAMONTANA_DOMINIO}/casas")" || {
registrar critico "el puerto 80 no responde"
return 0
}
destino="$(curl -s -o /dev/null -w '%{redirect_url}' -m 5 \
"http://${TRAMONTANA_DOMINIO}/casas")"
if [[ "$codigo" == "301" && "$destino" == https://* ]]; then
registrar ok "HTTP redirige a HTTPS ($destino)"
else
registrar critico "HTTP no redirige a HTTPS (codigo $codigo)"
fi
}
comprobar_https_y_cabeceras() {
local cabeceras
cabeceras="$(curl -sI -m 10 "https://${TRAMONTANA_DOMINIO}/" | tr 'A-Z' 'a-z')" || {
registrar critico "HTTPS no responde"
return 0
}
grep -q '^http/2 200' <<<"$cabeceras" \
&& registrar ok "HTTPS responde 200 por HTTP/2" \
|| registrar aviso "HTTPS responde pero no por HTTP/2"
local -a obligatorias=(
strict-transport-security
x-content-type-options
x-frame-options
referrer-policy
content-security-policy
)
local h faltan=0
for h in "${obligatorias[@]}"; do
grep -q "^${h}:" <<<"$cabeceras" || { registrar aviso "falta la cabecera $h"; faltan=1; }
done
(( faltan == 0 )) && registrar ok "las 5 cabeceras de seguridad estan presentes"
grep -q '^server: nginx$' <<<"$cabeceras" \
|| registrar aviso "server_tokens parece activado: se revela la version"
}
comprobar_tls() {
local salida
salida="$(echo | timeout 10 openssl s_client -connect "${TRAMONTANA_DOMINIO}:443" \
-servername "${TRAMONTANA_DOMINIO}" -status 2>/dev/null)" || {
registrar critico "no se pudo negociar TLS"
return 0
}
grep -q 'Verify return code: 0 (ok)' <<<"$salida" \
&& registrar ok "cadena de certificados valida" \
|| registrar critico "la cadena de certificados NO valida (revisa fullchain.pem)"
grep -q 'OCSP Response Status: successful' <<<"$salida" \
&& registrar ok "grapado OCSP activo" \
|| registrar aviso "sin grapado OCSP"
# TLS antiguo debe ser rechazado
if echo | timeout 5 openssl s_client -connect "${TRAMONTANA_DOMINIO}:443" \
-tls1_1 >/dev/null 2>&1; then
registrar critico "TLS 1.1 sigue aceptado"
else
registrar ok "TLS 1.0 y 1.1 rechazados"
fi
# Dias hasta la caducidad (reutiliza el umbral de 06-05)
local fin dias
fin="$(echo | openssl s_client -connect "${TRAMONTANA_DOMINIO}:443" \
-servername "${TRAMONTANA_DOMINIO}" 2>/dev/null | \
openssl x509 -noout -enddate | cut -d= -f2)"
dias=$(( ( $(date -d "$fin" +%s) - $(date +%s) ) / 86400 ))
if (( dias < 7 )); then
registrar critico "el certificado caduca en $dias dias"
elif (( dias < TRAMONTANA_UMBRAL_CERT )); then
registrar aviso "el certificado caduca en $dias dias"
else
registrar ok "certificado valido $dias dias mas"
fi
}
# La comprobacion mas importante: el puerto de la aplicacion NO debe
# ser alcanzable desde fuera. Se prueba DESDE OTRA MAQUINA, porque
# desde el propio servidor 127.0.0.1:8080 siempre respondera.
comprobar_puerto_app_cerrado() {
if ! ss -tlnp 2>/dev/null | grep -q "127.0.0.1:${TRAMONTANA_PUERTO_APP}"; then
registrar aviso "la aplicacion no escucha en 127.0.0.1:${TRAMONTANA_PUERTO_APP}"
fi
# Escucha en 0.0.0.0 = expuesta aunque el firewall la tape
if ss -tlnp 2>/dev/null | grep -qE "0\.0\.0\.0:${TRAMONTANA_PUERTO_APP}|\*:${TRAMONTANA_PUERTO_APP}"; then
registrar critico "la aplicacion escucha en TODAS las interfaces"
fi
if ! ssh -o BatchMode=yes -o ConnectTimeout=5 \
"operador@${TRAMONTANA_HOST_EXTERNO}" true 2>/dev/null; then
registrar aviso "no hay acceso a ${TRAMONTANA_HOST_EXTERNO}: no se pudo probar desde fuera"
return 0
fi
if ssh -o BatchMode=yes "operador@${TRAMONTANA_HOST_EXTERNO}" \
"nc -z -w3 ${TRAMONTANA_IP} ${TRAMONTANA_PUERTO_APP}" 2>/dev/null; then
registrar critico "el puerto ${TRAMONTANA_PUERTO_APP} ES ACCESIBLE desde fuera"
else
registrar ok "el puerto ${TRAMONTANA_PUERTO_APP} esta cerrado desde el exterior"
fi
}
comprobar_limite_tasa() {
local codigos=""
local i
for i in $(seq 1 12); do
codigos+="$(curl -s -o /dev/null -w '%{http_code}' -m 5 \
"https://${TRAMONTANA_DOMINIO}/acceso") "
done
if grep -q '429' <<<"$codigos"; then
registrar ok "el limite de tasa de /acceso responde 429 ante rafagas"
else
registrar aviso "el limite de tasa de /acceso no se activo: [$codigos]"
fi
}
comprobar_gancho_certbot() {
local gancho=/etc/letsencrypt/renewal-hooks/deploy/10-recargar-nginx.sh
if [[ -x "$gancho" ]]; then
registrar ok "gancho de recarga tras renovacion presente"
else
registrar aviso "falta el gancho de recarga: tras renovar seguiria el certificado viejo"
fi
}
main() {
requiere_comando curl
requiere_comando openssl
requiere_comando ss
comprobar_configuracion
comprobar_servicio
comprobar_redireccion
comprobar_https_y_cabeceras
comprobar_tls
comprobar_puerto_app_cerrado
comprobar_limite_tasa
comprobar_gancho_certbot
case "$estado_global" in
0) log "verificacion completa: todo correcto" ;;
1) error "verificacion completa con AVISOS" ;;
2) error "verificacion completa: estado CRITICO" ;;
esac
return "$estado_global"
}
main "$@"$ chmod 0750 ~/scripts/verificar_web.sh
$ shellcheck ~/scripts/verificar_web.sh && echo "sin avisos"
sin avisos
$ ~/scripts/verificar_web.sh; echo "estado: $?"
[2026-08-18 12:04:11] OK la configuracion de nginx es valida
[2026-08-18 12:04:11] OK nginx activo
[2026-08-18 12:04:12] OK HTTP redirige a HTTPS (https://reservas.tramontana.example/casas)
[2026-08-18 12:04:12] OK HTTPS responde 200 por HTTP/2
[2026-08-18 12:04:12] OK las 5 cabeceras de seguridad estan presentes
[2026-08-18 12:04:13] OK cadena de certificados valida
[2026-08-18 12:04:13] OK grapado OCSP activo
[2026-08-18 12:04:14] OK TLS 1.0 y 1.1 rechazados
[2026-08-18 12:04:14] OK certificado valido 71 dias mas
[2026-08-18 12:04:15] OK el puerto 8080 esta cerrado desde el exterior
[2026-08-18 12:04:17] OK el limite de tasa de /acceso responde 429 ante rafagas
[2026-08-18 12:04:17] OK gancho de recarga tras renovacion presente
[2026-08-18 12:04:17] verificacion completa: todo correcto
estado: 0Cuatro decisiones de diseño del script:
- El estado se acumula al peor, no al último. Un
AVISOseguido de unOKno debe borrar el aviso. De ahí la variableestado_globaly elregistrarque solo sube el nivel. return 0explícito en cada función. Conset -euo pipefail, una función cuya última expresión sea falsa aborta el script. Elreturn 0al final deregistrares imprescindible, y es un error queshellcheckno detecta.- La comprobación externa degrada a aviso, no a crítico, si no hay acceso. No poder probar algo no es lo mismo que probarlo y que falle. Confundirlo produce alertas falsas cuando la máquina de pruebas está apagada, y eso lleva directo a la fatiga de alertas de 08-06.
- Distingue «escucha en 0.0.0.0» de «es accesible». La primera es un fallo de configuración de la aplicación tapado por el firewall; la segunda es una exposición real. Ambas son críticas, pero la causa y el arreglo son distintos.
Para dejarlo operativo, un timer que lo ejecute tras cada renovación y cada mañana, en la línea de comprobar_copia.sh:
# /etc/systemd/system/verificar-web.timer
[Unit]
Description=Verificacion diaria del servidor web
[Timer]
OnCalendar=*-*-* 08:15:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.targetSilencio si todo va bien: la unidad de servicio solo escribe al journal, y solo un estado 2 dispara la notificación.
Solución 3
Por qué hemos puesto un servidor web delante de la aplicación Para: Marta Vidal · De: Operaciones de sistemas · 18 de agosto de 2026
Resumen: desde hoy, todo lo que viaja entre el navegador de un cliente y nuestro servidor va cifrado. Hasta esta mañana no lo estaba, y eso incluía nombres, teléfonos, correos y fechas de estancia de las personas que reservan. Era el problema abierto más serio que teníamos y ya está resuelto.
Qué hemos montado, sin tecnicismos. Hemos puesto un programa llamado Nginx a recibir las visitas de la web. Antes, la aplicación de reservas atendía directamente; ahora atiende Nginx, comprueba y ordena la petición, y se la pasa a la aplicación, que sigue exactamente igual que estaba, sin ningún cambio. Para el cliente es invisible; para nosotros cambia bastante.
Qué protege esta medida:
Protege contra Cómo Que alguien lea los datos de una reserva en tránsito Todo el tráfico va cifrado con el certificado que preparamos en junio Que alguien modifique lo que ve el cliente El cifrado también garantiza que nadie altera el contenido por el camino Que un navegador entre por error sin cifrar Redirección automática, y una instrucción al navegador para que no lo intente siquiera Que alguien machaque el formulario de acceso probando contraseñas Como máximo 5 intentos por minuto y dirección Que una sola dirección sature el servicio Límite de conexiones simultáneas Que un fallo de la aplicación exponga información técnica Nginx muestra una página de error propia Que la web se cargue disfrazada dentro de otra web fraudulenta Cabeceras de seguridad que el navegador respeta Qué NO protege, y conviene tenerlo claro:
- No protege los datos guardados. El cifrado es del viaje, no del almacén. Los datos siguen en la base de datos como estaban; eso se aborda en el trabajo de la semana que viene.
- No protege de un fallo de la propia aplicación. Si hay un error en el código de reservas, Nginx lo transmite igual.
- No protege de una contraseña débil de un empleado. Eso lo cubren otras medidas que ya tenemos.
- No sustituye a las copias de seguridad ni evita un borrado accidental.
- No hace la web más rápida en la primera visita. De hecho la primera conexión tarda unos 20 milisegundos más por el cifrado — una veinteava parte de un parpadeo.
Sobre si añade un punto de fallo: sí, y la pregunta es buena. Es literalmente cierto que ahora hay una pieza más que puede romperse. Tres matices:
- Nginx es de las piezas de software más probadas que existen: mueve una parte enorme de las webs del mundo y es extraordinariamente estable. La probabilidad de que falle él antes que nuestra aplicación es muy baja.
- También quita fallos. Antes, cada actualización de la aplicación cortaba el servicio unos segundos y el cliente veía un error de conexión. Ahora Nginx sigue ahí y muestra una página decente. Y las imágenes y los estilos los sirve él, así que si la aplicación se satura, la web al menos no se ve rota del todo.
- Lo hemos medido: la web transfiere un 78 % menos de datos que antes, porque Nginx los comprime. En un móvil con cobertura mala eso se nota más que los 20 milisegundos del cifrado.
Lo que ha costado. Una mañana de trabajo, y cero euros: el certificado es gratuito y se renueva solo cada tres meses, con un aviso automático veinte días antes por si algo fallara. La configuración está guardada como código, así que si mañana hubiera que reconstruir el servidor entero, esto se reproduce en minutos junto con el resto.
Qué queda pendiente y en qué orden lo propongo:
- Esta semana: subir a dos años la instrucción que impide a los navegadores usar conexiones sin cifrar. La hemos puesto de cinco minutos a propósito, para verificar unos días que todo funciona antes de comprometernos — una vez enviada, no se puede retirar.
- Próxima semana: revisar con Luis un detalle de la web (los ficheros de estilos), porque tras cada actualización algunos clientes pueden ver la versión anterior guardada en su navegador durante unos días. Ya sabemos cómo resolverlo.
- Este trimestre: la base de datos, que es la siguiente pieza que quiero dejar en condiciones.
Una nota final, importante para el cumplimiento. Servir datos personales sin cifrar es difícil de defender ante el Reglamento General de Protección de Datos, que exige medidas técnicas apropiadas. Con esto pasamos de una situación cuestionable a una correcta y, además, comprobable: puedo generar en cualquier momento un informe automático que acredite que el cifrado está activo y correctamente configurado. Lo incorporaré al procedimiento de revisión diaria.
Conclusión
La deuda más antigua del curso está cerrada. El certificado que emitiste en 06-05 y que llevaba dos módulos esperando en el disco ya está en uso: https://reservas.tramontana.example responde cifrado, con TLS 1.2 y 1.3, suites tomadas de una fuente mantenida en lugar de inventadas, grapado OCSP, cadena completa desde fullchain.pem, y las cinco cabeceras de seguridad que convierten al navegador del cliente en un aliado. Y lo has verificado con openssl s_client en vez de confiar en que funciona.
Entiendes el proxy inverso como lo que es: un punto único donde se aplica la política —cifrado, cabeceras, compresión, límite de tasa, registro— sin que la aplicación tenga que saber nada de ello. Sabes cómo Nginx elige el server por listen y server_name, y cómo elige el location con una precedencia en la que una regex vence a un prefijo más específico, que es la fuente de la mitad de los desconciertos. Sabes por qué cada proxy_set_header está donde está: sin Host los enlaces salen rotos, sin X-Real-IP los registros son inútiles, y sin X-Forwarded-Proto la aplicación entra en un bucle de redirecciones. Y sabes que nginx -t va siempre antes de un reload, y que reload va siempre en lugar de restart.
También has medido. Los 19 ms del saludo TLS, los 78 % menos de bytes por la compresión, las peticiones de estáticos que la aplicación ha dejado de atender, y el p99 de $upstream_response_time que separa «el cliente tiene mala conexión» de «la aplicación va lenta». Ese último dato apunta directamente a la siguiente lección, porque cuando urt sube, la culpable casi nunca es la aplicación: es una consulta.
En 08-02 montarás el servidor de base de datos en condiciones. PostgreSQL 16 lleva desde el Módulo 5 funcionando con la configuración por defecto, que está pensada para arrancar en cualquier sitio y no para los 3,8 GB de srv-tramontana. Ajustarás la memoria con fórmulas razonadas, conectarás con lo que aprendiste sobre páginas enormes transparentes en 07-03, resolverás de una vez el desajuste entre max_conexiones=80 de la aplicación y max_connections=100 del servidor —que ya provocó un incidente en 07-02— poniendo PgBouncer en modo transacción, cerrarás pg_hba.conf campo a campo, y montarás copias físicas con archivado de WAL y recuperación a un punto en el tiempo, que es lo único que permite deshacer un DELETE sin WHERE. Ahí es donde vive de verdad el negocio de Tramontana.
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
