Saltar a contenido

Seguridad

Novedades en v1.6.0

  • La API key del sandbox: en Odoo 14.0 a 19.0, scripts/deploy_local.py hace que Odoo cree una API key para el admin del sandbox y la guarda, sin imprimirla, en el .env del sandbox. Ver Infraestructura local.
  • Las revisiones de copias y de opciones (--check-copies y --check-options) no corren ningún programa, no contactan nada y nunca imprimen un valor sensible, contenga lo que contenga la entrada. Ver Credenciales.
  • El límite conocido 10 se cierra, salvo un caso: en Odoo 10 a 12, una clave de contexto con el nombre de un campo del modelo se lee como un valor. Ver Política de permisos.

Novedades en v1.5.1

  • Un hook de inicio de sesión que no lee ninguna opción del plugin ni credencial: hooks/session_start.py reemplaza a hooks/check_options.py. Ver Credenciales.
  • El servidor solo lee las credenciales que le pasan las opciones del plugin: ya no lee ODOO_HTTP_BEARER.
  • Diagnóstico local de solo metadatos, revisado contra una lista permitida. Ver Privacidad.
  • Qué reportan las revisiones del directorio de plugins, y dónde verificar cada hallazgo.
  • Un límite conocido nuevo de la política: un contexto pasado por posición en Odoo 10 a 12 (ver Política de permisos).

Novedades en v1.5.0

  • Política de permisos encima de los derechos del usuario de Odoo, con el perfil standard por defecto. Ver Política de permisos abajo.
  • Registro de auditoría local, encadenado por hash, sin valores ni secretos.
  • Sesiones por identidad completa: la clave de sesión incluye un HMAC del secreto por proceso, y se quitó el camino multi-tenant por cabeceras.
  • odoo_connections ya no devuelve el user:pass@ de la URL.
  • El hook de opciones revisa también las dos opciones de la política.

