Empezar aquí
Índice de la sección

Permisos y mínimo privilegio

Cómo limitar lo que puede hacer un agente con herramientas acotadas, credenciales de alcance mínimo, políticas en el código, sandbox y auditoría.

Publicado 4 min de lectura

El mínimo privilegio es el principio de dar a cada componente solo los permisos que necesita para su tarea, y nada más. En un agente se aplica a las herramientas que tiene, a las credenciales con las que esas herramientas actúan y al entorno donde se ejecutan. Es la defensa que limita el daño cuando todo lo demás falla: si el modelo se equivoca o sufre una inyección de prompt, solo puede hacer lo que sus permisos permiten.

El modelo no es la frontera de seguridad

El modelo decide qué herramienta pedir. Tu código decide si se ejecuta. Cualquier regla que importe tiene que estar en el código que ejecuta la herramienta, porque es la única parte que no se puede convencer con texto.

flowchart LR
  M[Modelo] -- tool_use --> P[Política en código]
  P -- permitido --> T[Herramienta con credencial limitada]
  P -- requiere aprobación --> H[Persona]
  P -- denegado --> R[tool_result con is_error]
  T --> S[Sistema externo]
  T --> A[Registro de auditoría]

Cuatro niveles de permisos

1. Qué herramientas existen

La herramienta más segura es la que el agente no tiene. Antes de añadir una, pregunta si la tarea la necesita de verdad. Un agente de soporte que consulta pedidos no necesita una herramienta genérica ejecutar_sql; necesita buscar_pedido(id).

En lugar deMejor
ejecutar_sql(consulta)buscar_pedido(pedido_id), listar_pedidos_cliente()
ejecutar_bash(comando)ejecutar_tests(), leer_archivo(ruta) dentro del repo
peticion_http(url, metodo, cuerpo)consultar_tiempo(ciudad) contra una API concreta
enviar_correo(para, asunto, cuerpo)responder_ticket(ticket_id, cuerpo) al cliente del ticket

Las herramientas específicas son más seguras y además el modelo las usa mejor: tienen menos parámetros y su propósito está claro. Más en Diseño de herramientas.

2. Qué parámetros acepta

Valida en el código cada argumento contra lo permitido, no contra lo que el modelo dice que quiere. Si la herramienta lee archivos, comprueba que la ruta resuelta está dentro del directorio permitido. Si envía correo, que el destinatario es el cliente del ticket. Si reembolsa, que el importe no supera el del pedido.

3. Con qué credenciales actúa

La herramienta no debería usar tus credenciales de administrador, sino unas con el alcance justo:

  • Solo lectura siempre que la tarea lo permita. En bases de datos, un usuario con permisos de lectura sobre las tablas necesarias.
  • Acotadas al usuario actual: el agente que atiende a un cliente solo ve los datos de ese cliente. Aplica el filtro en la consulta, con el identificador de la sesión, no con un parámetro que elige el modelo.
  • Caducidad corta cuando sea posible.
  • Separadas por herramienta: si una se filtra, no da acceso a todo.

4. Dónde se ejecuta

Si el agente ejecuta código, navega o usa el ordenador, hazlo en un sandbox: un contenedor o máquina virtual sin acceso a tu red interna, sin credenciales montadas y con salida a internet restringida a una lista de dominios. Para el uso del ordenador es obligatorio. Ver Uso del ordenador.

Ejemplo: políticas en el código

from dataclasses import dataclass
from pathlib import Path

RAIZ = Path("/srv/repo").resolve()

@dataclass
class Sesion:
    usuario_id: str
    rol: str  # "cliente" o "agente_soporte"

class Denegado(Exception):
    pass

def politica_leer_archivo(args: dict, s: Sesion) -> None:
    ruta = (RAIZ / args["ruta"]).resolve()
    if not ruta.is_relative_to(RAIZ):
        raise Denegado(f"Ruta fuera del repositorio: {args['ruta']}")

def politica_reembolsar(args: dict, s: Sesion) -> None:
    if s.rol != "agente_soporte":
        raise Denegado("Solo el personal de soporte puede reembolsar")
    if args["importe"] > 100:
        raise Denegado("Importe por encima del límite automático; requiere aprobación")

