Files
hechakuaa_bot/DEPLOY.md
markosbenitez fa72ced5a6 security: hardening contra 7 vectores de ataque identificados
Análisis ofensivo → defensas implementadas:

1. /health/history sin auth (CRÍTICO)
   - Requiere Bearer token (HEALTH_AUTH_TOKEN en .env)
   - chat_ids enmascarados como hash SHA256[:8] opaco ("usr_a3b4c5d6")
   - Accesos denegados loggeados con IP + User-Agent

2. /health revela inteligencia operacional (MEDIO)
   - Sin auth: solo {"status", "timestamp"} — sin conteo de tenants ni timing
   - Con auth: respuesta completa con métricas internas
   - Misma URL, auth desbloquea detalle

3. Variables sensibles en proceso env (CRÍTICO)
   - Después de cargar Settings(), se limpian FERNET_KEY, TELEGRAM_BOT_TOKEN,
     PJ_USUARIO_ENC, PJ_PASSWORD_ENC, WEBHOOK_SECRET, HEALTH_AUTH_TOKEN, INVITE_CODE
   - Protege contra /proc/<pid>/environ y herencia por subprocesos

4. Race condition TOCTOU en invite codes (MEDIO)
   - SELECT+UPDATE reemplazado por UPDATE WHERE usado=0
   - rowcount=0 → False; imposible que dos requests simultáneos consuman el mismo código

5. Timing oracle en invite codes (BAJO-MEDIO)
   - Delay mínimo de 800ms en _paso_codigo() independientemente del resultado
   - secrets.compare_digest() para comparación del código del .env

6. Information disclosure en respuestas a no-registrados (BAJO)
   - _rechazar_no_registrado: "Para usar este bot escribí /start." (sin revelar comandos)
   - handle_texto_libre: silencio total para usuarios sin registro (no confirmar que el bot existe)

7. Nginx rate limiting (DEPLOY.md)
   - limit_req_zone pedrito_webhook: 30r/s, burst=50
   - limit_req_zone pedrito_health: 10r/m, burst=5
   - Headers de seguridad: X-Content-Type-Options, X-Frame-Options

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 08:06:36 -03:00

37 KiB
Raw Blame History

Guía de despliegue — Pedrito Hechakuaa en VPS Ubuntu

Destinatario: equipo técnico de InQuality
Entorno objetivo: VPS Ubuntu 22.04 en Hostinger — producción
Tiempo estimado: 6090 minutos (primera vez)
Acceso requerido: SSH root al VPS + datos de configuración provistos por Marcos
Subdominio objetivo: pedrito.inqualityhq.com


Índice

PARTE A — Preparación (antes de tocar el servidor)

  1. Datos que necesitás antes de empezar
  2. Configurar DNS en Bluehost

PARTE B — Servidor: base 3. Conectarse al VPS 4. Actualizar el sistema y crear usuario 5. Instalar Docker 6. Instalar Nginx 7. Clonar el repositorio 8. Configurar el archivo .env 9. Cifrar las credenciales del PJ

PARTE C — HTTPS y producción 10. Obtener certificado SSL con Certbot 11. Configurar Nginx como reverse proxy 12. Configurar el firewall 13. Iniciar el bot con Docker 14. Activar el modo webhook

PARTE D — Operación 15. Verificación final 16. Monitoreo y logs 17. Health check HTTP 18. Cómo actualizar el bot 19. Troubleshooting 20. Referencia rápida de comandos


1. Datos que necesitás antes de empezar

Antes de conectarte al VPS, juntá estos datos. Sin alguno de ellos el deploy no puede completarse.

# Dato Quién lo tiene Dónde va
1 FERNET_KEY (clave de cifrado) Marcos la genera .env
2 TELEGRAM_BOT_TOKEN Marcos (de @BotFather) .env
3 TELEGRAM_CHAT_ID Marcos (de @userinfobot) .env
4 TELEGRAM_EXTRA_IDS Marcos (chat IDs de otros abogados, separados por coma) .env
5 INVITE_CODE Marcos lo decide .env
6 Cédula del PJ Marcos Script de cifrado
7 Contraseña del PJ Marcos Script de cifrado
8 IP pública del VPS Panel de Hostinger DNS + SSH
9 Credenciales SSH del VPS Panel de Hostinger Terminal

Tip: Pedile a Marcos que te envíe los datos 1-5 por un canal seguro (Signal, Bitwarden Send, etc.). Nunca por email ni WhatsApp.

⚠️ Seguridad crítica: Nunca guardés la contraseña del PJ (dato 7) en ningún archivo, chat, nota, ni Post-it. El script del paso 9 la pide en pantalla, la descarta de memoria y guarda solo el resultado cifrado. Una vez que el script termina exitosamente, podés olvidarla.


