query-api — puente SQL de solo lectura para Cowork

Puente HTTP genérico entre Cowork (Claude en la nube, solo tiene salida HTTPS, sin red hacia ctunlinux) y las bases de Trivasa: el warehouse Postgres (trivasa_dw) y SQL Server (.207 producción, .205 staging). Código en trivasa-bi-core/query_api/, documentado a fondo en query-api dentro de trivasa-context (docs/proyectos/query-api/index.md) — esta página es el resumen de infraestructura del lado de ctunlinux, no repite las reglas de seguridad completas.

No expone endpoints por tabla: un único POST /query acepta cualquier SELECT/WITH ... SELECT, para que quien lo consuma explore el esquema y arme cualquier query de solo lectura sin pedir cambios en la API.

Arquitectura

Cowork -> reportesweb.frento.com.mx (cloudflared) -> query-api:8765 (uvicorn)
                                                          │
                                                          ├── postgres_dw  -> trivasa_dw (rol ealcocer_ro)
                                                          ├── mssql_207    -> TRIVASADB (producción real)
                                                          └── mssql_205    -> TRIVASADB3 (staging)

Servicio systemd (query-api.service, enabled + active), no un proceso suelto ni un contenedor Docker — mismo patrón que cloudflared.service en este host. Escucha solo en 127.0.0.1:8765, nunca 0.0.0.0: todo el tráfico entrante pasa por el túnel.

Solo lectura, en dos capas

Filtro de texto (bloquea todo lo que no sea un único SELECT/WITH, sin ; en medio) más una segunda capa real de conexión: Postgres abre la transacción en modo postgresql_readonly=True; para SQL Server (pymssql no soporta un modo read-only real por sesión) la red de seguridad es nunca llamar commit() — SQLAlchemy hace rollback de lo que quede abierto al cerrar la conexión. Probado real, no solo revisado el código: DELETE/UPDATE/INSERT/multi-statement contra postgres_dw devuelven HTTP 400 sin tocar la base, tanto en local como a través del túnel público.

Publicación: mismo túnel de ctunlinux

reportesweb.frento.com.mx → http://127.0.0.1:8765 es una regla más del único cloudflared.service de este host (ver ctunlinux § túnel), mismo patrón que Metabase/Perses/Lightdash/varela-bot — no se creó túnel nuevo. Ese hostname ya estaba reservado (“pruebas de API ad-hoc”) de una pasada anterior; este servicio es lo primero real que lo ocupa. Antes del despliegue, el puerto 8765 lo tenía tomado un script de prueba stdlib (~/api-cowork/api.py, /health//ping//echo) que se dio de baja para dejarle el lugar — volvió a levantarse solo una vez durante el arranque (relanzado a mano fuera de la sesión que montó query-api, sin cron/systemd/watchdog de por medio), vale la pena confirmar que no se vuelva a correr por error mientras el puerto siga en uso real.

Secretos: dos proyectos de Infisical distintos, encadenados

A diferencia del resto de los servicios de este host (que usan un solo proyecto de Infisical o ninguno), query-api necesita secretos de dos proyectos al mismo tiempo: Trivasa (b6567423-9986-448e-b2b8-dffe44fe1657, las credenciales del rol ealcocer_ro de Postgres) y secret-management (2aefdbd1-389c-4fd0-bdb8-a5621af8aac1, el bearer token QUERY_API_TOKEN — mismo proyecto que usa varela-bot para su TELEGRAM_BOT_TOKEN). infisical run no soporta apuntar a dos proyectos en una sola invocación, así que el ExecStart de la unidad encadena dos llamadas anidadas, la de afuera envolviendo a la de adentro, que a su vez envuelve a uvicorn.

Gotcha real (2026-09-07): el EnvironmentFile de systemd no acepta el prefijo export, pese a que la documentación de systemd.exec dice que sí — en la práctica lo ignora en silencio (Ignoring invalid environment assignment) en vez de fallar fuerte, y el servicio entraba en loop de reinicio pensando que no había sesión de login de Infisical. El archivo que usa el shell interactivo (~/.config/infisical/ehas-uk.env, formato export KEY=valor) no sirve tal cual para EnvironmentFile= — hizo falta un segundo archivo, ~/.config/infisical/ehas-uk-systemd.env, en texto plano KEY=valor sin export, dedicado solo a esta unidad.

Auditoría

Cada consulta se loguea a Loki (job=query_api) y a trivasa-bi-core/logs/query_api.log — mismo criterio best-effort que el resto de observability/checks/*.py de ese repo: si Loki está caído, solo se advierte, nunca tumba la consulta.

Véase también

  • ctunlinux — el host y el túnel que publica reportesweb.frento.com.mx
  • trivasa-context — la documentación completa del proyecto (reglas de seguridad, endpoints, rotación del token) vive ahí, no aquí
  • varela-bot — el otro servicio de este host que también respalda un secreto en el proyecto secret-management de Infisical
  • Gestión de secretos — por qué las credenciales de Postgres van por Infisical pero las de SQL Server no