Saltar al contenido principal
En MKA1, el límite práctico de inquilino es:
  • una clave API de cuenta distinta
  • un ID de usuario final delegado opcional mediante X-On-Behalf-Of
  • la propiedad y el uso de recursos aguas abajo registrados contra ese contexto autenticado
Utiliza la siguiente guía paso a paso cuando quieras verificar que dos inquilinos están aislados entre sí en la práctica. Te proporciona un flujo exacto de Inquilino A / Inquilino B que puedes ejecutar de principio a fin.

Verifica el aislamiento de inquilinos en la práctica

Trata cada inquilino como una cuenta separada con su propia clave API. El objetivo es demostrar cuatro cosas:
  1. El Inquilino A y el Inquilino B usan claves API diferentes.
  2. El Inquilino A y el Inquilino B pueden tener configuraciones de cuota diferentes.
  3. El Inquilino A y el Inquilino B pueden tener políticas diferentes.
  4. Un recurso creado bajo el Inquilino A no es accesible bajo el Inquilino B.

Paso 1: define dos inquilinos

Comienza con dos claves API separadas.
export TENANT_A_KEY="<tenant-a-api-key>"
export TENANT_B_KEY="<tenant-b-api-key>"
export TENANT_A_USER="tenant_a_user"
export TENANT_B_USER="tenant_b_user"
Si ya tienes dos claves de producción o staging, puedes usarlas directamente. Si necesitas aprovisionar dos claves con límites diferentes para la demostración, créalas por separado.
curl https://apigw.mka1.com/api/v1/authentication/api-key/create/rate-limited \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Cookie: better-auth.session_token=<tenant-a-session>' \
  --data '{
    "name": "Tenant A demo key",
    "rateLimitEnabled": true,
    "rateLimitTimeWindow": 3600000,
    "rateLimitMax": 60
  }'
curl https://apigw.mka1.com/api/v1/authentication/api-key/create/rate-limited \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Cookie: better-auth.session_token=<tenant-b-session>' \
  --data '{
    "name": "Tenant B demo key",
    "rateLimitEnabled": true,
    "rateLimitTimeWindow": 3600000,
    "rateLimitMax": 600
  }'
Esto establece inmediatamente las dos primeras piezas de aislamiento:
  • El Inquilino A y el Inquilino B no comparten una clave.
  • El Inquilino A y el Inquilino B no comparten la misma configuración de cuota.

Paso 2: configura políticas diferentes para cada inquilino

Ahora asigna a los dos inquilinos diferentes políticas de guardrails. Configura el Inquilino A para bloquear la palabra confidential.
curl https://apigw.mka1.com/api/v1/llm/guardrails \
  --request PUT \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer ${TENANT_A_KEY}" \
  --data '{
    "guardrails": [
      {
        "mode": "ban_words",
        "enabled": true,
        "config": {
          "words": ["confidential"]
        }
      }
    ]
  }'
Configura el Inquilino B con una política diferente.
curl https://apigw.mka1.com/api/v1/llm/guardrails \
  --request PUT \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer ${TENANT_B_KEY}" \
  --data '{
    "guardrails": [
      {
        "mode": "ban_words",
        "enabled": true,
        "config": {
          "words": ["internal-only"]
        }
      }
    ]
  }'
Luego prueba el mismo contenido contra ambos inquilinos. El Inquilino A debería bloquear esto:
curl https://apigw.mka1.com/api/v1/llm/guardrails/test \
  --request POST \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer ${TENANT_A_KEY}" \
  --data '{
    "content": "Summarize this confidential roadmap."
  }'
El Inquilino B debería evaluar el mismo contenido contra su propia política en lugar de la del Inquilino A:
curl https://apigw.mka1.com/api/v1/llm/guardrails/test \
  --request POST \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer ${TENANT_B_KEY}" \
  --data '{
    "content": "Summarize this confidential roadmap."
  }'
El resultado esperado es:
  • El Inquilino A falla porque confidential está bloqueado en la política del Inquilino A.
  • El Inquilino B no hereda la política del Inquilino A.
Esta es la demostración práctica más clara de políticas independientes por inquilino.

Paso 3: demuestra cuotas separadas

