Ir al contenido principal
Secrets en agentes IA: inyecta credenciales sin exponerlas

Secrets en agentes IA: inyecta credenciales sin exponerlas

AI Integration
••6 min read•Por Daily Miranda Pardo

Tu agente acaba de decidir cobrar 500€ a un cliente. Para ejecutarlo, necesita la API key de Stripe.

Esa key, ¿dónde está en tu arquitectura?

Si la respuesta es "en el system prompt", "en el tool_result" o "como parámetro que pasa el LLM", tienes una filtración activa. No una vulnerabilidad potencial: una filtración que ya está ocurriendo en cada conversación, en cada trace, en cada log de error que Stripe te devuelve cuando algo falla.

Por qué los agentes filtran credenciales diferente al software clásico

En una aplicación normal, las API keys viven en variables de entorno. El código las lee al arrancar, las usa internamente y nunca salen al exterior. El problema con los agentes es que tienen un intermediario nuevo: el LLM y su contexto de conversación.

Cualquier string que pase por ese contexto —system prompt, mensajes, tool_use blocks, tool_result blocks— puede:

  • Ser registrado en tu plataforma de observabilidad (Langfuse, Helicone, Langsmith)
  • Aparecer en los logs de error de tu proveedor cloud
  • Ser repetido por el modelo en una respuesta si el prompt no está bien controlado
  • Filtrarse al cliente en un stream que no filtras correctamente

Los fallos semánticos no generan excepciones. Una API key en el contexto del LLM no lanza un error: simplemente está ahí, expuesta.

Los tres anti-patrones que más vemos en auditorías

1. La key en el system prompt

// ❌ NUNCA hagas esto
const systemPrompt = `
  Eres un asistente de pagos.
  Para procesar cobros usa la API key de Stripe: ${process.env.STRIPE_KEY}
  Llama a la herramienta charge_card con amount y currency.
`

La key entra en el historial de conversación con el primer mensaje. A partir de ahí viaja en cada request al LLM y en cada log que registres.

2. La key como parámetro de la tool

// ❌ Pasa por el contexto del LLM como tool_result
const tools = [{
  name: "charge_card",
  input_schema: {
    properties: {
      amount: { type: "number" },
      apiKey: { type: "string" } // ❌ el LLM decide qué poner aquí
    }
  }
}]

El LLM decide los valores de los parámetros. Si le dices que existe un campo apiKey, intentará rellenarlo. Si no se lo dices, puede inventárselo. En ambos casos el resultado llega como tool_use block al historial.

3. La key en el tool_result

// ❌ El resultado vuelve al contexto del LLM
const result = await stripe.charges.create({
  amount,
  currency,
  source: token,
}, { apiKey: process.env.STRIPE_KEY })

// Si incluyes los headers de la response en el tool_result, la key puede estar ahí
return JSON.stringify(result) // puede contener metadata con auth info

El patrón correcto: inyección post-LLM

La regla es simple: el LLM decide qué hacer y con qué parámetros de negocio, pero nunca toca las credenciales. Las credenciales se inyectan en la capa de ejecución de herramientas, después de que el LLM haya tomado su decisión.

type ToolCall = {
  name: string
  input: Record<string, unknown>
}

async function executeToolCall(toolCall: ToolCall): Promise<unknown> {
  switch (toolCall.name) {
    case 'charge_card': {
      // La key se carga aquí, DESPUÉS de la decisión del LLM
      const stripeKey = process.env.STRIPE_SECRET_KEY
      if (!stripeKey) throw new Error('Stripe not configured')

      const stripe = new Stripe(stripeKey)
      const result = await stripe.paymentIntents.create({
        amount: toolCall.input.amount as number,
        currency: toolCall.input.currency as string,
      })

      // Devuelves solo lo que el LLM necesita saber, sin auth metadata
      return { id: result.id, status: result.status, amount: result.amount }
    }
    // ... otros tools
  }
}

El LLM nunca ve STRIPE_SECRET_KEY. Solo ve el resultado limpio de la operación.

