Ir al contenido
mcpd
sin daemon
Código

Seguridad

Una página para responder a una revisión. Qué se guarda, qué se envía a otro sitio, quién puede leerlo y los controles que mcpd no tiene.

Todo lo que sigue está tomado del propio código del daemon y de spec/mcpd.spec.json, que es a partir de lo que se genera la API. Donde un control no existe, esta página lo dice en lugar de dejarte el hueco para que lo descubras en un cuestionario.

Qué almacena mcpd

mcpd almacena el texto de las conversaciones de tu agente. Ese es su propósito, y una revisión debería partir de ahí. Un span de tipo model_generation lleva input y output: el prompt exacto que se envió al modelo y el texto exacto que volvió. Para la mayoría de los usuarios, ese es el dato más sensible que tienen.

En las seis señales, los campos de texto libre que se almacenan son:

SeñalContenido que guarda
Spans input, output, attributes, metadata y el mensaje de error
Logsel mensaje y los fields estructurados
Métricaslabels
Puntuacionesreason y metadata
Feedbackel comment de texto libre y metadata
Notas de aspectola frase, en text

Junto al payload, cada señal lleva campos de correlación que atan una ejecución al resto de tu sistema: proyecto, entorno, nombre y versión del servicio, sesión, hilo, usuario, recurso, ejecución, solicitud, experimento y entrega. A user_id se le aplica un hash al entrar, salvo que desactives la ocultación. Los demás se almacenan tal como llegan, así que trata cualquiera de ellos como un campo por el que se puede identificar a una persona si pones una identidad dentro.

Los recuentos de tokens y una cifra de costo se calculan en la ingesta y se almacenan con la ejecución. Nada sobre la llamada al modelo se consulta a un proveedor.

Dónde se escribe

Un solo backend, elegido al arrancar. El que viene por defecto es un archivo SQLite embebido en ./mcpd.db, indicado con --data. El índice de texto completo vive en el mismo archivo, así que el almacén es un archivo que respaldar y un archivo que proteger. Con --postgres-url o --clickhouse-url los datos van a una base de datos que operas tú.

Las ejecuciones duraderas, cuando están activadas, viven en un segundo archivo SQLite al lado del primero, sea cual sea el almacén de trazas. Guarda la entrada, el estado, la salida y la correlación de cada ejecución, y la entrada, la salida y el error de cada paso, exactamente como se enviaron: sin ocultar, con user_id sin hash, porque un paso posterior los vuelve a leer y un valor reescrito cambiaría lo que hace la ejecución. Los spans que una ejecución escribe en el almacén de trazas se ocultan como cualquier otro. Protege el archivo de ejecuciones como proteges el almacén de trazas, o más. La retención y la supresión por sujeto llegan las dos a él, para las ejecuciones que han terminado; consulta Ejecuciones.

mcpd no cifra lo que escribe. El cifrado en reposo es tarea del volumen en el almacén embebido y de la base de datos en los otros dos.

Qué sale de la máquina

Con la configuración por defecto, nada. El daemon hace solicitudes salientes en exactamente cuatro sitios, y todas están apagadas hasta que las configures. No hay comprobación de actualizaciones, ni informes de uso, ni telemetría de ningún tipo.

DestinoQué se envíaApagado salvo que
Backend de almacenamiento Todo lo que mcpd almacena, por tu propia conexión. Configures --postgres-url o --clickhouse-url. El almacén por defecto es un archivo local y no necesita red en absoluto.
Reenvío Cada span almacenado, reemitido como OTLP JSON a {endpoint}/v1/traces. Esto incluye el payload: input y output salen como los atributos input.value y output.value, cada uno limitado a 32 KiB. Los valores ya ocultados siguen ocultados, y un user_id con hash se reenvía como el hash. Configures --forward-endpoint, o un bloque forward en el archivo de configuración.
Webhooks de monitores y objetivos Un cuerpo JSON fijo: el id y el nombre del monitor o del objetivo, el proyecto, la condición, qué vigila, el umbral, la lectura, el número de muestras, un detalle de una línea y una marca de tiempo. No lleva ningún texto de prompt ni de respuesta. Un monitor o un objetivo del archivo de configuración nombra un webhook.
Modelo juez El criterio, más la pregunta de la ejecución y su respuesta, cada una truncada a 4,000 caracteres, enviadas por POST a {endpoint}/chat/completions. Es la única ruta que envía texto de prompt y de respuesta almacenado a un proveedor de modelos. Un bloque judge en el archivo de configuración nombra un endpoint, un modelo y al menos un criterio, y quien llama pide ese evaluador por su nombre.