Después de crear dos claves con diferentes límites, envía el mismo tipo de solicitud a través de ambas claves. El Inquilino A usa la clave de bajo límite:
for i in $(seq 1 70); do
  curl https://apigw.mka1.com/api/v1/llm/responses \
    --request POST \
    --silent \
    --output /dev/null \
    --write-out "tenant-a request $i -> %{http_code}\n" \
    --header 'Content-Type: application/json' \
    --header "Authorization: Bearer ${TENANT_A_KEY}" \
    --header "X-On-Behalf-Of: ${TENANT_A_USER}" \
    --data '{
      "model": "openai:gpt-5",
      "input": "Reply with the word ok."
    }'
done
El Inquilino B usa la clave de mayor límite:
for i in $(seq 1 70); do
  curl https://apigw.mka1.com/api/v1/llm/responses \
    --request POST \
    --silent \
    --output /dev/null \
    --write-out "tenant-b request $i -> %{http_code}\n" \
    --header 'Content-Type: application/json' \
    --header "Authorization: Bearer ${TENANT_B_KEY}" \
    --header "X-On-Behalf-Of: ${TENANT_B_USER}" \
    --data '{
      "model": "openai:gpt-5",
      "input": "Reply with the word ok."
    }'
done
El resultado esperado es:
  • El Inquilino A comienza a recibir 429 antes.
  • El Inquilino B continúa teniendo éxito porque tiene una clave y una cuota diferente.
Esto demuestra que los inquilinos no comparten un limitador global.

Paso 4: demuestra el aislamiento de recursos

Crea un recurso bajo el Inquilino A. Una conversación es un ejemplo sencillo porque es visible a través de la API pública.
curl https://apigw.mka1.com/api/v1/llm/conversations \
  --request POST \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer ${TENANT_A_KEY}" \
  --header "X-On-Behalf-Of: ${TENANT_A_USER}" \
  --data '{
    "metadata": {
      "tenant": "A",
      "demo": "segregated-tenants"
    }
  }'
Supón que la respuesta devuelve conv_tenant_a_123. El Inquilino A puede leerlo:
curl https://apigw.mka1.com/api/v1/llm/conversations/conv_tenant_a_123 \
  --header "Authorization: Bearer ${TENANT_A_KEY}" \
  --header "X-On-Behalf-Of: ${TENANT_A_USER}"
El Inquilino B no debería poder leer el mismo recurso:
curl https://apigw.mka1.com/api/v1/llm/conversations/conv_tenant_a_123 \
  --header "Authorization: Bearer ${TENANT_B_KEY}" \
  --header "X-On-Behalf-Of: ${TENANT_B_USER}"
También puedes mostrar el aislamiento de listas:
curl 'https://apigw.mka1.com/api/v1/llm/conversations?limit=20' \
  --header "Authorization: Bearer ${TENANT_B_KEY}" \
  --header "X-On-Behalf-Of: ${TENANT_B_USER}"
El Inquilino B no debería ver la conversación del Inquilino A en su lista. Esta es la prueba práctica de enclave de inquilino:
  • se utiliza la misma superficie de API
  • se utiliza la misma plataforma
  • pero la identidad, la política, la cuota y la propiedad de los recursos se aplican por separado

Paso opcional 5: muestra libros de uso separados

Si quieres una prueba visible adicional, consulta el uso por separado para cada inquilino.
curl 'https://apigw.mka1.com/api/v1/llm/usage/responses?start_time=1704067200&end_time=1704153600&bucket_width=1d&group_by=api_key_id' \
  --header "Authorization: Bearer ${TENANT_A_KEY}"
curl 'https://apigw.mka1.com/api/v1/llm/usage/responses?start_time=1704067200&end_time=1704153600&bucket_width=1d&group_by=api_key_id' \
  --header "Authorization: Bearer ${TENANT_B_KEY}"
El objetivo de este paso no es solo la facturación. Demuestra que la plataforma registra la actividad contra el contexto autenticado del inquilino en lugar de fusionar todo el tráfico en una sola cuenta indiferenciada.

Qué demuestra este recorrido