2. Configurar DNS en Bluehost

El bot va a ser accesible en pedrito.inqualityhq.com. Para eso necesitamos apuntar ese subdominio a la IP del VPS con un registro DNS tipo A.

Necesitás: La IP pública del VPS (Panel de Hostinger → tu VPS → ver IP).


Opción A: Hacerlo vos mismo en Bluehost cPanel

  1. Entrá a bluehost.com con las credenciales de InQuality

  2. En el panel, buscá "cPanel" y hacé click

  3. Dentro de cPanel, en la sección "Domains", hacé click en "Zone Editor" (también puede aparecer como "DNS Zone Editor" o "Advanced DNS Editor")

  4. Buscá inqualityhq.com en la lista y hacé click en "Manage"

  5. Hacé click en "Add Record"

  6. Completá el formulario así:

    Campo Valor
    Name (o Host) pedrito
    Type A
    TTL 300 (o el mínimo disponible)
    Value (o Points To) <IP_DE_TU_VPS>
  7. Hacé click en "Save" o "Add Record"

  8. Para verificar que se propagó (puede tardar de 5 minutos a 2 horas):

    # Desde tu computadora local
    nslookup pedrito.inqualityhq.com
    # Debe mostrar la IP del VPS en la respuesta
    
    # O también:
    dig pedrito.inqualityhq.com A +short
    # Debe retornar solo la IP del VPS
    

Tip: Si nslookup aún no muestra la IP, esperá 10-15 minutos y volvé a intentar. La propagación en Bluehost suele ser rápida, pero a veces tarda más.


Opción B: Mensaje exacto para pedirle a un asistente de Bluehost (en inglés)

Si necesitás que un asistente del equipo (que tenga acceso al cPanel de Bluehost) lo haga por vos, podés enviarle exactamente este mensaje:


Hi,

I need to add a DNS A record to the inqualityhq.com domain managed through Bluehost cPanel. Could you please add the following record?

  • Domain: inqualityhq.com
  • Record type: A
  • Host/Name: pedrito (this will create pedrito.inqualityhq.com)
  • Value/Points to: <IP_DEL_VPS> (replace with the actual VPS IP)
  • TTL: 300 seconds (minimum available)

Once added, please confirm so I can verify propagation. If you need me to confirm with the account holder, let me know.

Thank you!


Reemplazá <IP_DEL_VPS> con la IP real del VPS antes de enviar el mensaje.


3. Conectarse al VPS

Desde tu computadora local, abrí una terminal:

ssh root@<IP_DE_LA_VPS>

Si tenés clave SSH configurada en Hostinger (recomendado):

ssh -i ~/.ssh/id_rsa root@<IP_DE_LA_VPS>

Si te pide contraseña, es la del panel de Hostinger.

Tip para Windows: Usá Windows Terminal o PuTTY. En Windows Terminal el comando SSH es el mismo.

Tip: Si es la primera conexión, va a preguntar:

Are you sure you want to continue connecting (yes/no/[fingerprint])?

Escribí yes y Enter.


4. Actualizar el sistema y crear usuario

Todos estos comandos se ejecutan como root.

4.1 Actualizar paquetes

apt update && apt upgrade -y

Esto puede tardar 2-5 minutos. Esperá que termine.

4.2 Verificar la versión de Ubuntu

lsb_release -a

Salida esperada: Ubuntu 22.04.x LTS. Si es otra versión, avisale a Marcos antes de continuar.

4.3 Crear el usuario pedrito

El bot no debe correr como root. Le creamos un usuario propio:

# Crear el usuario (sin contraseña de login interactivo)
adduser pedrito --disabled-password --gecos ""

# Verificar que se creó
id pedrito

Salida esperada:

uid=1001(pedrito) gid=1001(pedrito) groups=1001(pedrito)

¿Por qué UID 1001 y no 1000? En Ubuntu el usuario 1000 suele ser el primer usuario creado durante la instalación. En un VPS de Hostinger puede o no existir. Lo importante es que pedrito tenga su propio UID.

4.4 Permitir que pedrito use Docker (sin sudo)

# Esto lo hacemos después de instalar Docker (paso 5), pero lo dejamos acá como referencia
# usermod -aG docker pedrito   ← lo corremos en el paso 5.3

4.5 Configurar acceso SSH para pedrito (opcional pero recomendado)

mkdir -p /home/pedrito/.ssh
cp /root/.ssh/authorized_keys /home/pedrito/.ssh/
chown -R pedrito:pedrito /home/pedrito/.ssh
chmod 700 /home/pedrito/.ssh
chmod 600 /home/pedrito/.ssh/authorized_keys