La interfaz que el binario sirve en / está compilada dentro de él. Sus fuentes, estilos y scripts vienen del mismo origen, y no hace ninguna solicitud a terceros.

Claves y ámbitos

La autenticación son claves de API. No hay cuenta, ni sesión, ni inicio de sesión. Una clave se presenta como Authorization: Bearer … o en la cabecera X-Mcpd-Key, y ambos lados se pasan por SHA-256 antes de compararse, de modo que ni la longitud de la clave configurada ni la posición del primer carácter equivocado se deducen del tiempo de respuesta.

Hay seis ámbitos, en orden del alcance que conceden:

ÁmbitoQué permitePor qué es un ámbito aparte
ingest Escribir señales. No leer nada. Esta clave vive en cada contenedor que emite un span, incluidos los que ejecutan código de terceros. No debe abrir además todos los prompts almacenados.
read Leer las señales almacenadas. Leer es donde están los prompts.
write Cambiar datos almacenados: puntuaciones, feedback, datasets, experimentos y acuses de recibo. Varios de esos manejadores leen un payload de paso. Capturar una ejecución de producción en un dataset lee el input y el output de esa ejecución, que es más que emitir un span.
execute Arrancar, cancelar y señalizar ejecuciones, y publicarles eventos. Arrancar una ejecución gasta dinero y llega a tus sistemas. Una clave que puede registrar una puntuación no debe poder además lanzar trabajo.
worker Reclamar pasos e informar de lo que les pasó. Es la clave que vive en el proceso que ejecuta los cuerpos de los pasos. Una reclamación devuelve el input del paso y el output de cada paso anterior, así que lee payloads, y no debe poder además arrancar ejecuciones ni cambiar datos almacenados.
admin Destruir datos y leer el registro de auditoría. Borrar no es leer.

Qué ámbito toma cada endpoint se declara endpoint por endpoint en la especificación y se publica en la referencia generada de la API HTTP, así que hay una sola respuesta a «qué necesita este endpoint» en lugar de una por documento.

La lista de reserva, y qué significa para una clave que ya tienes

Una lista write sin definir recae en la lista ingest, y una lista admin sin definir recae en la lista read. Las listas execute y worker sin definir recaen en la lista write, y por tanto en la lista ingest cuando esa tampoco está definida. Eso es lo que impide que una actualización revoque el acceso en todos los despliegues configurados antes de que existieran los ámbitos más estrechos. Una clave de administración vale además para execute y worker, para que un operador pueda conducir una ejecución a mano. Juntas, estas reglas significan que, en un daemon que solo tiene una clave de ingesta y una clave de lectura:

  • la clave de ingesta también adjunta puntuaciones y feedback, arranca ejecuciones y reclama pasos, y
  • la clave de lectura también borra trazas y, a través de la reserva de administración, arranca ejecuciones y reclama pasos.

Así que una clave de lectura es una clave de borrado hasta que definas --admin-key, y las dos claves llegan a la superficie de ejecuciones hasta que definas --execute-key, --worker-key y --admin-key. Reparte las claves sabiéndolo. Nada se estrecha solo. Cada ámbito tiene su opción, --ingest-key, --read-key, --write-key, --admin-key, --execute-key y --worker-key, y cada una toma una lista; todas se definen además por proyecto en el archivo de configuración.

Sin claves, todos los ámbitos están abiertos

Ese valor por defecto es deliberado: una primera ejecución en un portátil no debe necesitar una credencial. Es también lo más peligroso de una instancia expuesta, así que, cuando el daemon se enlaza a una dirección que no es de loopback, registra al arrancar un aviso por cada superficie abierta a quien no debería alcanzarla. Cada aviso nombra la superficie y las opciones que la cierran. Ejecuta mcpd config antes de exponer un puerto: imprime la configuración a la que se resuelve una invocación y termina. Lleva una línea por ámbito, y nombra la lista en la que ha recaído un ámbito, de modo que admin auth key required (read keys) te dice que las claves de lectura todavía pueden borrar. Docker y Kubernetes muestra la salida.