Masking en traces: la capa que casi nadie implementa

Incluso con el patrón correcto, los mensajes de error de APIs externas pueden contener información de autenticación. Stripe, por ejemplo, incluye el request ID y parte del contexto en sus errores.

Antes de enviar cualquier mensaje a tu plataforma de observabilidad, aplica un mask:

const SENSITIVE_PATTERNS = [
  /sk_live_[a-zA-Z0-9]{24,}/g,
  /sk_test_[a-zA-Z0-9]{24,}/g,
  /Bearer [a-zA-Z0-9+/=_-]{20,}/g,
  /(?:api[_-]?key|apikey|api[_-]?token)["\s]*[:=]["\s]*([a-zA-Z0-9_-]{16,})/gi,
]

function maskSecrets(text: string): string {
  return SENSITIVE_PATTERNS.reduce(
    (masked, pattern) => masked.replace(pattern, '[REDACTED]'),
    text
  )
}

// Aplica antes de logear cualquier mensaje
function sanitizeMessages(messages: Message[]): Message[] {
  return messages.map((m) => ({
    ...m,
    content: typeof m.content === 'string'
      ? maskSecrets(m.content)
      : m.content,
  }))
}

En nuestra implementación de acceso seguro a bases de datos, usamos este mismo patrón de sanitización antes de cualquier log estructurado.

Credenciales por tenant en sistemas multi-cliente

Si construyes un agente que varios clientes usan con sus propias integraciones —cada uno con su clave de Stripe, su CRM diferente, su Twilio propio—, necesitas un layer de resolución de credenciales por tenant:

interface CredentialStore {
  get(tenantId: string, service: string): Promise<string>
}

class EnvCredentialStore implements CredentialStore {
  async get(tenantId: string, service: string): Promise<string> {
    // En producción: AWS Secrets Manager, Doppler, Vault
    // En desarrollo: variables de entorno prefijadas por tenant
    const key = `${tenantId.toUpperCase()}_${service.toUpperCase()}_KEY`
    const value = process.env[key]
    if (!value) throw new Error(`Credential not found: ${key}`)
    return value
  }
}

// El tool executor recibe el tenantId en cada request
async function createToolExecutor(tenantId: string, store: CredentialStore) {
  return {
    async execute(toolCall: ToolCall) {
      const credential = await store.get(tenantId, toolCall.name)
      return runWithCredential(toolCall, credential)
    }
  }
}

Este patrón se complementa directamente con el aislamiento de contexto por cliente que ya tenemos documentado: las credenciales y el contexto son dos superficies de ataque separadas, ambas necesitan su capa de protección.

Lo que vemos cuando auditamos sistemas en producción

El 80% de los agentes que llegan a DAILYMP para auditoría tienen al menos una de estas tres situaciones:

  1. Keys en system prompts porque fue lo más rápido al prototipar y nunca se revisitó
  2. Sin masking en traces, con Langfuse o Datadog registrando el contexto completo sin filtrar
  3. Tool results sin sanitizar, devolviendo el objeto de respuesta completo de la API (con headers y metadata) al contexto del LLM

Ninguno de estos fallos provoca un error en producción. El agente funciona. Los tests pasan. La key simplemente está ahí, en cientos de traces, esperando a que alguien mire.

Implementarlo bien desde el principio

El patrón es claro: el LLM toma decisiones de negocio, la capa de ejecución maneja las credenciales. Nunca al revés.

Si ya tienes un agente en producción y quieres auditar dónde están tus credenciales, escríbeme por WhatsApp y revisamos el stack juntos.

Hablar sobre seguridad en mi agente →

Compartir artículo

LinkedInXWhatsApp

¿Procesos repetitivos en tu empresa?

Descarga gratis el Mapa de Automatización IA — los 5 procesos que más tiempo roban y cómo resolverlos.

Sin spam. Solo el PDF. Puedes darte de baja cuando quieras.

Escrito por Daily Miranda Pardo

Ayudo a empresas a automatizar procesos, crear agentes IA y conectar sistemas inteligentes.