Si ejecutas el recorrido anterior, puedes hacer estas afirmaciones exactas:
  • Claves independientes: El Inquilino A y el Inquilino B se autentican con claves API diferentes.
  • Cuotas independientes: El Inquilino A y el Inquilino B pueden tener diferentes configuraciones de límite de tasa y recibir diferentes comportamientos de 429.
  • Políticas independientes: El Inquilino A y el Inquilino B pueden establecer diferentes guardrails y obtener diferentes resultados sobre el mismo contenido.
  • Aislamiento real: Un recurso creado bajo el Inquilino A no es legible bajo el Inquilino B.
La superficie de la API es compartida, pero el contexto de inquilino autenticado, la aplicación de cuotas, la configuración de políticas y la propiedad de los recursos no se comparten.

Cómo funciona el modelo de identidad por debajo

La autenticación en la API de MKA1 tiene tres capas:
  • Tu clave API identifica tu cuenta.
  • X-On-Behalf-Of identifica el usuario final para el que actúa tu servidor.
  • Un JWT intercambiado da a un servicio aguas abajo una credencial de corta duración derivada de esa clave API y contexto de usuario final.
La versión corta es simple:
  • Envía Authorization: Bearer <mka1-api-key> en cada solicitud del lado del servidor.
  • Añade X-On-Behalf-Of cuando la solicitud pertenezca a uno de tus usuarios finales.
  • Usa POST /api/v1/authentication/api-key/exchange-token cuando otro servicio deba recibir un token de corta duración en lugar de tu clave API.
Si solo necesitas el patrón de uso, comienza con la guía de autenticación. Esta página explica lo que sucede después de que la solicitud llega al gateway.

La ruta de la solicitud

La API de MKA1 no pide a los servicios aguas abajo que validen los tokens bearer por sí mismos. Las solicitudes pasan primero por el gateway, y el gateway inyecta cabeceras de identidad confiables para el resto de la plataforma. Cuando también envías X-On-Behalf-Of, el gateway mantiene esa identidad de usuario final delegada con la solicitud: El intercambio de JWT añade un paso extra:

Los tres patrones principales

Patrón 1: solicitudes solo backend

Úsalo cuando tu backend esté llamando a la API de MKA1 para su propio flujo de trabajo y no haya un usuario final separado que rastrear.
curl https://apigw.mka1.com/api/v1/llm/responses \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <mka1-api-key>' \
  --data '{
    "model": "openai:gpt-5",
    "input": "Write a short release note for our API update."
  }'
const response = await fetch("https://apigw.mka1.com/api/v1/llm/responses", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.MKA1_API_KEY}`,
  },
  body: JSON.stringify({
    model: "openai:gpt-5",
    input: "Write a short release note for our API update.",
  }),
});

Patrón 2: integración servidor multiusuario

Úsalo cuando tu backend esté haciendo la solicitud para uno de los usuarios de tu aplicación.
curl https://apigw.mka1.com/api/v1/llm/responses \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <mka1-api-key>' \
  --header 'X-On-Behalf-Of: user_123' \
  --data '{
    "model": "openai:gpt-5",
    "input": "Summarize this support ticket."
  }'
const response = await fetch("https://apigw.mka1.com/api/v1/llm/responses", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.MKA1_API_KEY}`,
    "X-On-Behalf-Of": "user_123",
  },
  body: JSON.stringify({
    model: "openai:gpt-5",
    input: "Summarize this support ticket.",
  }),
});
Este es el patrón correcto cuando deseas que las solicitudes, archivos, respuestas, conversaciones y el uso permanezcan asociados a un ID de usuario final estable de tu propio sistema.

Patrón 3: intercambia tu clave API por un JWT de corta duración

Úsalo cuando otro servicio deba recibir un token de tiempo limitado en lugar de tu clave API de larga duración.
curl https://apigw.mka1.com/api/v1/authentication/api-key/exchange-token \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <mka1-api-key>' \
  --data '{
    "audience": "https://my-service.example.com",
    "externalUserId": "user_123",
    "expiresIn": 3600,
    "permissions": ["agent:create", "agent:read"]
  }'