cerrado antes de ser accesible

mcpd --ingest-key "$INGEST" --read-key "$READ"

Un proyecto es una frontera de tenencia

Una clave configurada en el nivel superior concede todos los proyectos. Una clave listada en cambio bajo un proyecto con nombre en el bloque projects del archivo de configuración concede ese proyecto y ningún otro, y cada proyecto tiene su propia lista por ámbito. Una misma clave puede aparecer en varios proyectos, que es como un equipo de plataforma tiene una clave que abarca lo que opera sin tener el servidor entero.

En la ruta de escritura la comprobación es por señal, no por solicitud:

  • Un lote puede nombrar varios proyectos. Las señales que nombran un proyecto al que la clave no llega se descartan y se informan en la respuesta como rechazadas, con el motivo, así que una flota apuntando al proyecto equivocado se descubre desde la respuesta que ya lee. El resto del lote se almacena.

En la ruta de lectura, cada endpoint que nombra un proyecto lo comprueba contra los permisos de la clave: trazas, spans, logs, métricas, puntuaciones, feedback, analíticas, datasets, experimentos y el registro de auditoría. Los elementos de un dataset se direccionan por el id del dataset, y el daemon busca qué proyecto es dueño de ese dataset y comprueba la clave contra él, de modo que una clave que no puede abrir el proyecto no puede leer los casos capturados de él. Un id que no existe se rechaza igual que uno al que la clave no llega, así que el endpoint no sirve para averiguar qué ids existen. Tres endpoints no toman proyecto y son accesibles para cualquier clave que tenga el ámbito de lectura, sean cuales sean los proyectos a los que está limitada: la lista de proyectos, el catálogo de evaluadores y el conjunto de capacidades del almacenamiento (GET /v1/api/capabilities). La lista de proyectos se filtra por los permisos de la clave, así que una clave limitada a un proyecto solo ve los proyectos a los que llega. En una instancia compartida, los nombres de proyecto suelen ser nombres de equipos o de clientes. El estado de los monitores y el estado de los objetivos estuvieron en esa lista hasta que se filtraron por el alcance de proyecto de la clave, que es lo que siempre debieron hacer: un monitor nombra el proyecto que vigila y lleva el webhook al que envía. La API solo nombra el host de ese webhook, porque la URL de un webhook suele ser su propia credencial.

Un rechazo dice cuál de los dos problemas es

Una clave ausente o no reconocida es un 401, y una clave reconocida que este endpoint o este proyecto no admite es un 403. La división es operativa: un 401 te manda a buscar una errata en una credencial, un 403 te dice que la credencial es correcta y los permisos no.

cuatro rechazos
$ curl -sw "%{http_code} " "$MCPD/v1/api/traces?project=acme"401  this endpoint needs an API key; send it as `Authorization: Bearer <key>`$ curl … -H "authorization: Bearer $TYPO"401  that key is not one this daemon knows$ curl … -H "authorization: Bearer $INGEST"403  that key is not valid for reading$ curl … -H "authorization: Bearer $BILLING_READ"403  that key does not reach the project "acme"

Ese último mensaje es el mismo exista el proyecto o no. Confirmar qué nombres de proyecto son reales es una fuga pequeña, y en un daemon compartido esos nombres son los nombres de tus clientes.

El registro de auditoría

Devolver un payload almacenado fuera de mcpd queda anotado, y también cada borrado, así que la pregunta «quién leyó esta ejecución» tiene respuesta. Una fila lleva cuándo, qué clave, qué acción, qué proyecto, qué traza o dataset nombró y si la solicitud se sirvió o se rechazó. GET /v1/api/audit lo lee de vuelta, lo más reciente primero, y toma el ámbito admin.