Credenciales

  • Como plugin, las credenciales son opciones del plugin (/plugin → odoo-tools → Configure options). La API key y la contraseña son opciones sensitive: se guardan en el almacén seguro de credenciales del sistema operativo, nunca en settings.json ni en tu perfil de shell, y solo se pasan al proceso local del servidor MCP, que solo las envía a la URL de Odoo que configuraste. No exportes ODOO_* en tu shell para el plugin.
  • El plugin tiene dos hooks SessionStart, y ninguno contacta nada:
    • el primero corre python3 --version y muestra un aviso solo cuando python3 no se puede ejecutar (el servidor MCP no podría arrancar);
    • el segundo, hooks/session_start.py (nuevo en v1.5.1, reemplaza a hooks/check_options.py), arranca solo cuando python3 es 3.9 o superior y no lee ninguna opción del plugin ni credencial. Lee solo CLAUDE_PLUGIN_DATA y CLAUDE_CODE_ENTRYPOINT (el nombre de la app de Claude que inició la sesión), el origen y el id de sesión del evento, el final del diagnóstico del servidor (diagnostics/server.jsonl y su archivo rotado server.jsonl.1), si existe la marca del modo contenedor, sus marcas de tiempo diarias y la versión del plugin: nada fuera de las carpetas del plugin. Cuando los últimos 14 días no registran ni una conexión exitosa a Odoo ni un arranque del servidor con todos los datos de conexión, dice dónde corren las herramientas de Odoo (no en Cowork ni en Claude chat), como máximo una vez al día al iniciar sesión, y se lo explica a Claude con una línea de diagnóstico para el doctor (en la primera sesión tras instalar o actualizar el plugin, y donde no tiene carpeta de datos, solo a Claude); cuando el último arranque del servidor quedó bloqueado por la política de permisos, lo dice con su código E. Escribe una línea de metadatos por sesión en diagnostics/hook.jsonl (nunca a través de un enlace simbólico), guarda dos archivos de marca de tiempo para los avisos diarios y nunca detiene la sesión (siempre sale con 0). Con el modo contenedor de /odoo-tools:deploy-local no dice nada.
  • Claude Code exporta cada opción guardada del plugin, incluidas las sensibles, al entorno de cada proceso de hook del plugin como CLAUDE_PLUGIN_OPTION_*. Los hooks de este plugin no leen ninguna (lo comprueba server/tests/test_product_rules.py).
  • Las revisiones de copias y de opciones (run_stdio.py --check-copies y --check-options, que la ruta guiada y /odoo-tools:odoo-doctor corren en Claude Code) leen en su entrada estándar el JSON que imprime un comando de Claude Code (claude plugin list --json o claude plugin configure <id> --json): no corren ningún programa, no abren ningún archivo de Claude Code, no contactan nada y nunca imprimen un valor sensible, contenga lo que contenga la entrada. De la URL de Odoo, la revisión de opciones solo repite el esquema, el host y el puerto, nunca una ruta, una consulta ni un fragmento, que pueden llevar el token de un enlace de portal, y rechaza una URL con cualquier @ como usuario o contraseña, sin mostrar ninguna parte de ella. Las dos leen UTF-8, también desde una tubería de Windows. Ver Dos copias de odoo-tools.
  • En modo standalone (Docker, python -m odoo_mcp), el servidor lee las credenciales del entorno (ODOO_URL, ODOO_DB, ODOO_LOGIN, ODOO_API_KEY/ODOO_PASSWORD). El servidor nunca toma una URL ni un secreto de Odoo de una petición del cliente: el camino multi-tenant por cabeceras se quitó en la v1.5.0, y ODOO_MULTITENANT se lee pero queda reservada. El servidor no lee ninguna otra credencial: ODOO_HTTP_BEARER, que leía de su entorno pero nunca usaba, se quitó en la v1.5.1.
  • El servidor MCP no persiste secretos en disco ni los escribe en logs. Las sesiones se identifican por url|db|login más un HMAC del secreto calculado por proceso, así que la misma URL, base y login con otro secreto nunca reutiliza una sesión autenticada; el secreto mismo nunca forma parte de una clave ni de una línea de log.
  • El objeto Settings redacta las credenciales en repr()/str() para que un log accidental no las filtre (garantizado por tests/test_security_redaction.py).
  • Prefiere API keys (Odoo ≥ 14) sobre contraseñas. Usa usuarios de Odoo de mínimo privilegio y activa Read-only mode en las opciones del plugin para exploración o demos.
  • Qué datos se procesan y a qué servicios se contacta: ver Privacidad.

La CLI de respaldo sí escribe un archivo con credenciales

A diferencia del servidor MCP, la CLI de respaldo (/odoo-tools:odoo-setup-cli) guarda tus credenciales en un archivo .env local en ~/.claude/tools/odoo-cli. La configuración lo crea legible solo por tu usuario y con marcadores de posición: tú pegas el secreto en tu propio editor; Claude nunca te lo pide en el chat. Si usas esa superficie: conserva esos permisos restrictivos (equivalentes a chmod 600), no lo subas a control de versiones, y elimínalo cuando dejes de usar la CLI. Rota la API key con la misma cadencia que aplicarías a cualquier otro secreto guardado en disco.

Usuario dedicado para el agente

Crea un usuario de Odoo específico para el agente (por ejemplo ai-bot@tuempresa.com) con solo los permisos que necesita. Genera la API key desde ese usuario y rótala periódicamente. Así acotas el radio de daño si la key se filtra.

Qué pasa con los datos de tu empresa

Conviene distinguir dos rutas de datos:

  1. Del servidor MCP a Odoo (directo). El servidor corre en tu infraestructura y habla directamente con tu instancia Odoo por JSON-2 / JSON-RPC / XML-RPC. Transgenia no actúa de intermediario: no hacemos de proxy, no almacenamos ni vemos tus datos ni tus credenciales.
  2. Del agente de IA al proveedor del modelo. Cuando operas Odoo a través de un agente (Claude Code u otro cliente MCP), los resultados de cada herramienta (los registros de Odoo que consultas) se devuelven al cliente y se envían al proveedor del modelo (por ejemplo Anthropic, en el caso de Claude) para que el modelo los procese. Esto es inherente a cualquier flujo con agentes de IA; no es algo que agregue este plugin.

