censar: la sonda de dominios dejó a gioser incomunicado — «sólo lee» era falso

Censar gioser en `--local` disparó 29 peticiones HTTPS a sus propios dominios. Varios devuelven 502,
y su `fail2ban` (jaula `caddy-backend-down`) **baneó la propia IP de la máquina**: gioser dejó de
poder hablar con su propio gitea y los `git push` empezaron a fallar con «Could not read from remote
repository». Un censo que deja la máquina incomunicada no es de sólo lectura.

**La frase del script era la mentira**: «sólo lecturas, no se toca nada». No escribe nada, cierto —
pero leer POR RED tiene efectos. Corregida.

Tres arreglos:

1. **La sonda no se hace contra uno mismo.** Por defecto sólo con `--host`; en `--local` se saltea y
   se avisa por qué, con `--probe` para forzarlo. Además es metodológicamente mejor: lo que interesa
   es qué ve EL MUNDO, no qué ve la máquina de sí misma.
2. **Espaciada** medio segundo entre dominios, para no parecerle un escaneo al fail2ban del objetivo.
3. Clase nueva `sin-sondear`, para que un dominio sin datos no se confunda con un fósil.

Desbaneado 204.168.193.248 de `caddy-backend-down`. Y queda la regla: **antes de sondear una máquina,
pensar qué defensa propia le estás disparando.**

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
Sergio
2026-09-11 14:53:05 +00:00
co-authored by Claude Opus 5
parent 95c16097e4
commit 5d1120e583
+38 -4
View File
@@ -1,6 +1,9 @@
#!/usr/bin/env python3
"""censar.py — CENSO de una máquina: qué corre, qué sirve, qué datos tiene. Sin IA, sin adivinar.
⚠ «Sólo lee» hay que decirlo con cuidado: NO ESCRIBE nada, pero las sondas de RED dejan huella. Ver
`sondear_dominio` — censar gioser contra sí mismo hizo que su propio fail2ban lo baneara.
Primer paso de la mudanza (SDD 29). Produce un inventario TOML que un humano revisa y que las etapas
siguientes consumen. NO toca nada: todo lo que hace son lecturas.
@@ -159,7 +162,18 @@ def dominios(t):
def sondear_dominio(d, ips_maquina):
"""DNS + HTTP desde AFUERA (desde donde corre el censo). Determinista, sin juicio."""
"""DNS + HTTP desde donde corre el censo. Determinista, sin juicio.
⚠⚠ **NO se hace contra la máquina que uno está censando desde ella misma.** «Sólo lee» era falso:
leer POR RED tiene efectos. Medido el 2026-09-11 — censar gioser en modo `--local` disparó 29
peticiones HTTPS a sus propios dominios, varios devuelven 502, y su `fail2ban` (jaula
`caddy-backend-down`) **baneó la propia IP de la máquina**. Resultado: gioser dejó de poder hablar
con su propio gitea y los `git push` empezaron a fallar con «Could not read from remote
repository». Un censo que deja la máquina incomunicada no es de sólo lectura.
Y además es metodológicamente mejor: lo que interesa es **qué ve el mundo**, no qué ve la máquina
de sí misma. Por eso la sonda se hace desde donde corre el censo —el operador o la caja nueva— y
se SALTA cuando se censa en local, salvo `--probe` explícito."""
ip = ""
try:
r = subprocess.run(["getent", "ahostsv4", d], capture_output=True, text=True, timeout=10)
@@ -388,7 +402,7 @@ def resumen(c):
pd = {}
for d in c["dominios"]:
pd.setdefault(d["class"], []).append(d["domain"])
for cl in ("vivo", "ya-mudado", "backend-caido", "fosil-sin-dns"):
for cl in ("vivo", "ya-mudado", "backend-caido", "fosil-sin-dns", "sin-sondear"):
n = pd.get(cl, [])
if n:
print(f" {cl:16} {len(n):3} {' '.join(sorted(n)[:6])}{'' if len(n) > 6 else ''}")
@@ -406,6 +420,10 @@ def main():
ap.add_argument("--key", help="clave SSH")
ap.add_argument("--local", action="store_true", help="censar esta máquina")
ap.add_argument("--out", help="fichero TOML de salida (default: stdout sólo el resumen)")
ap.add_argument("--probe", action="store_true",
help="sondear los dominios por DNS+HTTP. Por defecto SÓLO con --host: hacerlo "
"contra uno mismo puede disparar el fail2ban de la propia máquina")
ap.add_argument("--no-probe", action="store_true", help="no sondear dominios")
ap.add_argument("--data-root", action="append", default=[],
help="raíz de datos a medir (repetible). Default: las habituales")
a = ap.parse_args()
@@ -413,14 +431,30 @@ def main():
ap.error("hace falta --host o --local")
t = Objetivo(a.host, a.key)
print(" censando… (sólo lecturas, no se toca nada)", file=sys.stderr)
print(" censando… (no se ESCRIBE nada; las sondas de red sí dejan huella: ver --probe)",
file=sys.stderr)
ident = identidad(t)
vivos = procesos_vivos(t)
decl = declarados(t)
svc, huerfanos = clasificar(vivos, decl)
pts = puertos(t)
doms = [sondear_dominio(d, set(ident["ips"])) for d in dominios(t)]
nombres = dominios(t)
sondear = (a.probe or bool(a.host)) and not a.no_probe
if nombres and not sondear:
print(f"{len(nombres)} dominio(s) declarados, SIN sondear: censando en local, y pedirle",
file=sys.stderr)
print(" a la máquina sus propios dominios puede disparar su fail2ban y dejarla",
file=sys.stderr)
print(" incomunicada (pasó). Sondealos desde otra máquina, o forzá con --probe.",
file=sys.stderr)
doms = [{"domain": d, "dns": "", "http": "", "class": "sin-sondear"} for d in nombres]
else:
doms = []
for i, d in enumerate(nombres):
if i:
time.sleep(0.5) # no parecer un escaneo ante el propio fail2ban del objetivo
doms.append(sondear_dominio(d, set(ident["ips"])))
raices = a.data_root or ["/var/www", "/var/lib", "/srv", "/opt", "/home", "/store"]
dat = datos(t, raices)
censo = {