Se anotan dieciocho acciones: get_trace, list_spans, list_logs, list_feedback, list_dataset_items, capture_into_dataset, read_trace_aspect_notes, get_theme, get_theme_flow, list_theme_history, list_theme_examples, delete_trace, erase_subject, y cinco sobre ejecuciones duraderas: list_runs, get_run, list_run_events, get_step_cause y cancel_run. La frontera es el payload, no la solicitud. Listar trazas devuelve una proyección de nombres, tiempos, recuentos de tokens y costo sin ningún prompt ni respuesta dentro, así que no queda anotado; abrir una de esas trazas sí, porque ahí está el texto. Listar ejecuciones duraderas sí queda anotado, porque cada ejecución de la lista lleva su entrada, su salida y su estado tal como se enviaron. Una lectura rechazada de una ejecución se anota bajo el proyecto dueño de la ejecución.

Las llamadas de worker no se anotan: reclamar un paso, declarar que ha empezado, el heartbeat, complete y fail. Un lease sí lleva la entrada del paso y las salidas de los pasos anteriores, pero el bucle del worker reclama muchas veces por minuto, y una fila por reclamación enterraría las lecturas que hizo una persona. Trata la clave worker como una clave que lee todos los payloads de las ejecuciones que sirve. Arrancar una ejecución tampoco se anota; volver a enviar una clave de idempotencia ya en uso responde con la ejecución que esa clave arrancó, así que trata la clave execute como una clave que puede leer las ejecuciones que arranca.

Cinco propiedades que un revisor debería conocer:

  • La clave nunca se escribe. El principal es un hash SHA-256 truncado de la clave, que distingue una clave de otra sin guardar un verificador para ella. Una solicitud sin clave en un daemon abierto se anota como anonymous.
  • La persona viene de tu ingress, o de ningún sitio. mcpd no tiene cuentas de usuario, así que por sí sola una fila atribuye una acción a una clave y no a una persona. El campo subject lleva una identidad cuando --trusted-identity-header y --trusted-proxy-cidr están ambos definidos, y está vacío en caso contrario, que es lo que hay por defecto. mcpd nunca establece esa identidad por sí mismo; anota lo que afirmó una dirección que tú nombraste.
  • Un rechazo también queda anotado. La fila se escribe antes de devolver el 403, porque un intento rechazado sobre el proyecto de otro equipo es exactamente la fila para la que existe un registro de auditoría. Leer el registro no queda anotado: crecería sin que jamás se leyera un payload, y la fila no diría nada que quien tiene la clave de administración no sepa ya.
  • Anota qué clave y, opcionalmente, quién. La clave se guarda como un hash truncado, nunca la clave en sí, así que el registro no es un sitio donde se acumulen credenciales. Eso responde a «una clave de administración borró esto» y no a «qué persona lo hizo», porque mcpd no tiene proveedor de identidad. Si quieres la segunda respuesta, dale la cabecera que tu ingress ya envía y las direcciones autorizadas a enviarla: --trusted-identity-header x-forwarded-user con --trusted-proxy-cidr 10.42.0.0/16. Hacen falta las dos mitades. Una cabecera es una cadena que puede enviar cualquiera que alcance el puerto, así que mcpd la lee solo desde una dirección que tú nombraste; una solicitud desde cualquier otro sitio se sirve con normalidad y su fila no lleva subject, que es la respuesta honesta en lugar de una suposición. Sin ninguna de las dos opciones definida, nunca se anota un subject, y eso es lo que hay por defecto.
  • La retención no lo recorta. La pasada de retención elimina spans, logs, métricas, trazas, puntuaciones, feedback, notas de aspecto y ejecuciones duraderas terminadas. Deja el registro de auditoría, porque un registro que caducara junto con los datos que describe no podría decir quién leyó los datos borrados.
  • Nunca hace fallar la solicitud que describe. Una fila que no se puede escribir se reporta en los logs y se cuenta como mcpd_audit_write_failures_total en /metrics, y la lectura devuelve igual. Un registro de auditoría capaz de tumbar la ruta de lectura es un riesgo peor que el que evita, así que vigila ese contador en lugar de confiar en que las solicitudes fallen.