Implicaciones prácticas: tú decides qué consultas y con qué usuario de Odoo (mínimo privilegio). Revisa las políticas de tratamiento de datos de tu proveedor de modelo y evita traer campos sensibles que el agente no necesita. Para datos regulados, consulta solo lo imprescindible y considera anonimizar en origen.

Cadena de suministro

  • Cero dependencias en tiempo de ejecución. Desde la v1.2.0, el servidor MCP usa solo la biblioteca estándar de Python (el protocolo MCP y los transportes JSON-2, JSON-RPC y XML-RPC están implementados sobre ella). El plugin lo lanza con python3 ${CLAUDE_PLUGIN_ROOT}/server/run_stdio.py: sin lanzador de paquetes (uv, uvx, npx), sin paso de instalación y sin descargas al arrancar. El código que corre es exactamente el código legible incluido en la carpeta del plugin.
  • run_stdio.py pone primero en sys.path el código incluido, de modo que otro paquete odoo_mcp instalado en el equipo no puede sustituir el código revisado.
  • Los extras opcionales (cache, metrics, otel) solo se usan si tú los instalas (modo standalone); el plugin no instala ninguno.
  • La CLI de respaldo opcional es la única pieza que instala paquetes: corre npm install al configurarla con /odoo-tools:odoo-setup-cli.

Qué reportan las revisiones del directorio de plugins

El directorio de plugins de Claude lista estos hallazgos para este plugin. Cada uno, con su nombre en inglés como lo muestra el directorio, y dónde verificarlo:

  • Uses a credential from the user's machine (.claude-plugin/plugin.json, server/src/odoo_mcp/config.py). El servidor MCP lee dos credenciales, la API key y la contraseña de Odoo, del entorno que .claude-plugin/plugin.json llena desde las opciones sensitive odoo_api_key y odoo_password. Solo las envía a la URL de Odoo que configuraste, el servicio al que pertenecen: los tres transportes de server/src/odoo_mcp/transport/ se conectan solo a esa URL y rechazan las redirecciones. server/tests/test_product_rules.py falla si el servidor lee cualquier otra credencial de su entorno.
  • scripts/deploy_local.py genera contraseñas aleatorias (secrets.token_urlsafe) para el sandbox Docker local de /odoo-tools:deploy-local, las guarda en un .env solo para tu usuario en la carpeta del sandbox y las usa solo con los contenedores de ese sandbox en 127.0.0.1. No son tus credenciales. En Odoo 14.0 y posteriores también hace que el Odoo del sandbox cree una API key para su usuario administrador (odoo shell en un contenedor de un solo uso del sandbox, con el propio res.users.apikeys de Odoo), comprueba que la key inicia sesión y la guarda en el mismo .env como ODOO_API_KEY, sin imprimirla nunca. Tú la copias en la opción Odoo API key del plugin; Claude nunca la lee.
  • Pruebas (server/tests/): usan credenciales falsas como testigo para demostrar que ninguna línea de log, de auditoría o de diagnóstico, ningún reporte de soporte y ninguna salida de hook contienen una credencial. El plugin nunca las corre.
  • Uses hooks. Los dos hooks SessionStart de Credenciales: hooks/session_start.py lee solo la carpeta de datos del plugin y CLAUDE_CODE_ENTRYPOINT, nunca una opción del plugin ni una credencial (lo comprueba server/tests/test_product_rules.py), y siempre sale con 0; el otro solo corre python3 --version.
  • Runs a local program for an MCP server. python3 ${CLAUDE_PLUGIN_ROOT}/server/run_stdio.py: solo la biblioteca estándar, sin instalar ni descargar nada (ver Cadena de suministro).
  • Contains a download-and-run command. Solo dos comandos descargan y corren código en tu equipo, y solo cuando tú los invocas y confirmas: /odoo-tools:deploy-local descarga y corre imágenes de Docker (copias sin cambios de las imágenes oficiales de PostgreSQL y Odoo, por etiqueta de serie, y la imagen del servidor MCP en la versión del plugin), y /odoo-tools:odoo-setup-cli corre npm install para la CLI opcional. El servidor MCP y los hooks no descargan ni instalan nada.