const response = await fetch("https://apigw.mka1.com/api/v1/authentication/api-key/exchange-token", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.MKA1_API_KEY}`,
  },
  body: JSON.stringify({
    audience: "https://my-service.example.com",
    externalUserId: "user_123",
    expiresIn: 3600,
    permissions: ["agent:create", "agent:read"],
  }),
});

const { token } = await response.json();

Qué cabeceras envías vs cuáles inyecta la plataforma

Cabeceras que envías

CabeceraRequeridoSignificado
AuthorizationTu clave API de MKA1 o JWT intercambiado en formato bearer
X-On-Behalf-OfNoTu identificador de usuario final estable para solicitudes delegadas del lado del servidor

Cabeceras confiables inyectadas dentro de la plataforma

CabeceraProducido porSignificado
X-User-IDGatewayEl ID de usuario autenticado de la cuenta
X-Api-Key-IDGatewayEl ID de clave API resuelto
X-User-RoleGatewayEl contexto de rol autenticado
X-Api-Key-PermissionsGatewayPermisos adjuntos a la clave API
X-Exchange-JWT-External-User-IDGatewayEl ID de usuario final delegado
X-Exchange-JWT-PermissionsGatewayPermisos incrustados en el JWT intercambiado
Los clientes envían Authorization y a veces X-On-Behalf-Of. Los clientes no envían directamente las cabeceras internas X-User-ID o X-Api-Key-ID. Estas son derivadas por el gateway después de la validación.

Cómo funciona el intercambio de JWT

POST /api/v1/authentication/api-key/exchange-token convierte una clave API de larga duración en un token de corta duración para otro servicio. El cuerpo de la solicitud tiene cuatro campos significativos:
  • audience: la URL exacta del servicio que debe aceptar el JWT
  • externalUserId: el ID de usuario final colocado en el subject del JWT
  • expiresIn: tiempo de vida del token en segundos, de 300 a 2592000
  • permissions: un subconjunto opcional de los permisos de la clave API
La respuesta es:
{
  "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Una carga útil decodificada del token se ve así:
{
  "ak": "cm4xqw8ij0001w8lcmj9tn6v7",
  "sub": "user_123",
  "iat": 1734624000,
  "exp": 1734627600,
  "iss": "https://exchange.meetkai.com",
  "aud": "https://my-service.example.com",
  "permissions": ["agent:create", "agent:read"]
}
Estas claims son las más importantes:
  • ak: el ID de clave API usado para búsqueda y aplicación de límite de tasa
  • sub: tu identidad de usuario final delegado
  • aud: el servicio que debe aceptar el token
  • permissions: el conjunto de capacidades permitidas para ese token

Cómo funciona el alcance de inquilino en la práctica

La decisión de diseño más importante es que la identidad de usuario final es explícita. Si envías X-On-Behalf-Of: user_123, los servicios aguas abajo pueden mantener los recursos y el uso asociados a ese usuario. Esto es importante para:
  • conversaciones guardadas
  • respuestas almacenadas
  • archivos y almacenes vectoriales
  • uso y trazabilidad por usuario
  • comprobaciones de autorización delegada
Cuando omites X-On-Behalf-Of, las solicitudes se ejecutan como trabajo de cuenta solo backend. Cuando lo incluyes, la solicitud se convierte en una solicitud delegada para un usuario final específico. Utiliza un identificador estable de tu propio sistema. No uses un nombre visible mutable a menos que ese ya sea tu ID de usuario canónico.

Límites de tasa y qué deben esperar los llamadores

Las claves API pueden tener configuraciones de límite de tasa personalizadas. La ruta de autenticación aplica los límites de tasa por clave API antes de que la solicitud llegue a los servicios aguas abajo. En la práctica, esto significa:
  • dos claves API diferentes pueden tener límites diferentes
  • una clave API sobrecargada no implica que otra clave esté agotada
  • un 429 es parte de la ruta de autenticación, no un error del modelo aguas abajo
El flujo de intercambio de token también permanece vinculado a la clave API de origen. Eso significa que un JWT aguas abajo aún hereda el comportamiento de límite de tasa a nivel de cuenta asociado a la clave fuente. Si deseas grupos de tráfico separados para diferentes aplicaciones o cargas de trabajo, utiliza claves API separadas.

Errores comunes

Enviar X-On-Behalf-Of desde código de navegador

No expongas tu clave API en código de navegador o cliente móvil. Tu servidor debe llamar a la API de MKA1 y adjuntar X-On-Behalf-Of allí.

Usar un identificador de usuario final inestable

Utiliza un ID interno duradero como user_123. No alternes entre direcciones de correo, nombres de usuario y nombres visibles para el mismo usuario.

Usar el audience incorrecto en JWTs intercambiados

El servicio aguas abajo debe validar el token para la audiencia prevista. Establece audience en la URL real del servicio que debe aceptar el JWT.

Solicitar permisos más amplios que la clave API

El endpoint de intercambio de token solo permite un subconjunto de los permisos de la clave API. Si solicitas un permiso que la clave API no tiene, el intercambio falla.

Tratar las cabeceras internas propagadas como cabeceras públicas de cliente

Cabeceras como X-User-ID y X-Api-Key-ID son parte de la ruta de solicitud interna confiable. No son un sustituto de Authorization.

Apéndice de rutas de código

Los siguientes fragmentos muestran la forma principal de implementación detrás del comportamiento público.

Kong valida el token bearer e inyecta cabeceras confiables

authorizationHeader = utils.StripBearer(authorizationHeader)

if utils.IsApiKey(authorizationHeader) {
    resp, err := c.authenticationClient.ValidateApiKey(ctx, authorizationHeader)
    if err != nil {
        c.handleAuthenticationServiceError(kong, err)
        return
    }

    c.writeApiKeyInfoToUnderlyingHeaders(kong, resp.GetApiKeyInfo())
    c.writeUserInfoToUnderlyingHeaders(kong, resp.GetUserInfo())

    onBehalfOfHeader, _ := kong.Request.GetHeader(headerOnBehalfOf)
    if onBehalfOfHeader != "" {
        c.writeExchangeJwtInfoToUnderlyingHeaders(kong, &authentication.ExchangeJWTInfo{
            ExternalUserId: onBehalfOfHeader,
            Permissions:    resp.GetApiKeyInfo().GetPermissions(),
        })
    }

    return
}
Por eso los servicios aguas abajo pueden confiar en X-User-ID, X-Api-Key-ID y X-Exchange-JWT-External-User-ID después de la validación del gateway.

El endpoint exchange-token verifica la clave API, revisa permisos y firma el JWT

const apiKeyResult = await auth.api.verifyApiKey({
  body: {
    key: apiKey,
  },
});

if (!apiKeyResult.valid || !apiKeyResult.key) {
  ctx.set.status = 401;
  return { message: "Invalid API key" };
}

const apiKeyPermissions = Object.entries(verifiedKey.permissions ?? {}).map(
  ([key, value]) => `${key}:${value}`,
);
const permissions = validatedBody.permissions ?? apiKeyPermissions;

if (permissions.some((permission) => !apiKeyPermissions.includes(permission))) {
  ctx.set.status = 401;
  return { message: "Permissions mismatch" };
}
Luego firma una carga útil que incluye el ID de clave API, el subject de usuario final delegado y los permisos filtrados:
payload: {
  ak: verifiedKey.id,
  sub: validatedBody.externalUserId,
  iat: DateTime.now().toUTC().toUnixInteger(),
  permissions: permissions,
}

Los servicios aguas abajo leen la identidad desde cabeceras confiables

const apiKey = reqHeaders.get(KONG_API_KEY_HEADER)?.trim();
const userId = reqHeaders.get(KONG_USER_ID_HEADER)?.trim();
const externalUserId = reqHeaders.get(KONG_EXTERNAL_USER_ID_HEADER)?.trim() ?? "";

if (!apiKey?.length || !userId?.length) {
  throw KONG_AUTH_ERROR;
}

return { apiKey, userId, externalUserId, userRole };
Por eso los servicios detrás del gateway normalmente no analizan los tokens bearer por sí mismos. Operan sobre un contexto de identidad validado en su lugar.

Próximos pasos

Utiliza la guía de autenticación para la versión corta y ejemplos para copiar y pegar. Utiliza la visión general de la API para inspeccionar la referencia generada y el OpenAPI spec en vivo. Luego revisa las páginas de endpoint relevantes en la Referencia de API para detalles exactos de solicitud y respuesta.