Para verificar: en una segunda terminal, intentá ssh pedrito@<IP>. Si entra sin contraseña, perfecto.


5. Instalar Docker

Docker es el motor de contenedores que va a correr el bot de forma aislada y reproducible.

5.1 Instalar dependencias previas

apt install -y \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

5.2 Agregar el repositorio oficial de Docker

# Crear directorio para las claves GPG
install -m 0755 -d /etc/apt/keyrings

# Descargar e instalar la clave GPG de Docker
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
    gpg --dearmor -o /etc/apt/keyrings/docker.gpg

chmod a+r /etc/apt/keyrings/docker.gpg

# Agregar el repositorio
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | \
  tee /etc/apt/sources.list.d/docker.list > /dev/null

# Actualizar lista de paquetes
apt update

5.3 Instalar Docker Engine y docker-compose

apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Verificar que Docker se instaló correctamente
docker --version
# Salida esperada: Docker version 24.x.x o mayor

docker compose version
# Salida esperada: Docker Compose version v2.x.x

5.4 Agregar a pedrito al grupo docker

# Esto permite que pedrito corra docker sin sudo
usermod -aG docker pedrito

# Verificar
groups pedrito
# Debe incluir "docker" en la lista

5.5 Verificar que Docker funciona

docker run --rm hello-world

Si imprime Hello from Docker!, Docker está funcionando.

¿Qué es un contenedor? Pensalo como una "caja" cerrada que incluye el código del bot y todas sus dependencias. Si algo falla dentro de la caja, no afecta al resto del servidor. docker compose up abre la caja, docker compose down la cierra.


6. Instalar Nginx

Nginx actúa como intermediario entre internet y el bot. Recibe las conexiones HTTPS (puerto 443) y las reenvía internamente al bot (puerto 8080). Esto permite tener SSL sin que el bot lo maneje.

# Instalar Nginx
apt install -y nginx

# Verificar que arrancó
systemctl status nginx

Salida esperada:

● nginx.service - A high performance web server and reverse proxy server
     Active: active (running) since ...

Si dice inactive, arrancalo manualmente:

systemctl start nginx
systemctl enable nginx   # para que arranque en cada reboot

7. Clonar el repositorio

A partir de acá trabajamos como el usuario pedrito:

# Cambiar al usuario pedrito
su - pedrito

# Verificar que estamos en el home correcto
pwd
# Debe mostrar: /home/pedrito

# Clonar el repositorio
git clone https://github.com/InqGit/hechakuaa_bot.git

# Entrar al directorio
cd hechakuaa_bot

# Verificar que los archivos están presentes
ls -la

Deberías ver archivos como: main.py, config.py, telegram_bot.py, Dockerfile, docker-compose.yml, migrations/, etc.

Crear el directorio de datos

# Este directorio persiste entre reinicios del bot (DB, snapshots)
mkdir -p /home/pedrito/hechakuaa_bot/data

Salir de vuelta a root para el siguiente paso:

exit
# Ahora estás como root de nuevo

Ajustar permisos del directorio data para Docker

El contenedor Docker corre con usuario UID 1000. El directorio data/ necesita ser escribible por ese UID:

chown -R 1000:1000 /home/pedrito/hechakuaa_bot/data

# Verificar
ls -la /home/pedrito/hechakuaa_bot/data
# Debe mostrar: drwxr-xr-x 2 1000 1000 ... data

¿Por qué 1000 y no pedrito? Dentro del contenedor Docker existe un usuario con UID 1000. El servidor no sabe que ese usuario se llama "pedrito" — solo entiende el número UID. En el sistema anfitrión, UID 1000 puede corresponder a un usuario diferente, pero Docker solo necesita que el directorio sea escribible por ese número.


8. Configurar el archivo .env

El archivo .env contiene toda la configuración sensible del bot. Se crea directamente en el VPS y nunca se sube al repositorio ni se copia entre máquinas.

# Como pedrito:
su - pedrito
cd /home/pedrito/hechakuaa_bot

# Crear .env a partir del template
cp .env.example .env

# Editar (nano es el editor más simple)
nano .env

Guía de campos del .env

Completá los campos marcados con ← COMPLETAR. Los demás se pueden dejar con sus valores por defecto:

# ── CIFRADO ──────────────────────────────────────────────────────────────────
FERNET_KEY=                    # ← COMPLETAR (Marcos te la da o la generás abajo)

# ── CREDENCIALES PJ (se completan en el paso 9) ───────────────────────────────
PJ_USUARIO_ENC=                # ← se completa automáticamente con el script de cifrado
PJ_PASSWORD_ENC=               # ← se completa automáticamente con el script de cifrado