Qué no cubre ninguna clave

  • La interfaz. El marcado en / se sirve a cualquiera que alcance el puerto. Los datos que hay detrás no: cada solicitud que hace pasa por la API de consulta y se topa con las mismas claves. No hay pantalla de inicio de sesión en el daemon.
  • Las sondas y /metrics. Sin autenticar a propósito, porque un kubelet y un recolector de métricas no pueden llevar una clave. Informan del estado, la versión, el tiempo en marcha, el nombre del backend de almacenamiento y sus contadores, nunca payloads.
  • El origen. La API de consulta y las sondas responden con Access-Control-Allow-Origin: *, así que una página de navegador en cualquier origen puede llamarlas. mcpd no usa cookies ni credenciales ambientales, así que esa página todavía tiene que tener una clave: una política de origen permisiva no se la entrega.

Qué hace la ocultación, y qué no puede hacer

La ocultación está activada por defecto y corre antes de la escritura, como etapa tres del pipeline de entrada. Un valor que una regla capta nunca llega a la base de datos, que es una garantía distinta de la de un valor borrado después, y la diferencia es la que pregunta un auditor. Lo que no puede hacer es evitar que los bytes salgan a la red: ya la han cruzado cuando corre la regla. Para eso, los SDK llevan MCPD_CAPTURE_PAYLOADS=false, que descarta input y output antes de encolar un span y deja todo lo demás.

Ingest pipeline
  1. Decode

    OTLP or native, gzip or plain

  2. Sample

    Decided once, at the root

  3. Redact

    Before anything is written

  4. Price

    From the configured price book

  5. Store

    One transaction per batch

Redaction is stage three, not a cleanup job. A value that a rule matches never reaches storage at all.

Una regla es un conjunto de subcadenas en minúscula y un modo. Una clave coincide cuando su nombre, en minúscula, contiene una de ellas. Los valores coincidentes se enmascaran, se enmascaran en parte, se pasan por un hash con clave o se descartan. Cuando una clave coincidente contiene un objeto, todo lo que hay bajo ella se oculta también, se llamen como se llamen las claves hijas, y un secreto enviado como número JSON se trata igual que uno enviado como cadena. La tabla completa de reglas está en Ocultación y retención.

Ahora el límite, que una revisión va a preguntar y que ninguna configuración quita. Las reglas coinciden con nombres de campo, no con la forma de un valor. Una clave de API que un usuario pegó en mitad de una frase, y que llega en un campo llamado content o question, no la capta nada del pipeline. El nombre del campo es inocente, así que el valor se almacena entero.

Por eso la ocultación detiene una cosa concreta:

  • Detiene la captura estructurada de credenciales e identificadores, cuando una credencial llega en un campo que se llama como lo que es.
  • No analiza texto libre, y no es un clasificador de contenido.
  • No detecta un secreto por su forma ni por su entropía.
  • No toca el nombre de un span ni el mensaje de un registro de log. Esos son identificadores que sirven para agrupar y no payloads, y reescribirlos rompería la agrupación. No construyas el nombre de un span con el correo de un cliente.
  • No toca el almacén de ejecuciones. Los payloads de una ejecución duradera son su estado de trabajo y se guardan tal como se enviaron; solo se ocultan los spans que una ejecución escribe en el almacén de trazas.

Un control sí trabaja sobre la forma, y es detección a posteriori en lugar de prevención: el evaluador integrado no_leaked_secret baja la nota de una ejecución cuya respuesta lleva un token con aspecto de credencial, por prefijo conocido o por una tirada larga alfanumérica con mayúsculas y minúsculas. Puntúa una ejecución que ya está almacenada. No quita nada, no lee el prompt, y solo corre cuando lo pides. Tómalo como una forma de enterarte de que esto ha pasado, no como una segunda línea de ocultación.

Dos límites se aplican haya coincidido una regla o no. Toda cadena de más de 128 KiB se trunca en una frontera de carácter con una marca, y el recorrido se detiene a una profundidad de 32. Ambos existen para que un documento desbocado no pueda decidir el tamaño de tu base de datos ni clavar una CPU.

--no-redact apaga el pipeline entero, lo que solo tiene sentido cuando algo aguas arriba ya oculta. Donde la ocultación aplica un hash a un identificador, define --redact-key: la clave integrada es una constante en código publicado, y los identificadores vienen de espacios pequeños, así que un hash sin clave se recupera probando candidatos.