Chequeos automáticos (CI)

El flujo .github/workflows/security.yml corre en cada push/PR:

  • gitleaks: escanea historia y diffs en busca de secretos commiteados. Se compila con go install en una etiqueta fija (v8.18.4, con cada módulo verificado contra la base de checksums de Go), sin descargar y ejecutar binarios con curl.
  • leak-guard: busca marcadores internos o de tenant prohibidos (datos de clientes, hosts de infraestructura, identificadores fiscales, credenciales hardcodeadas) y hace fallar el build si aparecen. El repositorio es vendor-neutral: solo contiene el tooling genérico más la marca/autoría pública y la oferta de servicios. Los marcadores específicos de la organización se leen de un secreto del repositorio en lugar de estar escritos en el flujo (la carpeta del plugin es la raíz del repositorio, así que el flujo llega a cada instalación), y sus coincidencias se reportan solo por nombre de archivo.
  • .gitignore: excluye .env, *.env, entornos virtuales y salida de build para que las credenciales locales no se commiteen por accidente.

Transporte

  • Usa URLs de Odoo con HTTPS. JSON-2, JSON-RPC y XML-RPC corren sobre la URL que proporciones; una URL http:// se envía sin cifrar.
  • JSON-2 (Odoo saas~18.4 / 19.0+) envía la API key como Authorization: bearer en cada petición, nunca en una redirección (las redirecciones se rechazan), no envía cookies y nunca muestra en un resultado de herramienta la traza del servidor de Odoo (el campo debug de un error). Solo acepta API keys, así que una configuración con contraseña se queda en los endpoints clásicos.
  • Todos los transportes verifican el certificado TLS (incluido el nombre del host) con el almacén de confianza del sistema (respetan SSL_CERT_FILE; si el paquete opcional certifi está instalado, también usan su lista de CA). No hay opción para desactivar la verificación. Todos aplican ODOO_TIMEOUT (120 s por defecto; opción del plugin Request timeout).
  • JSON-RPC no sigue redirecciones HTTP (un POST redirigido se convertiría en un GET sin cuerpo); configura la URL final de tu Odoo.
  • Autenticación básica delante de Odoo: ODOO_URL puede llevar user:pass@; JSON-RPC la envía como cabecera Authorization y ningún error ni log ni resultado de herramienta repite la contraseña (desde la v1.5.0, odoo_connections quita el user:pass@ de la URL que reporta). JSON-2 necesita esa misma cabecera para su API key, así que una URL así se queda en los endpoints clásicos. Odoo URL no es una opción sensitive: esa contraseña se guarda en la configuración del plugin, no en el almacén seguro de credenciales.
  • El servidor registra logs solo en stderr; stdout está reservado al canal MCP.

Registro de auditoría