# ── TELEGRAM ─────────────────────────────────────────────────────────────────
TELEGRAM_BOT_TOKEN=            # ← COMPLETAR (de @BotFather)
TELEGRAM_CHAT_ID=              # ← COMPLETAR (chat ID de Marcos)
TELEGRAM_EXTRA_IDS=            # ← COMPLETAR si hay más abogados (ej: 111222333,444555666)

# ── INVITACIÓN ────────────────────────────────────────────────────────────────
INVITE_CODE=                   # ← COMPLETAR (código maestro ilimitado)

# ── WEBHOOK (completar después del paso 14) ───────────────────────────────────
WEBHOOK_URL=                   # ← dejar vacío por ahora, completar en el paso 14
WEBHOOK_SECRET=                # ← dejar vacío por ahora, completar en el paso 14

# ── HEALTH CHECK ─────────────────────────────────────────────────────────────
HEALTH_PORT=8080               # dejar así
HEALTH_ALERT_HOURS=2           # alerta al admin si el poller no actualizó en 2 horas
# Token para /health (detallado) y /health/history — generar con:
# python3 -c "import secrets; print(secrets.token_hex(32))"
HEALTH_AUTH_TOKEN=             # ← COMPLETAR (recomendado)

# ── POLLING ──────────────────────────────────────────────────────────────────
POLL_INTERVAL_MINUTES=60       # revisar el PJ cada 60 minutos
POLL_HORA_INICIO=7             # desde las 7am (hora Paraguay)
POLL_HORA_FIN=18               # hasta las 6pm (hora Paraguay)

Guardar y salir de nano: Ctrl+OEnterCtrl+X.

Generar FERNET_KEY si Marcos no te la dio

# Generar una clave nueva (solo si no tenés una preexistente)
uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Salida esperada: una cadena de ~44 caracteres como gAAAAABx3.... Copiarla y pegarla en .env.

⚠️ Guardá la FERNET_KEY en un gestor de contraseñas (Bitwarden, 1Password, etc.). Si se pierde, las credenciales cifradas de todos los abogados quedan irrecuperables.

Instalar uv (si no está disponible como pedrito)

curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env
uv --version

Restringir permisos del .env

chmod 600 /home/pedrito/hechakuaa_bot/.env

# Verificar
ls -la /home/pedrito/hechakuaa_bot/.env
# Debe mostrar: -rw------- 1 pedrito pedrito

9. Cifrar las credenciales del PJ

El bot nunca guarda la cédula ni la contraseña del PJ en texto plano. Este script las cifra con la FERNET_KEY y guarda el resultado cifrado en .env.

# Como pedrito, dentro del directorio del proyecto:
cd /home/pedrito/hechakuaa_bot
uv run python scripts/cifrar_credenciales.py

El script te va a pedir:

  1. Cédula de identidad — el número con el que Marcos entra a apps.csj.gov.py
  2. Contraseña — la contraseña de ese sistema (no se muestra mientras escribís)

Salida esperada:

✅ Credenciales cifradas y guardadas en .env
   PJ_USUARIO_ENC y PJ_PASSWORD_ENC actualizadas.

Verificar que se guardaron:

grep "PJ_USUARIO_ENC" /home/pedrito/hechakuaa_bot/.env
# Debe mostrar un valor cifrado largo, NUNCA la cédula en texto plano

10. Obtener certificado SSL con Certbot

HTTPS es obligatorio para el modo webhook de Telegram y para cualquier servicio expuesto en producción. Certbot obtiene el certificado gratis de Let's Encrypt.

Prerequisito: El DNS del paso 2 ya debe estar propagado. Verificalo:

nslookup pedrito.inqualityhq.com
# Debe retornar la IP de este VPS

Si no retorna la IP correcta, esperá y volvé después.

10.1 Instalar Certbot

# Como root:
exit   # si estabas como pedrito, volver a root

apt install -y certbot python3-certbot-nginx

10.2 Obtener el certificado

certbot --nginx -d pedrito.inqualityhq.com

Certbot te va a preguntar:

  1. Tu dirección de email (para avisos de vencimiento) — ponés la de Marcos o la del equipo
  2. Aceptar los términos de servicio → Y
  3. Si querés compartir el email con EFF → N (opcional)

Salida esperada al terminar:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/pedrito.inqualityhq.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/pedrito.inqualityhq.com/privkey.pem
...

¿Por qué usamos --nginx? Le decimos a Certbot que tenemos Nginx instalado y que puede configurar automáticamente el sitio para HTTPS. Pero vamos a sobreescribir esa configuración en el siguiente paso para personalizarla.

10.3 Verificar la renovación automática

Los certificados de Let's Encrypt duran 90 días y se renuevan automáticamente:

# Verificar que el timer de renovación automática existe
systemctl status certbot.timer
# Debe estar "active (waiting)"