POLITICAS = {"leer_archivo": politica_leer_archivo, "reembolsar": politica_reembolsar}

def ejecutar(bloque, funciones: dict, sesion: Sesion) -> dict:
    try:
        if bloque.name not in funciones:
            raise Denegado(f"Herramienta no permitida: {bloque.name}")
        if bloque.name in POLITICAS:
            POLITICAS[bloque.name](bloque.input, sesion)
        salida = funciones[bloque.name](**bloque.input, usuario_id=sesion.usuario_id)
        return {"type": "tool_result", "tool_use_id": bloque.id, "content": str(salida)}
    except Denegado as e:
        return {"type": "tool_result", "tool_use_id": bloque.id, "content": f"Denegado: {e}", "is_error": True}
    except Exception as e:
        return {"type": "tool_result", "tool_use_id": bloque.id, "content": f"Error: {e}", "is_error": True}
import path from "node:path";
import Anthropic from "@anthropic-ai/sdk";

const RAIZ = path.resolve("/srv/repo");
type Sesion = { usuarioId: string; rol: "cliente" | "agente_soporte" };
class Denegado extends Error {}
type Politica = (args: any, s: Sesion) => void;

const POLITICAS: Record<string, Politica> = {
  leer_archivo: (args) => {
    const ruta = path.resolve(RAIZ, args.ruta);
    if (ruta !== RAIZ && !ruta.startsWith(RAIZ + path.sep)) throw new Denegado(`Ruta fuera del repositorio: ${args.ruta}`);
  },
  reembolsar: (args, s) => {
    if (s.rol !== "agente_soporte") throw new Denegado("Solo el personal de soporte puede reembolsar");
    if (args.importe > 100) throw new Denegado("Importe por encima del límite automático; requiere aprobación");
  },
};

export async function ejecutar(
  b: Anthropic.ToolUseBlock, funciones: Record<string, (a: any) => Promise<unknown>>, s: Sesion,
): Promise<Anthropic.ToolResultBlockParam> {
  try {
    if (!(b.name in funciones)) throw new Denegado(`Herramienta no permitida: ${b.name}`);
    POLITICAS[b.name]?.(b.input, s);
    const salida = await funciones[b.name]({ ...(b.input as object), usuarioId: s.usuarioId });
    return { type: "tool_result", tool_use_id: b.id, content: String(salida) };
  } catch (e) {
    const prefijo = e instanceof Denegado ? "Denegado" : "Error";
    return { type: "tool_result", tool_use_id: b.id, content: `${prefijo}: ${(e as Error).message}`, is_error: true };
  }
}

Fíjate en dos cosas. El usuario_id sale de la sesión, nunca de los argumentos que propone el modelo. Y la denegación vuelve al modelo como is_error, para que pueda explicárselo al usuario o pedir aprobación por la vía correcta (Humano en el bucle).

Auditoría

Registra cada ejecución de herramienta con sesión, argumentos, resultado y si se aprobó o denegó. Ante un incidente, es la única forma de saber qué hizo el agente y con qué permisos. El registro debe estar fuera del alcance del agente: que ninguna herramienta pueda escribir o borrar en él. Los detalles de qué registrar están en Observabilidad.

Servidores MCP

Si conectas servidores MCP, cada uno es un conjunto de herramientas con sus propios permisos. Revisa qué expone cada servidor antes de conectarlo, habilita solo las herramientas que necesitas y prefiere servidores de solo lectura. Ver Servidor MCP de solo lectura y Seguridad y despliegue de servidores.

Errores frecuentes

  • Herramientas genéricas por comodidad: ejecutar_sql convierte cualquier inyección en acceso total.
  • Identificadores elegidos por el modelo: si el modelo pasa el usuario_id, puede pasar el de otro.
  • Una credencial para todo: si se filtra, se filtra todo.
  • Sandbox con la red abierta: el código malicioso no puede tocar tu disco, pero sí enviar datos fuera.
  • Permisos que solo crecen: revisa cada cierto tiempo qué herramientas ya no se usan y quítalas.

Ver también