Desde la v1.5.0, cada llamada a herramienta, rechazos incluidos, deja una línea JSON: la herramienta, los nombres de modelo y operación que tocó (op:modelo[:método]), la capa y la regla de un rechazo, conteos, la duración, el nombre de la clase de excepción de un error y el digest de la política. Nunca guarda valores de argumentos, dominios, valores de campos, ids, contenido de archivos, la URL, tokens, llaves, contraseñas ni textos de mensajes de error; un nombre que no pasa la revisión de nombres de la política se escribe como ?. La base de datos y el login de Odoo aparecen solo en la línea start que se escribe al arrancar el servidor.

  • El plugin escribe archivos diarios (odoo-tools-audit-AAAA-MM-DD.jsonl) en su carpeta de datos (ODOO_AUDIT_DIR), solo para tu usuario (carpeta 0700, archivos 0600, nunca a través de un enlace simbólico), y los conserva 90 días (ODOO_AUDIT_RETENTION_DAYS; 0 los conserva).
  • Sin carpeta, las líneas van al logger odoo_mcp.audit en stderr; ODOO_AUDIT_DIR=off no escribe ninguna.
  • Cada línea lleva un hash de la anterior, y python3 server/run_stdio.py --verify-audit ARCHIVO nombra la primera línea editada, reordenada o insertada, o una última línea incompleta. Con un solo archivo no puede detectar líneas completas quitadas del final de un día ni un archivo de un día borrado (el prefijo que sobrevive sigue encadenado). En local esto es evidencia, no prevención: quien puede escribir la carpeta puede reescribir los archivos.
  • Un fallo al escribir nunca rompe una llamada a herramienta.

Detalle y formato en Registro de auditoría.

Política de permisos

En el modo local (stdio), la política de permisos limita lo que Claude puede hacer a través de las herramientas MCP de odoo-tools. Protege contra errores y contra instrucciones escondidas en los datos que Claude lee. No limita a la persona que tiene la API key, y no es un sandbox para un agente con acceso sin restricciones a la shell del mismo equipo. La frontera que resiste frente a todos es el propio usuario de Odoo: dale al agente un usuario de Odoo dedicado y de mínimo privilegio (no un administrador) y una API key con caducidad. En un futuro modo remoto, donde el servidor guarde la credencial de Odoo, la misma política se volverá una aplicación real de permisos.

  • Qué es. Un techo encima de los derechos de acceso, las reglas de registro y los grupos de campos de Odoo, fijado por las opciones Permission profile (readonly, standard por defecto, builder, full) y un Permission policy file opcional, leído una vez al arrancar desde una ruta absoluta explícita (nunca desde una ubicación por defecto). Nunca concede lo que Odoo rechaza y nunca es más débil que Read-only mode. full conserva el comportamiento de la 1.4. Referencia: Política de permisos.
  • Qué cierra standard. Los secretos guardados (parámetros del sistema, credenciales de servidores de correo, LDAP y proveedores de pago, tokens de IAP y de sincronización bancaria, certificados y llaves CFDI, la cola de correo saliente) no se pueden leer; usuarios, grupos, reglas de acceso, módulos, cuentas bancarias, fusiones de contactos, historial del chatter y secuencias fiscales quedan en solo lectura; los modelos que envían correo, SMS, cartas o enlaces para compartir, y los métodos de salida (message_post salvo notas internas, message_subscribe, send_mail, etc.), quedan cerrados; las acciones de servidor, automatizaciones, vistas y otros registros ir.* quedan en solo lectura; los borrados se rechazan; los campos con aspecto de credencial se retiran de las lecturas y se rechazan en los argumentos. full sigue rechazando crear, cambiar o revocar API keys y desinstalar módulos y, para hacerlo cumplir, una llamada cuyos argumentos o modelos relacionados la política no puede revisar.
  • Dos capas. Los accesos declarados de cada herramienta se deciden con sus argumentos antes de que exista una sesión (capa A: nada llega a Odoo). Luego cada llamada a Odoo que hace la herramienta pasa por una sesión vigilada (capa B) que la revisa otra vez con sus valores y rutas (comandos x2many, rutas de dominio y de exportación, el padre de un adjunto, el correo de contacto de un usuario) antes de enviarla, y detiene como un error cualquier llamada que la herramienta no declaró. Una herramienta sin mapeo de permisos no se puede registrar, listar ni llamar.
  • Una política inválida bloquea el servidor: solo funciona odoo_version, que explica el error sin contactar a Odoo.
  • Cinturón de seguridad, no sandbox. En stdio, un agente con una shell sin restricciones puede leer el entorno del servidor, editar el archivo de política o las opciones del plugin, o llamar a Odoo directamente. Las reglas permissions.deny de Claude Code sobre la carpeta de la política, las confirmaciones de Bash y la línea de instrucciones (un rechazo de la política es definitivo) lo hacen más difícil, no imposible. odoo_version advierte cuando el usuario de Odoo es administrador, porque entonces Odoo no acota nada.
  • Límites conocidos (detallados en Límites conocidos): los efectos dentro de los métodos de negocio (un correo de confirmación, las automatizaciones existentes) son de Odoo; las cadenas que devuelven los métodos (una URL de portal con un token) no se redactan; los modelos puntero distintos de ir.attachment importan solo cuando un archivo rechaza lecturas de negocio; los campos relacionados con nombres inofensivos pueden llevar datos retenidos; los enlaces many2one no se revisan como toques; el contenido publicado es un canal de salida por su efecto; los conteos y agregados permiten inferir; builder equivale a full en confidencialidad, porque las automatizaciones corren código en el servidor; en Odoo 10 a 12, una clave de un objeto pasado por posición a un método de negocio fuera de la tabla de la política que es el nombre de un campo del modelo se lee como un valor, así que no se somete a la lista de claves de contexto permitidas.