# Simular una renovación para verificar que funciona
certbot renew --dry-run
# Debe decir: "Simulating renewal of an existing certificate..."

11. Configurar Nginx como reverse proxy

Nginx recibe las requests de internet y las redirige al bot corriendo en el contenedor Docker (puerto 8080). También maneja el SSL automáticamente.

11.1 Crear el archivo de configuración

# Como root:
nano /etc/nginx/sites-available/pedrito

Pegar exactamente este contenido (reemplazá el nombre de dominio si usás uno diferente):

# /etc/nginx/sites-available/pedrito
# Reverse proxy para Pedrito Hechakuaa

# ── Rate limiting ─────────────────────────────────────────────────────────────
# Limita requests por IP para mitigar escaneo automatizado y ataques de fuerza bruta.
# La zona se define fuera del bloque server (en el contexto http).
# Si tenés otros sites en este Nginx, mover estas líneas a /etc/nginx/nginx.conf
# dentro del bloque http { }.
limit_req_zone $binary_remote_addr zone=pedrito_webhook:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=pedrito_health:10m  rate=10r/m;

server {
    listen 80;
    server_name pedrito.inqualityhq.com;
    # Redirigir todo el tráfico HTTP a HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    server_name pedrito.inqualityhq.com;

    # Certificados SSL de Let's Encrypt
    ssl_certificate     /etc/letsencrypt/live/pedrito.inqualityhq.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/pedrito.inqualityhq.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # Seguridad: no exponer versión de Nginx ni headers informativos
    server_tokens off;
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options DENY;

    # Tamaño máximo de payload (Telegram puede enviar archivos)
    client_max_body_size 20M;

    # ── Health check ──────────────────────────────────────────────────────────
    # /health es público (respuesta mínima sin datos sensibles)
    # /health/history requiere Bearer token — el bot lo rechaza sin él
    location /health {
        limit_req zone=pedrito_health burst=5 nodelay;
        proxy_pass http://127.0.0.1:8080/health;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 10s;
    }

    location /health/history {
        limit_req zone=pedrito_health burst=3 nodelay;
        proxy_pass http://127.0.0.1:8080/health/history;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        # Pasar Authorization header para que el bot pueda validar el Bearer token
        proxy_set_header Authorization $http_authorization;
        proxy_read_timeout 10s;
    }

    # ── Webhook de Telegram ───────────────────────────────────────────────────
    # Solo Telegram envía aquí — los IPs de Telegram son bien conocidos (149.154.x, 91.108.x)
    # El bot valida el X-Telegram-Bot-Api-Secret-Token internamente
    location /webhook {
        limit_req zone=pedrito_webhook burst=50 nodelay;
        proxy_pass http://127.0.0.1:8080/webhook;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Telegram-Bot-Api-Secret-Token $http_x_telegram_bot_api_secret_token;
        proxy_read_timeout 30s;
    }

    # ── Denegar cualquier otra ruta ───────────────────────────────────────────
    # Respuesta genérica — no revelar que hay un bot ni su propósito
    location / {
        return 404;
    }
}

Guardar: Ctrl+OEnterCtrl+X.

11.2 Activar el sitio

# Crear el enlace simbólico que activa el sitio
ln -s /etc/nginx/sites-available/pedrito /etc/nginx/sites-enabled/pedrito

# Verificar que no hay errores de sintaxis en la configuración
nginx -t

Salida esperada:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Si hay errores, revisá que el archivo se pegó correctamente (especialmente las comillas y los ;).

11.3 Recargar Nginx

systemctl reload nginx
systemctl status nginx
# Debe estar active (running)

11.4 Verificar que Nginx responde

# Desde el propio servidor:
curl -sk https://pedrito.inqualityhq.com/health
# Debe retornar un error de conexión rechazada (porque el bot no está corriendo aún)
# o un JSON si ya está corriendo

# Verificar que HTTP redirige a HTTPS:
curl -I http://pedrito.inqualityhq.com/health
# Debe retornar: HTTP/1.1 301 Moved Permanently

12. Configurar el firewall

# Como root
ufw allow OpenSSH
ufw allow 'Nginx Full'   # abre puertos 80 (HTTP) y 443 (HTTPS)
ufw --force enable

# Verificar estado
ufw status

Salida esperada:

Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
Nginx Full                 ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
Nginx Full (v6)            ALLOW       Anywhere (v6)

Importante: El puerto 8080 del bot no se abre al exterior. Solo Nginx (que corre en el mismo servidor) se comunica con el bot internamente. El bot no es accesible directamente desde internet, solo a través de Nginx.


13. Iniciar el bot con Docker

13.1 Actualizar docker-compose.yml para producción

