#!/usr/bin/env bash # ============================================================================= # demo-02 · Las correcciones de configuración # ============================================================================= # A qué punto del guion sirve: punto 13 # «Y se puede tratar en Traefik y en Umami» # # Qué se ve en pantalla: los dos fragmentos correctos, con la línea que importa # señalada, y una comprobación de que lo son de verdad. # 1. Traefik → `config/traefik-entrypoints.yml` # forwardedHeaders.trustedIPs y proxyProtocol.trustedIPs # apuntando SOLO al proxy correcto. # 2. Umami → `config/umami.env` # CLIENT_IP_HEADER=x-real-ip # # La frase que resume las dos: una cabecera que el proxy REESCRIBE frente a una # cabecera que CUALQUIERA PUEDE ESCRIBIR. # # Estado: VERIFICADO en local. El YAML se parsea y el fichero de entorno se # comprueba con comandos; la salida real está en # `verificacion/demo-02-salida.txt`. # # Seguridad: solo lee los ficheros de `config/`. No hay ningún secreto en ellos # (el token de Cloudflare, si algún día se usa, no vive aquí: va en un `podman # secret`). La única dirección de la confianza es la del borde de este montaje # (loopback); donde se usa un valor de mentira se marca como rango de # documentación (RFC 5737). # ============================================================================= set -u AQUI="$(cd "$(dirname "$0")" && pwd)" TRAEFIK="$AQUI/config/traefik-entrypoints.yml" UMAMI="$AQUI/config/umami.env" titulo() { printf '\n\033[1m── %s\033[0m\n' "$1"; } orden() { printf '\n\033[1m$ %s\033[0m\n' "$1"; } nota() { printf ' ↳ %s\n' "$1"; } ok() { printf ' \033[1m✔ %s\033[0m\n' "$1"; } fallo() { printf ' \033[1m✘ %s\033[0m\n' "$1"; } rc_final=0 printf '\033[1mLas dos correcciones del punto 13\033[0m\n' # ----------------------------------------------------------------------------- titulo "1 · Traefik: de quién se fía el proxy" # ----------------------------------------------------------------------------- # El error de partida (lo que se cuenta en voz alta, no hace falta fichero): # proxyProtocol: trustedIPs: [el rango de tu vecindario de contenedores] # ← confía en TODO el vecindario: cualquiera # que alcance el puerto puede contar que # viene de donde le apetezca # forwardedHeaders: insecure: true ← se cree cualquier cabecera # Con eso, cualquiera que alcance el puerto de Traefik entra diciendo que viene de # donde quiera. orden "cat config/traefik-entrypoints.yml" cat "$TRAEFIK" # ¿Se parsea? Un YAML roto en el arranque deja el proxy sin servicios. if command -v python3 >/dev/null 2>&1 && python3 -c 'import yaml' 2>/dev/null; then orden "python3 -c 'yaml.safe_load(...)' # ¿el YAML es válido y dice lo que creemos?" python3 - "$TRAEFIK" <<'PY' import sys, yaml with open(sys.argv[1]) as f: cfg = yaml.safe_load(f) ep = cfg["entryPoints"]["websecure"] print("address :", ep["address"]) print("proxyProtocol.trustedIPs :", ep["proxyProtocol"]["trustedIPs"]) print("forwardedHeaders.insecure :", ep.get("forwardedHeaders", {}).get("insecure", False)) print("forwardedHeaders.trustedIPs :", ep["forwardedHeaders"]["trustedIPs"]) PY ok "YAML válido y con las listas presentes." else nota "python3 + PyYAML no disponibles: se salta la comprobación de sintaxis." fi # Auditoría: lo que NO debe haber. Solo miran las líneas ACTIVAS (sin '#'). orden "grep -vE '^[[:space:]]*#' config/traefik-entrypoints.yml | grep -nE 'insecure: true|/[0-9]{1,2}\"' " if grep -vE '^[[:space:]]*#' "$TRAEFIK" | grep -E 'insecure: true|/(0|[0-9]|1[0-9]|2[0-3])"'; then fallo "hay un rango demasiado ancho o un 'insecure' activo." rc_final=1 else ok "ni 'insecure: true' ni rangos anchos activos: solo la IP del borde." fi # La misma auditoría, pero entendiendo el YAML (si hay python3). Es la que manda. if command -v python3 >/dev/null 2>&1 && python3 -c 'import yaml' 2>/dev/null; then orden "python3 -c 'auditar el YAML: insecure y prefijos de red'" if python3 - "$TRAEFIK" <<'PY' import sys, ipaddress, yaml ep = yaml.safe_load(open(sys.argv[1]))["entryPoints"]["websecure"] problemas = [] for nombre in ("forwardedHeaders", "proxyProtocol"): bloque = ep.get(nombre) or {} if bloque.get("insecure"): problemas.append(f"{nombre}.insecure: true ← se cree a cualquiera") for cidr in bloque.get("trustedIPs") or []: red = ipaddress.ip_network(cidr, strict=False) minimo = 24 if red.version == 4 else 64 if red.prefixlen < minimo: problemas.append(f"{nombre}.trustedIPs demasiado ancho: {cidr}") else: print(f" {nombre}.trustedIPs → {cidr} (red pequeña, correcto)") if problemas: for p in problemas: print(" ✘", p) sys.exit(1) print(" ✔ sin 'insecure' y sin rangos anchos.") PY then ok "el YAML auditado no tiene confianzas excesivas." else fallo "el YAML confía en más gente de la que debería." rc_final=1 fi fi # ----------------------------------------------------------------------------- titulo "2 · Umami: qué cabecera lee" # ----------------------------------------------------------------------------- # Mismo razonamiento, al revés: aquí no se restringe QUIÉN puede llegar, se elige # QUÉ cabecera creerse. Se elige la que el proxy reescribe. orden "cat config/umami.env" cat "$UMAMI" orden "grep -n '^CLIENT_IP_HEADER' config/umami.env" if grep -q '^CLIENT_IP_HEADER=x-real-ip' "$UMAMI"; then ok "CLIENT_IP_HEADER=x-real-ip → la cabecera que Traefik reconstruye." else fallo "CLIENT_IP_HEADER no apunta a x-real-ip." rc_final=1 fi orden "grep -nE 'cf-connecting-ip|true-client-ip' config/umami.env # no debe haber ninguna activa" if grep -E '^[[:space:]]*CLIENT_IP_HEADER=.*(cf-connecting-ip|true-client-ip)' "$UMAMI"; then fallo "queda una cabecera que cualquiera puede escribir." rc_final=1 else ok "ninguna cabecera de cliente (cf-connecting-ip / true-client-ip) activa." fi # ----------------------------------------------------------------------------- titulo "Por qué, en una frase (para decir en el aire)" # ----------------------------------------------------------------------------- cat <<'TEXTO' · En el proxy: no te fíes de todos. 'trustedIPs' con la IP exacta del borde; si te fías del rango entero, cualquiera que llegue te cuenta de dónde viene. · En la analítica: lee la cabecera que el proxy reescribe (x-real-ip), no una que pueda rellenar el primero que pase (cf-connecting-ip, x-forwarded-for). · Y recuerda el límite: esto ordena la casa, no tapa el agujero. Si el 8443 está abierto al mundo, la fiabilidad la da el firewall, no la cabecera. · Y ojo con el alcance: esto funcionaba… hasta que un sábado por la tarde el bloqueo judicial tumbó el rango entero del proxy que había delante. TEXTO printf '\n' if [ "$rc_final" -eq 0 ]; then printf '\033[1mResultado: las dos correcciones están en su sitio.\033[0m\n\n' else printf '\033[1mResultado: hay algo que corregir (ver arriba).\033[0m\n\n' fi exit "$rc_final"