Modo solo lectura

  • Con Read-only mode (ODOO_READONLY=1) el servidor ya no lista las herramientas que no son de solo lectura (odoo_create, odoo_write, odoo_unlink, odoo_execute, odoo_translate_set, las dos herramientas Studio-style y las dos de importación: también odoo_import_preview, porque su prueba revertida puede consumir números de secuencia y disparar webhooks), y las rechaza con un error claro antes de tocar Odoo cuando se llaman por su nombre. Las lecturas, exportaciones, odoo_telemetry_preview y las herramientas Online de solo lectura siguen funcionando. Ningún perfil ni archivo de política puede devolver una escritura mientras está activo.
  • La CLI de respaldo no tiene un equivalente, ni una política de permisos: su comando write siempre intenta la llamada. Usa ahí un usuario de Odoo de mínimo privilegio.
  • odoo_online_profile lista las API keys del usuario solo por nombre, alcance y fechas: Odoo no guarda el material de la key en un campo legible, y el plugin nunca crea, rota ni revoca keys. odoo_record_documents devuelve un archivo solo si es adjunto del registro pedido y mide como máximo max_bytes (máximo 5 MiB).

Infraestructura local (/odoo-tools:deploy-local)

  • Solo corre cuando lo invocas y confirmas; ningún hook ejecuta Docker.
  • Odoo se publica solo en 127.0.0.1; no queda expuesto a tu red.
  • Las contraseñas se generan localmente con secrets de Python, se guardan en ~/.odoo-tools/sandbox/.env (0600, carpeta 0700) y nunca se imprimen; Claude no lee ese archivo.
  • En Odoo 14.0 y posteriores, la API key que Odoo crea para el admin del sandbox se guarda en ese mismo archivo como ODOO_API_KEY y tampoco se imprime, ni siquiera en un mensaje de error. Tú la copias en la opción Odoo API key del plugin; nunca la pegues en el chat. Odoo la lista como "odoo-tools sandbox" en Preferencias > Account Security; si la quitas ahí, el siguiente sandbox up crea otra.
  • Las imágenes vienen de ghcr.io/transgenia (copias sin cambios de las oficiales, actualizadas cada semana) con respaldo a Docker Hub.
  • La imagen ghcr.io/transgenia/odoo-mcp-tools corre como usuario sin privilegios y recibe las credenciales por --env-file, nunca en la línea de comandos.

Reportar una vulnerabilidad

Escribe a dev@transgenia.org con detalles y pasos de reproducción. Por favor no abras issues públicos para reportes de seguridad. Buscamos acusar recibo en un plazo de 5 días hábiles.