Por seguridad, el bot solo debe escuchar en localhost (no en todas las interfaces de red). Modificá el puerto en docker-compose.yml:

# Como pedrito:
su - pedrito
cd /home/pedrito/hechakuaa_bot

# Editar docker-compose.yml
nano docker-compose.yml

Buscá la línea:

      - "8080:8080"

Cambiala por:

      - "127.0.0.1:8080:8080"

Guardar y salir. Esto hace que el bot solo sea accesible desde el mismo servidor (por Nginx), no desde internet.

13.2 Construir la imagen Docker

# Construir la imagen (tarda 1-3 minutos la primera vez)
docker compose build

# Salida esperada al final:
# => exporting to image
# => => naming to docker.io/library/hechakuaa_bot-pedrito

13.3 Primera prueba: correr en primer plano

Antes de dejarlo en background, verificamos que arranca bien:

docker compose up

Deberías ver logs como:

pedrito-1  | {"event": "migration_aplicada", "version": "001_initial_schema", ...}
pedrito-1  | {"event": "database_iniciada", ...}
pedrito-1  | {"event": "health_server_iniciado", "port": 8080, ...}
pedrito-1  | {"event": "bot_telegram_iniciado", ...}
pedrito-1  | {"event": "pedrito_hechakuaa_corriendo", ...}
pedrito-1  | {"event": "poller_iniciado", ...}
pedrito-1  | {"event": "login_exitoso", ...}

Si aparece login_exitoso, el bot se conectó al PJ. Detenerlo con Ctrl+C.

Si no aparece login_exitoso: ver la sección Troubleshooting.

13.4 Correr en background (modo producción)

# Arrancar en modo detached (background)
docker compose up -d

# Verificar que el contenedor está corriendo
docker compose ps

Salida esperada:

NAME                IMAGE                     COMMAND                  SERVICE   STATUS    PORTS
hechakuaa_bot-...   hechakuaa_bot-pedrito      "uv run python main.…"   pedrito   running   127.0.0.1:8080->8080/tcp

El STATUS debe decir running.

13.5 Configurar Docker para que arranque en cada reboot

# Como root:
exit   # salir de pedrito

# El servicio de Docker ya inicia automáticamente
# Solo verificar que el contenedor tiene restart policy correcta
systemctl enable docker

El docker-compose.yml ya tiene restart: unless-stopped, lo que hace que el contenedor se reinicie automáticamente si el VPS se reinicia.


14. Activar el modo webhook

En modo webhook, Telegram envía los mensajes al bot directamente (HTTPS push), en lugar de que el bot los pida constantemente (polling). Es más eficiente y tiene menor latencia.

14.1 Generar el WEBHOOK_SECRET

Este token es como una contraseña que Telegram incluye en cada request para probar que es legítimo:

# Generar un token aleatorio seguro:
python3 -c "import secrets; print(secrets.token_hex(32))"
# Salida: una cadena de 64 caracteres hexadecimales

Copiar la salida. La vamos a usar en el siguiente paso.

14.2 Actualizar .env con los datos del webhook

su - pedrito
nano /home/pedrito/hechakuaa_bot/.env

Completar estas dos líneas:

WEBHOOK_URL=https://pedrito.inqualityhq.com
WEBHOOK_SECRET=<el token que generaste arriba>

Guardar y salir.

14.3 Reiniciar el bot para aplicar el cambio

cd /home/pedrito/hechakuaa_bot
docker compose restart

# Ver los logs para confirmar que el webhook se registró:
docker compose logs --tail=20

Buscar en los logs:

{"event": "webhook_registrado", "url": "https://pedrito.inqualityhq.com/webhook", ...}

14.4 Verificar que el webhook quedó registrado en Telegram

# Reemplazá TU_BOT_TOKEN con el valor real del .env
curl -s "https://api.telegram.org/bot<TU_BOT_TOKEN>/getWebhookInfo" | python3 -m json.tool

Salida esperada:

{
    "ok": true,
    "result": {
        "url": "https://pedrito.inqualityhq.com/webhook",
        "has_custom_certificate": false,
        "pending_update_count": 0,
        ...
    }
}

El campo url debe mostrar la URL correcta. Si está vacío o muestra otra URL, revisá los logs del bot.


15. Verificación final

Correr esta lista de verificaciones antes de dar el deploy por terminado:

# 1. El contenedor Docker está corriendo
docker compose ps
# STATUS debe ser "running"

# 2. Los logs no tienen errores
docker compose logs --tail=30
# Buscar "login_exitoso" y "pedrito_hechakuaa_corriendo"
# NO debe aparecer: ERROR, exception, Traceback

# 3. El health check responde
curl -s https://pedrito.inqualityhq.com/health | python3 -m json.tool
# Debe retornar JSON con "status": "ok"