Retención y supresión

--retention-days borra las señales más viejas que ese número de días. Omítela y no se borra nunca nada, que es el valor por defecto correcto: una herramienta que descartara en silencio las trazas del mes pasado se descubriría en el peor momento.

La pasada corre en un temporizador horario dentro del daemon, nunca dentro de la solicitud de un usuario. Cubre cada proyecto nombrado en el bloque projects, cada uno con su propio retention_days donde lo define y con la cifra global donde no, más el proyecto por defecto. Un proyecto sin ventana en ningún sitio no se recorta en absoluto.

Para una solicitud sobre una persona, DELETE /v1/api/subjects/{user_id}?project=… elimina todas las ejecuciones anotadas contra esa persona en el proyecto, con los spans, logs, métricas, puntuaciones y feedback adjuntos, y responde con un recuento por señal. Al identificador se le aplica un hash con la misma clave antes de compararlo, así que la solicitud funciona contra identificadores con hash sin que tú tengas que aplicar ninguno, y la respuesta dice cuántos identificadores almacenados estaban en una forma que la solicitud no pudo comparar. Para eliminar una ejecución en lugar de una persona, DELETE /v1/api/traces/{trace_id} la elimina junto con todo lo adjunto en una sola transacción. Ambas toman el ámbito admin y ambas quedan en el registro de auditoría. Ocultación y retención recorre la solicitud de supresión y lo que puede y no puede prometer.

Qué no hace mcpd

Nada de lo siguiente existe en esta compilación. Se enumera porque una revisión que lo descubre más tarde es un desenlace peor que una revisión que lo lee aquí.

No está en mcpdQué hacer en su lugar
Terminación TLS. mcpd habla HTTP en claro. Los prompts y las completions cruzan ese enlace enteros. Termina TLS en tu ingress o en tu malla de servicios, y mantén el puerto del daemon fuera de cualquier red no confiable.
SSO. Sin integración con proveedores de identidad, sin OIDC, sin SAML, sin pantalla de inicio de sesión. Pon la interfaz detrás del proxy de identidad que ya operas. La identidad de una persona es la que establezca ese proxy.
SCIM. Sin aprovisionamiento de usuarios ni de grupos, porque mcpd no tiene el concepto de cuenta de usuario. Aprovisiona en el proxy. Dentro de mcpd la unidad es una clave, añadida a una lista o quitada de ella.
Roles más allá de los seis ámbitos. La autorización es un ámbito y una lista de proyectos. No hay rol por endpoint ni grupo. Separa por clave y por proyecto. Si una distinción importa, hazla un proyecto.
Establecer quién es una persona. El registro de auditoría puede anotar un subject, pero mcpd nunca averigua quién es quien llama. Anota lo que afirmó tu ingress, y solo desde una dirección que tú nombraste. Define --trusted-identity-header y --trusted-proxy-cidr para que la cabecera que tu proxy ya envía llegue a la fila de auditoría. Sin ellas una fila atribuye una acción a una clave, y emitir una clave por equipo estrecha el hash.
Ciclo de vida de las claves. Las claves son listas leídas al arrancar. No hay caducidad, ni calendario de rotación, ni cuota por clave. El techo que sí existe es por proyecto: --quota-per-minute limita cuántas señales puede almacenar un proyecto en un minuto, sea cual sea la clave que las envió. Configura varias claves, una por equipo o por flota, para que revocar una sea una edición y un reinicio en lugar de un redespliegue de todo lo que emite un span. Haz que una flota sea su propio proyecto si necesita su propio techo.
Cifrado en reposo. mcpd escribe archivos en claro o filas en claro. Cifra el volumen, o usa una base de datos que lo haga.

Informar de algo

Las vulnerabilidades sospechadas van a [email protected] con "Security" en el asunto, no a una incidencia pública. El SECURITY.md del repositorio indica el alcance y qué contiene un buen informe. Una instancia sin autenticar que alguien decidió exponer es un valor por defecto documentado, no un hallazgo. Cualquier cosa que pase una clave que sí estaba configurada, que pase una regla de ocultación configurada, o que cruce una frontera de proyecto, sí lo es.

A continuación