demo-01-diagnostico-de-tu-motor.sh(文件已创建)
| @@ -0,0 +1,156 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # ============================================================================= | |
| 3 | + | # demo-01 · Diagnóstico de tu motor | |
| 4 | + | # ============================================================================= | |
| 5 | + | # A qué punto del guion sirve: puntos 1, 2, 10 y 11 | |
| 6 | + | # · 1 «Dos mundos: Podman en mi portátil, Docker en el servidor» | |
| 7 | + | # → `podman --version` y `docker --version`: dos binarios, dos diseños. | |
| 8 | + | # · 2 «Y ahora quiero el servidor sin root» | |
| 9 | + | # → cómo se sabe si tu contenedor corre rootless: `podman info` / | |
| 10 | + | # `docker info`, y el mapa de identificadores del espacio de usuario. | |
| 11 | + | # · 10 «Qué cambia rootless de verdad» | |
| 12 | + | # → la línea `rootlessPortForwarder` de `podman info`: el reenviador | |
| 13 | + | # que hace de proxy y se come la IP de origen. | |
| 14 | + | # · 11 «No es cosa de Podman: es cosa de rootless» | |
| 15 | + | # → el mismo dato, buscado en `docker info`. | |
| 16 | + | # | |
| 17 | + | # Qué se ve en pantalla: cuatro bloques, uno por idea. Cada bloque lanza el | |
| 18 | + | # comando exacto y comenta en una línea lo que significa lo que ha salido. | |
| 19 | + | # | |
| 20 | + | # Estado: | |
| 21 | + | # · Podman (versiones, rootless, mapa de UID, reenviador) → VERIFICADO en | |
| 22 | + | # local con Podman 6.1.4 rootless. La salida real está en | |
| 23 | + | # `verificacion/demo-01-salida.txt`. | |
| 24 | + | # · Docker → NO VERIFICADO aquí: en esta máquina no hay binario `docker`. | |
| 25 | + | # El bloque está escrito para que funcione si lo hay, y si no, imprime la | |
| 26 | + | # salida ESPERADA y qué línea hay que buscar. | |
| 27 | + | # | |
| 28 | + | # Seguridad de la demo: solo LEE. No crea contenedores, no toca la red del | |
| 29 | + | # sistema y no modifica nada. `podman unshare` abre un espacio de nombres de | |
| 30 | + | # usuario, ejecuta `cat` y sale. | |
| 31 | + | # ============================================================================= | |
| 32 | + | ||
| 33 | + | set -u | |
| 34 | + | ||
| 35 | + | titulo() { printf '\n\033[1m── %s\033[0m\n' "$1"; } | |
| 36 | + | orden() { printf '\n\033[1m$ %s\033[0m\n' "$1"; } | |
| 37 | + | nota() { printf ' ↳ %s\n' "$1"; } | |
| 38 | + | esperado() { printf '\n\033[1m[ESPERADO, no ejecutado aquí]\033[0m\n'; } | |
| 39 | + | ||
| 40 | + | printf '\033[1mDiagnóstico de tu motor de contenedores\033[0m (puntos 1, 2, 10 y 11)\n' | |
| 41 | + | printf 'Máquina: %s · %s\n' "$(hostname)" "$(date -Is)" | |
| 42 | + | ||
| 43 | + | # ----------------------------------------------------------------------------- | |
| 44 | + | titulo "1 · ¿Qué motor tengo? · punto 1" | |
| 45 | + | # ----------------------------------------------------------------------------- | |
| 46 | + | # La versión dice mucho más de lo que parece: aquí no se elige "Podman o | |
| 47 | + | # Docker" por gusto, son dos arquitecturas. Podman no tiene demonio; Docker sí. | |
| 48 | + | if command -v podman >/dev/null 2>&1; then | |
| 49 | + | orden "podman --version" | |
| 50 | + | podman --version | |
| 51 | + | nota "Un binario más que contesta. No hay servicio con privilegios detrás:" | |
| 52 | + | nota "el contenedor es un hijo de esta sesión y muere con ella." | |
| 53 | + | else | |
| 54 | + | nota "no hay 'podman' en el PATH de esta máquina." | |
| 55 | + | fi | |
| 56 | + | ||
| 57 | + | if command -v docker >/dev/null 2>&1; then | |
| 58 | + | orden "docker --version" | |
| 59 | + | docker --version | |
| 60 | + | nota "Mismo software por debajo, distinto modelo: hay un 'dockerd'" | |
| 61 | + | nota "arrancado como root al que le pides el contenedor." | |
| 62 | + | else | |
| 63 | + | esperado | |
| 64 | + | printf '$ docker --version\n' | |
| 65 | + | printf 'Docker version 28.x.y, build xxxxxxx\n' | |
| 66 | + | nota "La versión en sí no te dice si es rootless: para eso, el bloque 2." | |
| 67 | + | fi | |
| 68 | + | ||
| 69 | + | # ----------------------------------------------------------------------------- | |
| 70 | + | titulo "2 · ¿Mi motor es rootless? · punto 2" | |
| 71 | + | # ----------------------------------------------------------------------------- | |
| 72 | + | # Pregunta útil, sobre todo en el servidor: si es rootless, el usuario cero | |
| 73 | + | # dentro del contenedor NO es el root de la máquina. | |
| 74 | + | if command -v podman >/dev/null 2>&1; then | |
| 75 | + | orden "podman info --format '{{.Host.Security.Rootless}}'" | |
| 76 | + | podman info --format '{{.Host.Security.Rootless}}' | |
| 77 | + | nota "'true' = rootless. 'false' = los contenedores corren como root de verdad." | |
| 78 | + | ||
| 79 | + | orden "podman info | grep -i rootless" | |
| 80 | + | podman info 2>/dev/null | grep -i rootless | |
| 81 | + | nota "Tres líneas, y ninguna sobra: 'rootlessNetworkCmd' (con qué se monta la" | |
| 82 | + | nota "red: aquí 'pasta'), 'rootlessPortForwarder' (quiénes publican los puertos:" | |
| 83 | + | nota "aquí 'rootlessport') y 'rootless: true', que es la tercera y la misma" | |
| 84 | + | nota "respuesta del comando anterior. El reenviador del bloque 10 es la segunda." | |
| 85 | + | else | |
| 86 | + | nota "no hay 'podman' en el PATH: se salta la comprobación rootless." | |
| 87 | + | fi | |
| 88 | + | ||
| 89 | + | # ----------------------------------------------------------------------------- | |
| 90 | + | titulo "3 · Qué significa rootless por dentro · punto 2" | |
| 91 | + | # ----------------------------------------------------------------------------- | |
| 92 | + | # El truco es del kernel: espacios de nombres de usuario. Dentro, el proceso se | |
| 93 | + | # cree root; fuera, es un usuario sin privilegios con un rango de identificadores | |
| 94 | + | # prestados. Se ve en el mapa de UID. | |
| 95 | + | # Se usa 'podman unshare' para NO tener que crear ningún contenedor: abre el | |
| 96 | + | # espacio de nombres, ejecuta 'cat' y sale. | |
| 97 | + | if command -v podman >/dev/null 2>&1; then | |
| 98 | + | orden "podman unshare cat /proc/self/uid_map" | |
| 99 | + | podman unshare cat /proc/self/uid_map 2>&1 | |
| 100 | + | nota "El 0 de la izquierda (dentro) es un 1000 cualquiera (fuera):" | |
| 101 | + | nota "quien escape del contenedor no encuentra el root del host, encuentra un don nadie." | |
| 102 | + | nota "En un contenedor ya en marcha, el mismo dato se ve con:" | |
| 103 | + | nota " cat /proc/self/uid_map (dentro del contenedor)" | |
| 104 | + | else | |
| 105 | + | nota "sin podman no hay espacio de nombres que enseñar." | |
| 106 | + | fi | |
| 107 | + | ||
| 108 | + | # ----------------------------------------------------------------------------- | |
| 109 | + | titulo "4 · El reenviador: donde se pierde la IP · punto 10" | |
| 110 | + | # ----------------------------------------------------------------------------- | |
| 111 | + | # En rootless nadie abre el puerto en el kernel por ti: lo abre un proceso tuyo | |
| 112 | + | # que escucha y reenvía hacia el contenedor. Ese proceso es un proxy, y por eso | |
| 113 | + | # dentro del contenedor la conexión no llega del visitante sino del mensajero. | |
| 114 | + | if command -v podman >/dev/null 2>&1; then | |
| 115 | + | orden "podman info --format '{{.Host.RootlessPortForwarder}}'" | |
| 116 | + | podman info --format '{{.Host.RootlessPortForwarder}}' 2>&1 | |
| 117 | + | orden "podman info --format '{{.Host.RootlessNetworkCmd}}'" | |
| 118 | + | podman info --format '{{.Host.RootlessNetworkCmd}}' 2>&1 | |
| 119 | + | nota "Esos dos nombres son la pieza de la que habla el guion: el reenvío" | |
| 120 | + | nota "de puertos ocurre en el espacio de usuario, no en el NAT del kernel." | |
| 121 | + | else | |
| 122 | + | nota "sin podman no hay reenviador que enseñar." | |
| 123 | + | fi | |
| 124 | + | ||
| 125 | + | # ----------------------------------------------------------------------------- | |
| 126 | + | titulo "5 · ¿Y en Docker? Exactamente lo mismo · punto 11" | |
| 127 | + | # ----------------------------------------------------------------------------- | |
| 128 | + | # La frase del episodio: no es cosa de Podman, es cosa de rootless. Docker | |
| 129 | + | # rootless arrastra el mismo comportamiento porque se apoya en RootlessKit. | |
| 130 | + | if command -v docker >/dev/null 2>&1; then | |
| 131 | + | orden "docker info --format '{{.SecurityOptions}}'" | |
| 132 | + | docker info --format '{{.SecurityOptions}}' 2>&1 | |
| 133 | + | nota "Si aparece 'rootless' en la lista, es rootless. Si no aparece, no lo es." | |
| 134 | + | ||
| 135 | + | orden "docker context inspect --format '{{.Endpoints.docker.Host}}'" | |
| 136 | + | docker context inspect --format '{{.Endpoints.docker.Host}}' 2>&1 | |
| 137 | + | nota "Segunda pista: un socket en /run/user/<uid>/docker.sock es rootless;" | |
| 138 | + | nota "unix:///var/run/docker.sock es el demonio de root de toda la vida." | |
| 139 | + | else | |
| 140 | + | esperado | |
| 141 | + | printf '$ docker info --format '"'"'{{.SecurityOptions}}'"'"'\n' | |
| 142 | + | printf '[name=seccomp,profile=builtin name=rootless name=cgroupns]\n\n' | |
| 143 | + | printf '$ docker context inspect --format '"'"'{{.Endpoints.docker.Host}}'"'"'\n' | |
| 144 | + | printf 'unix:///run/user/1000/docker.sock\n' | |
| 145 | + | nota "Docker rootless se instala aparte y se arranca como usuario;" | |
| 146 | + | nota "si no lo has hecho, tu Docker es rootful aunque no te lo diga la versión." | |
| 147 | + | fi | |
| 148 | + | ||
| 149 | + | # ----------------------------------------------------------------------------- | |
| 150 | + | titulo "Cierre del bloque" | |
| 151 | + | # ----------------------------------------------------------------------------- | |
| 152 | + | printf ' · Dos binarios, dos diseños: sin demonio (Podman) / con demonio (Docker).\n' | |
| 153 | + | printf ' · Rootless se comprueba con %s, no con la versión.\n' "'info'" | |
| 154 | + | printf ' · Y en rootless los puertos los publica un reenviador: ese es el que\n' | |
| 155 | + | printf ' hace que dentro del contenedor veas al mensajero y no al visitante.\n\n' | |
| 156 | + | exit 0 | |
demo-02-correcciones-traefik-y-umami.sh(文件已创建)
| @@ -0,0 +1,163 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # ============================================================================= | |
| 3 | + | # demo-02 · Las correcciones de configuración | |
| 4 | + | # ============================================================================= | |
| 5 | + | # A qué punto del guion sirve: punto 13 | |
| 6 | + | # «Y se puede tratar en Traefik y en Umami» | |
| 7 | + | # | |
| 8 | + | # Qué se ve en pantalla: los dos fragmentos correctos, con la línea que importa | |
| 9 | + | # señalada, y una comprobación de que lo son de verdad. | |
| 10 | + | # 1. Traefik → `config/traefik-entrypoints.yml` | |
| 11 | + | # forwardedHeaders.trustedIPs y proxyProtocol.trustedIPs | |
| 12 | + | # apuntando SOLO al proxy correcto. | |
| 13 | + | # 2. Umami → `config/umami.env` | |
| 14 | + | # CLIENT_IP_HEADER=x-real-ip | |
| 15 | + | # | |
| 16 | + | # La frase que resume las dos: una cabecera que el proxy REESCRIBE frente a una | |
| 17 | + | # cabecera que CUALQUIERA PUEDE ESCRIBIR. | |
| 18 | + | # | |
| 19 | + | # Estado: VERIFICADO en local. El YAML se parsea y el fichero de entorno se | |
| 20 | + | # comprueba con comandos; la salida real está en | |
| 21 | + | # `verificacion/demo-02-salida.txt`. | |
| 22 | + | # | |
| 23 | + | # Seguridad: solo lee los ficheros de `config/`. No hay ningún secreto en ellos | |
| 24 | + | # (el token de Cloudflare, si algún día se usa, no vive aquí: va en un `podman | |
| 25 | + | # secret`). La única dirección de la confianza es la del borde de este montaje | |
| 26 | + | # (loopback); donde se usa un valor de mentira se marca como rango de | |
| 27 | + | # documentación (RFC 5737). | |
| 28 | + | # ============================================================================= | |
| 29 | + | ||
| 30 | + | set -u | |
| 31 | + | ||
| 32 | + | AQUI="$(cd "$(dirname "$0")" && pwd)" | |
| 33 | + | TRAEFIK="$AQUI/config/traefik-entrypoints.yml" | |
| 34 | + | UMAMI="$AQUI/config/umami.env" | |
| 35 | + | ||
| 36 | + | titulo() { printf '\n\033[1m── %s\033[0m\n' "$1"; } | |
| 37 | + | orden() { printf '\n\033[1m$ %s\033[0m\n' "$1"; } | |
| 38 | + | nota() { printf ' ↳ %s\n' "$1"; } | |
| 39 | + | ok() { printf ' \033[1m✔ %s\033[0m\n' "$1"; } | |
| 40 | + | fallo() { printf ' \033[1m✘ %s\033[0m\n' "$1"; } | |
| 41 | + | ||
| 42 | + | rc_final=0 | |
| 43 | + | ||
| 44 | + | printf '\033[1mLas dos correcciones del punto 13\033[0m\n' | |
| 45 | + | ||
| 46 | + | # ----------------------------------------------------------------------------- | |
| 47 | + | titulo "1 · Traefik: de quién se fía el proxy" | |
| 48 | + | # ----------------------------------------------------------------------------- | |
| 49 | + | # El error de partida (lo que se cuenta en voz alta, no hace falta fichero): | |
| 50 | + | # proxyProtocol: trustedIPs: [el rango de tu vecindario de contenedores] | |
| 51 | + | # ← confía en TODO el vecindario: cualquiera | |
| 52 | + | # que alcance el puerto puede contar que | |
| 53 | + | # viene de donde le apetezca | |
| 54 | + | # forwardedHeaders: insecure: true ← se cree cualquier cabecera | |
| 55 | + | # Con eso, cualquiera que alcance el puerto de Traefik entra diciendo que viene de | |
| 56 | + | # donde quiera. | |
| 57 | + | orden "cat config/traefik-entrypoints.yml" | |
| 58 | + | cat "$TRAEFIK" | |
| 59 | + | ||
| 60 | + | # ¿Se parsea? Un YAML roto en el arranque deja el proxy sin servicios. | |
| 61 | + | if command -v python3 >/dev/null 2>&1 && python3 -c 'import yaml' 2>/dev/null; then | |
| 62 | + | orden "python3 -c 'yaml.safe_load(...)' # ¿el YAML es válido y dice lo que creemos?" | |
| 63 | + | python3 - "$TRAEFIK" <<'PY' | |
| 64 | + | import sys, yaml | |
| 65 | + | with open(sys.argv[1]) as f: | |
| 66 | + | cfg = yaml.safe_load(f) | |
| 67 | + | ep = cfg["entryPoints"]["websecure"] | |
| 68 | + | print("address :", ep["address"]) | |
| 69 | + | print("proxyProtocol.trustedIPs :", ep["proxyProtocol"]["trustedIPs"]) | |
| 70 | + | print("forwardedHeaders.insecure :", ep.get("forwardedHeaders", {}).get("insecure", False)) | |
| 71 | + | print("forwardedHeaders.trustedIPs :", ep["forwardedHeaders"]["trustedIPs"]) | |
| 72 | + | PY | |
| 73 | + | ok "YAML válido y con las listas presentes." | |
| 74 | + | else | |
| 75 | + | nota "python3 + PyYAML no disponibles: se salta la comprobación de sintaxis." | |
| 76 | + | fi | |
| 77 | + | ||
| 78 | + | # Auditoría: lo que NO debe haber. Solo miran las líneas ACTIVAS (sin '#'). | |
| 79 | + | orden "grep -vE '^[[:space:]]*#' config/traefik-entrypoints.yml | grep -nE 'insecure: true|/[0-9]{1,2}\"' " | |
| 80 | + | if grep -vE '^[[:space:]]*#' "$TRAEFIK" | grep -E 'insecure: true|/(0|[0-9]|1[0-9]|2[0-3])"'; then | |
| 81 | + | fallo "hay un rango demasiado ancho o un 'insecure' activo." | |
| 82 | + | rc_final=1 | |
| 83 | + | else | |
| 84 | + | ok "ni 'insecure: true' ni rangos anchos activos: solo la IP del borde." | |
| 85 | + | fi | |
| 86 | + | ||
| 87 | + | # La misma auditoría, pero entendiendo el YAML (si hay python3). Es la que manda. | |
| 88 | + | if command -v python3 >/dev/null 2>&1 && python3 -c 'import yaml' 2>/dev/null; then | |
| 89 | + | orden "python3 -c 'auditar el YAML: insecure y prefijos de red'" | |
| 90 | + | if python3 - "$TRAEFIK" <<'PY' | |
| 91 | + | import sys, ipaddress, yaml | |
| 92 | + | ep = yaml.safe_load(open(sys.argv[1]))["entryPoints"]["websecure"] | |
| 93 | + | problemas = [] | |
| 94 | + | for nombre in ("forwardedHeaders", "proxyProtocol"): | |
| 95 | + | bloque = ep.get(nombre) or {} | |
| 96 | + | if bloque.get("insecure"): | |
| 97 | + | problemas.append(f"{nombre}.insecure: true ← se cree a cualquiera") | |
| 98 | + | for cidr in bloque.get("trustedIPs") or []: | |
| 99 | + | red = ipaddress.ip_network(cidr, strict=False) | |
| 100 | + | minimo = 24 if red.version == 4 else 64 | |
| 101 | + | if red.prefixlen < minimo: | |
| 102 | + | problemas.append(f"{nombre}.trustedIPs demasiado ancho: {cidr}") | |
| 103 | + | else: | |
| 104 | + | print(f" {nombre}.trustedIPs → {cidr} (red pequeña, correcto)") | |
| 105 | + | if problemas: | |
| 106 | + | for p in problemas: | |
| 107 | + | print(" ✘", p) | |
| 108 | + | sys.exit(1) | |
| 109 | + | print(" ✔ sin 'insecure' y sin rangos anchos.") | |
| 110 | + | PY | |
| 111 | + | then | |
| 112 | + | ok "el YAML auditado no tiene confianzas excesivas." | |
| 113 | + | else | |
| 114 | + | fallo "el YAML confía en más gente de la que debería." | |
| 115 | + | rc_final=1 | |
| 116 | + | fi | |
| 117 | + | fi | |
| 118 | + | ||
| 119 | + | # ----------------------------------------------------------------------------- | |
| 120 | + | titulo "2 · Umami: qué cabecera lee" | |
| 121 | + | # ----------------------------------------------------------------------------- | |
| 122 | + | # Mismo razonamiento, al revés: aquí no se restringe QUIÉN puede llegar, se elige | |
| 123 | + | # QUÉ cabecera creerse. Se elige la que el proxy reescribe. | |
| 124 | + | orden "cat config/umami.env" | |
| 125 | + | cat "$UMAMI" | |
| 126 | + | ||
| 127 | + | orden "grep -n '^CLIENT_IP_HEADER' config/umami.env" | |
| 128 | + | if grep -q '^CLIENT_IP_HEADER=x-real-ip' "$UMAMI"; then | |
| 129 | + | ok "CLIENT_IP_HEADER=x-real-ip → la cabecera que Traefik reconstruye." | |
| 130 | + | else | |
| 131 | + | fallo "CLIENT_IP_HEADER no apunta a x-real-ip." | |
| 132 | + | rc_final=1 | |
| 133 | + | fi | |
| 134 | + | ||
| 135 | + | orden "grep -nE 'cf-connecting-ip|true-client-ip' config/umami.env # no debe haber ninguna activa" | |
| 136 | + | if grep -E '^[[:space:]]*CLIENT_IP_HEADER=.*(cf-connecting-ip|true-client-ip)' "$UMAMI"; then | |
| 137 | + | fallo "queda una cabecera que cualquiera puede escribir." | |
| 138 | + | rc_final=1 | |
| 139 | + | else | |
| 140 | + | ok "ninguna cabecera de cliente (cf-connecting-ip / true-client-ip) activa." | |
| 141 | + | fi | |
| 142 | + | ||
| 143 | + | # ----------------------------------------------------------------------------- | |
| 144 | + | titulo "Por qué, en una frase (para decir en el aire)" | |
| 145 | + | # ----------------------------------------------------------------------------- | |
| 146 | + | cat <<'TEXTO' | |
| 147 | + | · En el proxy: no te fíes de todos. 'trustedIPs' con la IP exacta del borde; | |
| 148 | + | si te fías del rango entero, cualquiera que llegue te cuenta de dónde viene. | |
| 149 | + | · En la analítica: lee la cabecera que el proxy reescribe (x-real-ip), no una | |
| 150 | + | que pueda rellenar el primero que pase (cf-connecting-ip, x-forwarded-for). | |
| 151 | + | · Y recuerda el límite: esto ordena la casa, no tapa el agujero. Si el 8443 | |
| 152 | + | está abierto al mundo, la fiabilidad la da el firewall, no la cabecera. | |
| 153 | + | · Y ojo con el alcance: esto funcionaba… hasta que un sábado por la tarde el | |
| 154 | + | bloqueo judicial tumbó el rango entero del proxy que había delante. | |
| 155 | + | TEXTO | |
| 156 | + | ||
| 157 | + | printf '\n' | |
| 158 | + | if [ "$rc_final" -eq 0 ]; then | |
| 159 | + | printf '\033[1mResultado: las dos correcciones están en su sitio.\033[0m\n\n' | |
| 160 | + | else | |
| 161 | + | printf '\033[1mResultado: hay algo que corregir (ver arriba).\033[0m\n\n' | |
| 162 | + | fi | |
| 163 | + | exit "$rc_final" | |
demo-03-prueba-del-ciudadano.sh(文件已创建)
| @@ -0,0 +1,206 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # ============================================================================= | |
| 3 | + | # demo-03 · La prueba del ciudadano | |
| 4 | + | # ============================================================================= | |
| 5 | + | # A qué punto del guion sirve: punto 15 | |
| 6 | + | # «Qué puedes hacer tú: prueba, mide y denuncia» | |
| 7 | + | # | |
| 8 | + | # Qué hace: cuando un sitio legítimo te falla, guarda la prueba CON FECHA en un | |
| 9 | + | # fichero fechado. Dominio, operador, hora exacta y el error tal cual. | |
| 10 | + | # · curl → ¿responde? ¿con qué código? ¿cuánto tarda? y el error completo. | |
| 11 | + | # · dig → ¿resuelve? ¿a qué direcciones? ¿con qué servidor de nombres? | |
| 12 | + | # · whois → ¿de quién es esa dirección? (si es de un rango compartido, no se | |
| 13 | + | # ha caído «tu» servidor: se ha caído un vector que usan cientos de | |
| 14 | + | # miles de sitios). Si no hay `whois` instalado, se usa RDAP con | |
| 15 | + | # `curl`, que da la misma información sin instalar nada. | |
| 16 | + | # | |
| 17 | + | # Uso: | |
| 18 | + | # ./demo-03-prueba-del-ciudadano.sh <dominio> [operador] | |
| 19 | + | # ./demo-03-prueba-del-ciudadano.sh tudominio.example "Mi operador" | |
| 20 | + | # | |
| 21 | + | # El informe se guarda en: | |
| 22 | + | # $PRUEBAS_DIR/<AAAA-MM-DD_HHMMSS>-prueba-<dominio>.txt | |
| 23 | + | # (por defecto $PRUEBAS_DIR=/tmp/opencode/pruebas-ciudadano) | |
| 24 | + | # | |
| 25 | + | # Códigos de salida (para que sirva también en un cron o en un script): | |
| 26 | + | # 0 = el sitio responde | |
| 27 | + | # 1 = el sitio NO responde (conexión, TLS o tiempo agotado) | |
| 28 | + | # 2 = el nombre no resuelve | |
| 29 | + | # 3 = no se ha podido escribir el informe | |
| 30 | + | # | |
| 31 | + | # Estado: VERIFICADO en local (cuatro casos: responde, no resuelve, no responde | |
| 32 | + | # y bloqueo detectado). Salidas reales en `verificacion/demo-03-*.txt`. | |
| 33 | + | # | |
| 34 | + | # Seguridad de la demo: solo lectura. Nada de `ssh`, nada de tocar servidores | |
| 35 | + | # ajenos, nada que modifique el sistema. El único tráfico que sale es el de tus | |
| 36 | + | # propias consultas DNS/HTTP, que es justo lo que se está probando. | |
| 37 | + | # ============================================================================= | |
| 38 | + | ||
| 39 | + | set -u | |
| 40 | + | ||
| 41 | + | DOMINIO="${1:-example.com}" | |
| 42 | + | OPERADOR="${2:-${OPERADOR:-(anota aquí tu operador: Movistar, Vodafone, Digi…)}}" | |
| 43 | + | DESTINO="${PRUEBAS_DIR:-/tmp/opencode/pruebas-ciudadano}" | |
| 44 | + | ||
| 45 | + | if [ "$DOMINIO" = "-h" ] || [ "$DOMINIO" = "--help" ]; then | |
| 46 | + | sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//' | |
| 47 | + | exit 0 | |
| 48 | + | fi | |
| 49 | + | ||
| 50 | + | mkdir -p "$DESTINO" || { printf 'no se puede crear %s\n' "$DESTINO" >&2; exit 3; } | |
| 51 | + | FECHA="$(date +%Y-%m-%d_%H%M%S)" | |
| 52 | + | INFORME="$DESTINO/${FECHA}-prueba-${DOMINIO}.txt" | |
| 53 | + | : > "$INFORME" || exit 3 | |
| 54 | + | ||
| 55 | + | ULTIMO_RC=0 | |
| 56 | + | salida() { printf '%s\n' "$*" | tee -a "$INFORME"; } | |
| 57 | + | orden() { printf '\n$ %s\n' "$*" | tee -a "$INFORME"; } | |
| 58 | + | run() { # run <comando…> → lanza, guarda la salida y el código, nunca aborta | |
| 59 | + | local cabecera salida_cmd rc=0 | |
| 60 | + | cabecera="$(printf '$'; for a in "$@"; do | |
| 61 | + | case "$a" in | |
| 62 | + | *" "*) printf " '%s'" "$a" ;; | |
| 63 | + | *) printf ' %s' "$a" ;; | |
| 64 | + | esac | |
| 65 | + | done)" | |
| 66 | + | printf '\n%s\n' "$cabecera" | tee -a "$INFORME" | |
| 67 | + | salida_cmd="$("$@" 2>&1)" || rc=$? | |
| 68 | + | printf '%s\n' "$salida_cmd" | tee -a "$INFORME" | |
| 69 | + | if [ "$rc" -ne 0 ]; then printf '[exit %s]\n' "$rc" | tee -a "$INFORME"; fi | |
| 70 | + | ULTIMO_RC=$rc | |
| 71 | + | return 0 | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | # ------------------------------------------------------------------ cabecera | |
| 75 | + | salida "================================================================" | |
| 76 | + | salida " PRUEBA DE ACCESO — prueba del ciudadano" | |
| 77 | + | salida "================================================================" | |
| 78 | + | salida " Dominio : $DOMINIO" | |
| 79 | + | salida " Operador : $OPERADOR" | |
| 80 | + | salida " Fecha : $(date -Is)" | |
| 81 | + | salida " Equipo : $(hostname) · $(uname -sr)" | |
| 82 | + | salida "================================================================" | |
| 83 | + | salida "" | |
| 84 | + | salida "Nota: esta prueba NO dice quién bloquea ni por qué. Solo deja constancia" | |
| 85 | + | salida "fechada de lo que ha pasado desde tu conexión. Una prueba, o varias, es" | |
| 86 | + | salida "lo que se adjunta a una reclamación." | |
| 87 | + | ||
| 88 | + | # --------------------------------------------------------------- 1 · curl | |
| 89 | + | salida "" | |
| 90 | + | salida "--- 1 · ¿El sitio responde? -------------------------------------" | |
| 91 | + | if command -v curl >/dev/null 2>&1; then | |
| 92 | + | run curl -sS -o /dev/null \ | |
| 93 | + | --connect-timeout 10 --max-time 20 \ | |
| 94 | + | -w 'HTTP %{http_code} · connect %{time_connect}s · TLS %{time_appconnect}s · total %{time_total}s · IP destino %{remote_ip}\n' \ | |
| 95 | + | "https://$DOMINIO/" | |
| 96 | + | RC_CURL=$ULTIMO_RC | |
| 97 | + | orden "curl -v https://$DOMINIO/ # el error tal cual, para adjuntar" | |
| 98 | + | curl -v -o /dev/null --connect-timeout 10 --max-time 20 "https://$DOMINIO/" 2>&1 \ | |
| 99 | + | | tee -a "$INFORME" | |
| 100 | + | else | |
| 101 | + | salida "curl no está instalado: sin esta parte no hay prueba de acceso." | |
| 102 | + | RC_CURL=127 | |
| 103 | + | fi | |
| 104 | + | ||
| 105 | + | # --------------------------------------------------------------- 2 · dig | |
| 106 | + | salida "" | |
| 107 | + | salida "--- 2 · ¿Resuelve el nombre? ------------------------------------" | |
| 108 | + | IP_A="" | |
| 109 | + | if command -v dig >/dev/null 2>&1; then | |
| 110 | + | run dig "$DOMINIO" A | |
| 111 | + | IP_A="$(dig +short A "$DOMINIO" | grep -E '^[0-9.]+$' | head -1)" | |
| 112 | + | run dig +short "$DOMINIO" AAAA | |
| 113 | + | run dig +short "$DOMINIO" NS | |
| 114 | + | if [ -n "$IP_A" ]; then | |
| 115 | + | salida "" | |
| 116 | + | salida "Primera dirección IPv4 anotada: $IP_A" | |
| 117 | + | fi | |
| 118 | + | else | |
| 119 | + | salida "dig no está instalado (paquete bind-utils / dnsutils)." | |
| 120 | + | fi | |
| 121 | + | ||
| 122 | + | # --------------------------------------------------------------- 3 · whois | |
| 123 | + | salida "" | |
| 124 | + | salida "--- 3 · ¿De quién es esa dirección? -----------------------------" | |
| 125 | + | salida "(si es de un rango compartido, no está caído «tu» servidor:" | |
| 126 | + | salida " está caído un vector que usan cientos de miles de sitios)" | |
| 127 | + | if [ -z "$IP_A" ]; then | |
| 128 | + | salida "Sin dirección IPv4 no hay nada que consultar." | |
| 129 | + | elif command -v whois >/dev/null 2>&1; then | |
| 130 | + | run whois "$IP_A" | |
| 131 | + | else | |
| 132 | + | salida "No hay 'whois' instalado en este equipo. Alternativa sin instalar nada:" | |
| 133 | + | salida "RDAP, el registro de direcciones que habla HTTP." | |
| 134 | + | if command -v curl >/dev/null 2>&1; then | |
| 135 | + | orden "curl -sSL https://rdap.org/ip/$IP_A" | |
| 136 | + | RDAP="$(curl -sSL --connect-timeout 10 --max-time 20 "https://rdap.org/ip/$IP_A" 2>&1)" \ | |
| 137 | + | || salida "(la consulta RDAP ha fallado)" | |
| 138 | + | if command -v jq >/dev/null 2>&1 && printf '%s' "$RDAP" | jq -e . >/dev/null 2>&1; then | |
| 139 | + | printf '%s' "$RDAP" | jq -r ' | |
| 140 | + | " titular : " + (.name // .handle // (.entities[0].handle // "?")), | |
| 141 | + | " registro : " + (.handle // "?"), | |
| 142 | + | " país : " + (.country // "?"), | |
| 143 | + | " rango : " + ( | |
| 144 | + | if .["cidr0_cidrs"] then | |
| 145 | + | (.["cidr0_cidrs"][0] | "\(.v4prefix // .v6prefix)/\(.length)") | |
| 146 | + | elif .startAddress then "\(.startAddress) - \(.endAddress)" | |
| 147 | + | else "?" end | |
| 148 | + | ) | |
| 149 | + | ' | tee -a "$INFORME" | |
| 150 | + | else | |
| 151 | + | printf '%s\n' "$RDAP" | head -40 | tee -a "$INFORME" | |
| 152 | + | fi | |
| 153 | + | fi | |
| 154 | + | fi | |
| 155 | + | ||
| 156 | + | # ------------------------------------------------------------- veredicto | |
| 157 | + | salida "" | |
| 158 | + | salida "--- Resultado ---------------------------------------------------" | |
| 159 | + | if [ -n "$IP_A" ] && [ "${RC_CURL:-1}" -eq 0 ]; then | |
| 160 | + | salida " ALCANZABLE: el sitio ha respondido desde esta conexión." | |
| 161 | + | salida " Si aún así a ti te falla a ratos, repite esta prueba cada vez que" | |
| 162 | + | salida " falle: lo que vale es la serie con fecha y hora, no una sola foto." | |
| 163 | + | VEREDICTO=0 | |
| 164 | + | elif [ -z "$IP_A" ]; then | |
| 165 | + | salida " NO RESUELVE EL NOMBRE: el dominio no se ha podido traducir a dirección." | |
| 166 | + | salida " Prueba desde otra red (datos móviles, otra casa) para saber si es" | |
| 167 | + | salida " cosa de tu operador o del dominio." | |
| 168 | + | VEREDICTO=2 | |
| 169 | + | else | |
| 170 | + | salida " NO ALCANZABLE: el nombre resuelve ($IP_A) pero la conexión no llega." | |
| 171 | + | salida " Guarda este fichero: es exactamente la prueba que hay que adjuntar." | |
| 172 | + | VEREDICTO=1 | |
| 173 | + | fi | |
| 174 | + | ||
| 175 | + | # ------------------------------------------------------- qué hacer ahora | |
| 176 | + | cat <<'TEXTO' | tee -a "$INFORME" | |
| 177 | + | ||
| 178 | + | --- Y ahora, los otros dos pasos --------------------------------- | |
| 179 | + | 2 · MEDIR. Instala OONI Probe (libre, sin registro) y lanza la prueba | |
| 180 | + | desde tu propia red: las mediciones se publican solas en OONI | |
| 181 | + | Explorer. Una anomalía sola no prueba nada, pero suma. | |
| 182 | + | App e instrucciones : https://ooni.org/install/ | |
| 183 | + | Mediciones públicas : https://explorer.ooni.org/ | |
| 184 | + | ||
| 185 | + | 3 · DENUNCIAR (esto deja constancia; no hay caso documentado de una | |
| 186 | + | reclamación individual que levante uno de estos bloqueos): | |
| 187 | + | · Tu operador, primero, y pide respuesta por escrito. | |
| 188 | + | · Oficina de Atención al Usuario de Telecomunicaciones (SETELECO): | |
| 189 | + | https://usuariosteleco.digital.gob.es/reclamaciones/telefonia-e-internet/tramitacion-electronica | |
| 190 | + | · AEPD, solo si hubo inspección de comunicaciones o tratamiento | |
| 191 | + | indebido de datos: | |
| 192 | + | https://www.aepd.es/ | |
| 193 | + | · CNMC, como coordinador de servicios digitales (DSA): | |
| 194 | + | https://www.cnmc.es/sectores-que-regulamos/servicios-digitales-dsa/reclamaciones | |
| 195 | + | ||
| 196 | + | Guarda también: capturas con la fecha visible, tu operador y, si puedes, | |
| 197 | + | la misma prueba desde otra conexión. | |
| 198 | + | TEXTO | |
| 199 | + | ||
| 200 | + | salida "" | |
| 201 | + | salida "Informe guardado en: $INFORME" | |
| 202 | + | # Sin códigos ANSI a propósito: la salida se guarda tal cual y se puede pegar en | |
| 203 | + | # un informe o un tique de incidencia sin que aparezcan escapes de color. | |
| 204 | + | printf '\nInforme guardado en: %s\n' "$INFORME" | |
| 205 | + | printf 'Código de salida: %s (0 responde · 1 no responde · 2 no resuelve)\n\n' "$VEREDICTO" | |
| 206 | + | exit "$VEREDICTO" | |
demo-04-borde-haproxy.sh(文件已创建)
| @@ -0,0 +1,210 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # ============================================================================= | |
| 3 | + | # demo-04 · El borde con HAProxy | |
| 4 | + | # ============================================================================= | |
| 5 | + | # A qué punto del guion sirve: puntos 16, 17, 18 y 19 | |
| 6 | + | # · 16 «HAProxy en bare metal» → el borde corre en el HOST, | |
| 7 | + | # fuera del contenedor. | |
| 8 | + | # · 17 «Directo al 443» → el puerto privilegiado lo abre | |
| 9 | + | # HAProxy, en el metal. | |
| 10 | + | # · 18 «Traefik se levanta en el 8443» → el contenedor solo publica un | |
| 11 | + | # puerto alto. | |
| 12 | + | # · 19 «Y no necesito reglas especiales» → sin ip_unprivileged_port_start, | |
| 13 | + | # sin setcap, sin iptables. | |
| 14 | + | # | |
| 15 | + | # Qué se ve en pantalla: | |
| 16 | + | # 1. `config/haproxy.cfg` entero, con las tres líneas clave señaladas. | |
| 17 | + | # 2. El recorrido: :443 → HAProxy → `send-proxy-v2` → Traefik :8443. | |
| 18 | + | # 3. La validación de sintaxis de verdad, con `haproxy -c` y un control | |
| 19 | + | # negativo (un fichero roto a propósito) para demostrar que el chequeo | |
| 20 | + | # no dice «OK» a todo. | |
| 21 | + | # | |
| 22 | + | # Estado: VERIFICADO en local. La validación se ejecuta con HAProxy 3.4.7 | |
| 23 | + | # dentro de un contenedor EFÍMERO (`--rm`, sin puertos publicados y sin tocar | |
| 24 | + | # el host). Salida real en `verificacion/demo-04-salida.txt`. | |
| 25 | + | # En esta máquina NO hay binario `haproxy` ni `docker` instalados: por eso la | |
| 26 | + | # validación va por contenedor y ese camino es el que está verificado. | |
| 27 | + | # | |
| 28 | + | # Si no hay ni `haproxy` ni `podman` ni `docker`, el script NO falla: avisa de | |
| 29 | + | # que la sintaxis queda sin validar y muestra el comando exacto para hacerlo. | |
| 30 | + | # ============================================================================= | |
| 31 | + | ||
| 32 | + | set -u | |
| 33 | + | ||
| 34 | + | AQUI="$(cd "$(dirname "$0")" && pwd)" | |
| 35 | + | CFG="$AQUI/config/haproxy.cfg" | |
| 36 | + | # Imagen del validador. Se mira primero la caché local y, si no hay ninguna, se | |
| 37 | + | # descarga. Son las mismas versiones que el binario oficial de HAProxy: | |
| 38 | + | # · docker.io/library/haproxy:lts-alpine → la canónica | |
| 39 | + | # · mirror.gcr.io/library/haproxy:lts-alpine → espejo público (el Hub devolvió | |
| 40 | + | # error de autenticación al preparar este material, así que el espejo es lo | |
| 41 | + | # que acabó funcionando) | |
| 42 | + | # · docker.io/library/haproxy:latest → por si ya la tienes por casa | |
| 43 | + | CANDIDATAS=( | |
| 44 | + | "docker.io/library/haproxy:lts-alpine" | |
| 45 | + | "mirror.gcr.io/library/haproxy:lts-alpine" | |
| 46 | + | "docker.io/library/haproxy:latest" | |
| 47 | + | ) | |
| 48 | + | IMAGEN="${HAPROXY_IMAGEN:-${CANDIDATAS[0]}}" | |
| 49 | + | TMP="${TMPDIR:-/tmp/opencode}" | |
| 50 | + | ||
| 51 | + | resolver_imagen() { # elige la primera imagen disponible (caché primero, luego descarga) | |
| 52 | + | local img | |
| 53 | + | for img in "${CANDIDATAS[@]}"; do | |
| 54 | + | if podman image exists "$img" >/dev/null 2>&1; then IMAGEN="$img"; return 0; fi | |
| 55 | + | done | |
| 56 | + | for img in "${CANDIDATAS[@]}"; do | |
| 57 | + | printf ' (descargando %s…)\n' "$img" | |
| 58 | + | if podman pull "$img" >/dev/null 2>&1; then IMAGEN="$img"; return 0; fi | |
| 59 | + | done | |
| 60 | + | return 1 | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | titulo() { printf '\n\033[1m── %s\033[0m\n' "$1"; } | |
| 64 | + | orden() { printf '\n\033[1m$ %s\033[0m\n' "$1"; } | |
| 65 | + | nota() { printf ' ↳ %s\n' "$1"; } | |
| 66 | + | ok() { printf ' \033[1m✔ %s\033[0m\n' "$1"; } | |
| 67 | + | fallo() { printf ' \033[1m✘ %s\033[0m\n' "$1"; } | |
| 68 | + | ||
| 69 | + | rc_final=0 | |
| 70 | + | ||
| 71 | + | printf '\033[1mEl borde en bare metal\033[0m (puntos 16, 17, 18 y 19)\n' | |
| 72 | + | ||
| 73 | + | # ----------------------------------------------------------------------------- | |
| 74 | + | titulo "1 · El fichero del borde" | |
| 75 | + | # ----------------------------------------------------------------------------- | |
| 76 | + | orden "cat config/haproxy.cfg" | |
| 77 | + | cat "$CFG" | |
| 78 | + | ||
| 79 | + | # ----------------------------------------------------------------------------- | |
| 80 | + | titulo "2 · Las cuatro líneas que sostienen el diseño" | |
| 81 | + | # ----------------------------------------------------------------------------- | |
| 82 | + | orden "grep -nE 'mode tcp|bind \\*:443|send-proxy-v2|127.0.0.1:8443' config/haproxy.cfg" | |
| 83 | + | grep -nE 'mode tcp|bind \*:443|send-proxy-v2|127.0.0.1:8443' "$CFG" | |
| 84 | + | nota "'bind *:443' → el puerto privilegiado lo abre el HOST (punto 17)." | |
| 85 | + | nota "'mode tcp' → no descifra: pasa la conexión tal cual." | |
| 86 | + | nota "'send-proxy-v2' → avisa al siguiente salto de quién llama." | |
| 87 | + | nota "'127.0.0.1:8443' → Traefik, en rootless, en un puerto alto (punto 18)." | |
| 88 | + | ||
| 89 | + | # Comprobación de que están de verdad (no solo de que el grep los encuentre). | |
| 90 | + | for patron in 'bind \*:443' 'send-proxy-v2' '127\.0\.0\.1:8443'; do | |
| 91 | + | if grep -qE "$patron" "$CFG"; then | |
| 92 | + | ok "presente: $patron" | |
| 93 | + | else | |
| 94 | + | fallo "falta: $patron" | |
| 95 | + | rc_final=1 | |
| 96 | + | fi | |
| 97 | + | done | |
| 98 | + | ||
| 99 | + | # ----------------------------------------------------------------------------- | |
| 100 | + | titulo "3 · Los tres apaños que NO hacen falta (punto 19)" | |
| 101 | + | # ----------------------------------------------------------------------------- | |
| 102 | + | orden "grep -nE 'ip_unprivileged_port_start|setcap|iptables|nftables' config/haproxy.cfg # solo aparecen en los comentarios" | |
| 103 | + | if grep -nE 'ip_unprivileged_port_start|setcap|iptables|nftables' "$CFG"; then | |
| 104 | + | nota "aparecen mencionados en los comentarios, para explicar por qué no se usan;" | |
| 105 | + | nota "ninguno está como directiva activa (en HAProxy no existen como directivas)." | |
| 106 | + | else | |
| 107 | + | nota "ni siquiera se mencionan." | |
| 108 | + | fi | |
| 109 | + | nota "El motivo, en una frase: el contenedor no abre ningún puerto < 1024," | |
| 110 | + | nota "así que no hay nada que habilitar dentro del espacio de nombres." | |
| 111 | + | ||
| 112 | + | # ----------------------------------------------------------------------------- | |
| 113 | + | titulo "4 · Validar la sintaxis con HAProxy de verdad" | |
| 114 | + | # ----------------------------------------------------------------------------- | |
| 115 | + | validar() { # validar <fichero> → 0 si HAProxy acepta la configuración | |
| 116 | + | if command -v haproxy >/dev/null 2>&1; then | |
| 117 | + | haproxy -c -f "$1" | |
| 118 | + | elif command -v podman >/dev/null 2>&1; then | |
| 119 | + | podman run --rm -v "$1:/usr/local/etc/haproxy/haproxy.cfg:ro" \ | |
| 120 | + | "$IMAGEN" haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg | |
| 121 | + | elif command -v docker >/dev/null 2>&1; then | |
| 122 | + | docker run --rm -v "$1:/usr/local/etc/haproxy/haproxy.cfg:ro" \ | |
| 123 | + | "$IMAGEN" haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg | |
| 124 | + | else | |
| 125 | + | return 127 | |
| 126 | + | fi | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | if command -v haproxy >/dev/null 2>&1; then | |
| 130 | + | orden "haproxy -v" | |
| 131 | + | haproxy -v | head -2 | |
| 132 | + | VALIDADOR="haproxy (binario del sistema)" | |
| 133 | + | elif command -v podman >/dev/null 2>&1; then | |
| 134 | + | [ -n "${HAPROXY_IMAGEN:-}" ] || resolver_imagen || true | |
| 135 | + | if podman run --rm "$IMAGEN" haproxy -v >/dev/null 2>&1; then | |
| 136 | + | orden "podman run --rm $IMAGEN haproxy -v" | |
| 137 | + | podman run --rm "$IMAGEN" haproxy -v 2>&1 | head -2 | |
| 138 | + | VALIDADOR="HAProxy en contenedor efímero (podman)" | |
| 139 | + | nota "El contenedor se borra solo (--rm): no deja nada en el host." | |
| 140 | + | nota "Imagen usada: $IMAGEN" | |
| 141 | + | else | |
| 142 | + | VALIDADOR="" | |
| 143 | + | fi | |
| 144 | + | elif command -v docker >/dev/null 2>&1; then | |
| 145 | + | if docker run --rm "$IMAGEN" haproxy -v >/dev/null 2>&1; then | |
| 146 | + | orden "docker run --rm $IMAGEN haproxy -v" | |
| 147 | + | docker run --rm "$IMAGEN" haproxy -v 2>&1 | head -2 | |
| 148 | + | VALIDADOR="HAProxy en contenedor efímero (docker)" | |
| 149 | + | else | |
| 150 | + | VALIDADOR="" | |
| 151 | + | fi | |
| 152 | + | else | |
| 153 | + | VALIDADOR="" | |
| 154 | + | fi | |
| 155 | + | ||
| 156 | + | if [ -n "$VALIDADOR" ]; then | |
| 157 | + | orden "haproxy -c -f config/haproxy.cfg" | |
| 158 | + | if validar "$CFG"; then | |
| 159 | + | ok "configuración válida (exit 0). Validador: $VALIDADOR" | |
| 160 | + | else | |
| 161 | + | fallo "HAProxy NO acepta la configuración." | |
| 162 | + | rc_final=1 | |
| 163 | + | fi | |
| 164 | + | ||
| 165 | + | # --- control negativo: si el chequeo dijera «OK» a todo, no valdría nada --- | |
| 166 | + | orden "control negativo: una copia con 'send-proxy-v9' (no existe)" | |
| 167 | + | ROTO="$(mktemp "$TMP/haproxy-roto-XXXXXX.cfg")" | |
| 168 | + | sed 's/send-proxy-v2/send-proxy-v9/' "$CFG" > "$ROTO" | |
| 169 | + | if validar "$ROTO" >/dev/null 2>&1; then | |
| 170 | + | fallo "ha aceptado una directiva inventada: la validación no vale." | |
| 171 | + | rc_final=1 | |
| 172 | + | else | |
| 173 | + | ok "la copia rota se rechaza → el chequeo es real (exit != 0)." | |
| 174 | + | fi | |
| 175 | + | rm -f "$ROTO" | |
| 176 | + | else | |
| 177 | + | nota "No hay 'haproxy', ni 'podman', ni 'docker' en esta máquina:" | |
| 178 | + | nota "la sintaxis queda SIN VALIDAR. Para validarla, en cualquier equipo:" | |
| 179 | + | nota " podman run --rm -v \"\$PWD/config/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro\" \\" | |
| 180 | + | nota " $IMAGEN haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg" | |
| 181 | + | fi | |
| 182 | + | ||
| 183 | + | # ----------------------------------------------------------------------------- | |
| 184 | + | titulo "5 · El recorrido, para contarlo en voz alta" | |
| 185 | + | # ----------------------------------------------------------------------------- | |
| 186 | + | cat <<'TEXTO' | |
| 187 | + | visitante ──:443──► HAProxy (HOST, servicio del sistema) | |
| 188 | + | │ mode tcp: no descifra nada | |
| 189 | + | │ send-proxy-v2: antepone quién llama | |
| 190 | + | ▼ | |
| 191 | + | Traefik :8443 (rootless, en su contenedor) | |
| 192 | + | │ proxyProtocol.trustedIPs: solo ese borde | |
| 193 | + | │ forwardedHeaders: lee la IP de verdad | |
| 194 | + | ▼ | |
| 195 | + | tu web, tus logs y la analítica | |
| 196 | + | ||
| 197 | + | · El 443 no lo pelea el contenedor: lo abre el host. Por eso no hacen falta | |
| 198 | + | ip_unprivileged_port_start, ni setcap, ni reglas de iptables. | |
| 199 | + | · Sin certificados duplicados: HAProxy no termina TLS, solo lo atraviesa. | |
| 200 | + | · Cada cosa en la capa que le toca: el puerto en el metal, el enrutado en el | |
| 201 | + | contenedor rootless. | |
| 202 | + | TEXTO | |
| 203 | + | ||
| 204 | + | printf '\n' | |
| 205 | + | if [ "$rc_final" -eq 0 ]; then | |
| 206 | + | printf '\033[1mResultado: el diseño del borde está completo y validado.\033[0m\n\n' | |
| 207 | + | else | |
| 208 | + | printf '\033[1mResultado: hay algo que revisar (ver arriba).\033[0m\n\n' | |
| 209 | + | fi | |
| 210 | + | exit "$rc_final" | |