# 4. El webhook está registrado (si activaste el modo webhook)
curl -s "https://api.telegram.org/bot<TU_BOT_TOKEN>/getWebhookInfo" | python3 -m json.tool
# url debe ser: https://pedrito.inqualityhq.com/webhook

# 5. El .env tiene los permisos correctos
ls -la /home/pedrito/hechakuaa_bot/.env
# Debe mostrar: -rw------- 1 pedrito pedrito

# 6. El directorio data existe y tiene contenido
ls -la /home/pedrito/hechakuaa_bot/data/
# Debe aparecer pasante.db y snapshot_*.json

# 7. Nginx redirige HTTP a HTTPS
curl -I http://pedrito.inqualityhq.com/health
# Debe retornar: HTTP/1.1 301 Moved Permanently

Si todos los checks pasan, avisarle a Marcos para que pruebe mandándole /start al bot en Telegram.


16. Monitoreo y logs

Logs del bot en tiempo real

# Como pedrito:
su - pedrito
cd /home/pedrito/hechakuaa_bot

# Ver logs en tiempo real (Ctrl+C para salir)
docker compose logs -f

# Ver solo los últimos 50 registros
docker compose logs --tail=50

# Ver logs con timestamps más legibles
docker compose logs --tail=50 --timestamps

Logs de Nginx

# Accesos (como root):
tail -f /var/log/nginx/access.log

# Errores:
tail -f /var/log/nginx/error.log

# Solo errores del bot (filtra por el path):
grep "/webhook\|/health" /var/log/nginx/access.log | tail -20

Eventos útiles que buscar en los logs del bot

Evento Qué significa
login_exitoso Se conectó al PJ correctamente
migration_aplicada Se aplicó una migración de DB (solo al arrancar)
health_server_iniciado El server HTTP de health está escuchando
webhook_registrado El webhook se registró con Telegram correctamente
polling_tenant_ok Revisión de notificaciones completada sin errores
polling_tenant_error Error al revisar notificaciones de un abogado
pj_error_conexion El PJ no respondió (puede ser transitorio)
bot_notificacion_enviada Se envió una notificación a Telegram
onboarding_completado Un abogado completó el registro
health_alerta_enviada El bot detectó que el poller está detenido y avisó al admin

Ver historial de eventos vía HTTP

# Últimos 50 eventos del audit log:
curl -s https://pedrito.inqualityhq.com/health/history | python3 -m json.tool

# Últimos 100 eventos:
curl -s "https://pedrito.inqualityhq.com/health/history?limit=100" | python3 -m json.tool

17. Health check HTTP

El bot expone un endpoint HTTP en https://pedrito.inqualityhq.com/health que retorna el estado actual.

Ejemplo de respuesta normal (status 200)

{
  "status": "ok",
  "timestamp": "2026-05-25T10:00:00+00:00",
  "tenants_activos": 3,
  "last_poll": "2026-05-25T09:58:42+00:00",
  "last_poll_tenants": 3,
  "seconds_since_last_poll": 78,
  "poll_interval_minutes": 60,
  "webhook_mode": true
}

Ejemplo de respuesta degradada (status 503)

Si el poller no actualizó en más de HEALTH_ALERT_HOURS horas, retorna:

{
  "status": "degraded",
  ...
}

En ese caso, el bot también manda un mensaje de alerta automáticamente al Telegram de Marcos.

Integrar con un monitor externo (opcional)

Si tienen un servicio de uptime monitoring (UptimeRobot, BetterUptime, etc.):

  • URL a monitorear: https://pedrito.inqualityhq.com/health
  • Método: GET
  • Código esperado: 200
  • Intervalo: 5 minutos

18. Cómo actualizar el bot

Cuando hay cambios en el código (nuevo commit en el repositorio):

# Como pedrito:
su - pedrito
cd /home/pedrito/hechakuaa_bot

# 1. Traer los cambios del repositorio
git pull

# 2. Reconstruir la imagen con el nuevo código
docker compose build

# 3. Reiniciar el contenedor con la nueva imagen
docker compose up -d

# 4. Verificar que levantó bien
docker compose ps
docker compose logs --tail=20

Importante: El archivo .env y el directorio data/ no se tocan con git pull — están en .gitignore. Las credenciales y el historial de notificaciones persisten entre actualizaciones.

Si hubo cambios en .env.example: revisar manualmente si hay nuevos campos que agregar al .env existente.

# Ver diferencias entre el template nuevo y el .env actual:
diff .env.example .env

19. Troubleshooting

El contenedor no arranca

# Ver el error específico:
docker compose logs --tail=50

# O ver el estado del contenedor:
docker compose ps

FERNET_KEY inválida La clave en .env es incorrecta o tiene espacios/comillas. Verificar que el valor en .env no tiene comillas ni espacios al inicio/final.

telegram.error.InvalidToken El TELEGRAM_BOT_TOKEN en .env tiene un error. Verificar copiando el token directamente desde la conversación con @BotFather en Telegram.

ModuleNotFoundError: No module named 'X' La imagen necesita ser reconstruida:

docker compose build --no-cache
docker compose up -d

Error response from daemon: Cannot connect to the Docker daemon Docker no está corriendo:

systemctl start docker
docker compose up -d

El bot arranca pero no responde en Telegram

  1. Verificar que el contenedor está corriendo: docker compose ps
  2. Verificar que el webhook está registrado: curl -s "https://api.telegram.org/bot<TOKEN>/getWebhookInfo"
  3. Verificar que Nginx está ok: systemctl status nginx
  4. Verificar que el health check responde: curl -s https://pedrito.inqualityhq.com/health
  5. Revisar logs de Nginx: tail -20 /var/log/nginx/error.log

Error de credenciales del PJ

su - pedrito
cd /home/pedrito/hechakuaa_bot

# Repetir el cifrado de credenciales
uv run python scripts/cifrar_credenciales.py

# Reiniciar el bot
docker compose restart
docker compose logs --tail=10

El certificado SSL venció o hay error de SSL

# Renovar manualmente (normalmente es automático):
certbot renew

# Recargar Nginx para que use el nuevo certificado:
systemctl reload nginx

El webhook no recibe mensajes (pero el bot arranca)

# 1. Verificar que Nginx está reenviando correctamente:
tail -20 /var/log/nginx/access.log
# Debe aparecer peticiones POST a /webhook cuando alguien manda un mensaje

# 2. Verificar el health check del bot directamente (sin pasar por Nginx):
# Desde el propio servidor:
curl -s http://127.0.0.1:8080/health

# 3. Volver a registrar el webhook manualmente:
curl "https://api.telegram.org/bot<TOKEN>/setWebhook?url=https://pedrito.inqualityhq.com/webhook&secret_token=<WEBHOOK_SECRET>"

El VPS se reinició y el bot no volvió

El contenedor tiene restart: unless-stopped, así que debería volver solo. Si no:

su - pedrito
cd /home/pedrito/hechakuaa_bot
docker compose up -d

Si Docker en sí no arrancó:

systemctl start docker
su - pedrito
cd /home/pedrito/hechakuaa_bot
docker compose up -d

Ver dos instancias corriendo (causa conflictos de Telegram)

docker compose ps
# Si hay más de un contenedor corriendo:
docker compose down
docker compose up -d

Error de permisos en el directorio data/

# Como root:
chown -R 1000:1000 /home/pedrito/hechakuaa_bot/data
# Reiniciar:
su - pedrito
cd /home/pedrito/hechakuaa_bot
docker compose restart

20. Referencia rápida de comandos

Gestión del bot (como usuario pedrito)

cd /home/pedrito/hechakuaa_bot

# Estado
docker compose ps

# Iniciar / detener / reiniciar
docker compose up -d
docker compose down
docker compose restart

# Logs en tiempo real
docker compose logs -f

# Logs últimos 50 registros
docker compose logs --tail=50

# Actualizar (git pull + rebuild + restart)
git pull && docker compose build && docker compose up -d

Nginx (como root)

# Verificar configuración
nginx -t

# Recargar sin cortar conexiones
systemctl reload nginx

# Reiniciar completamente
systemctl restart nginx

# Ver logs de error
tail -f /var/log/nginx/error.log

SSL

# Renovar certificado manualmente
certbot renew

# Ver fecha de vencimiento del certificado actual
certbot certificates

Rutas de archivos importantes

Archivo Ruta absoluta
Código del bot /home/pedrito/hechakuaa_bot/
Configuración /home/pedrito/hechakuaa_bot/.env
Base de datos SQLite /home/pedrito/hechakuaa_bot/data/pasante.db
Snapshots de notificaciones /home/pedrito/hechakuaa_bot/data/snapshot_*.json
Docker Compose /home/pedrito/hechakuaa_bot/docker-compose.yml
Config Nginx del bot /etc/nginx/sites-available/pedrito
Logs de Nginx /var/log/nginx/
Certificado SSL (cert) /etc/letsencrypt/live/pedrito.inqualityhq.com/fullchain.pem
Certificado SSL (clave) /etc/letsencrypt/live/pedrito.inqualityhq.com/privkey.pem

Repositorio: https://github.com/InqGit/hechakuaa_bot
Ante dudas técnicas no cubiertas acá, contactar a Marcos Benítez.