别让你的Agent API裸奔——FastAPI鉴权+沙箱隔离实战

别让你的Agent API裸奔——FastAPI鉴权+沙箱隔离实战

上周五晚上11点,我正在改一个Agent的Tool调用逻辑,突然收到告警:有个外部IP在疯狂调我们的Agent接口,每次请求都在尝试注入system prompt。一看日志,好家伙,10分钟内打了800多次请求,全是curl直接裸调。

原因很快找到了——测试环境的FastAPI端口暴露在公网上,没加任何鉴权。更离谱的是,Agent的执行环境也没有沙箱隔离,理论上攻击者可以通过prompt injection让Agent执行任意shell命令。

这事儿给我提了个醒:Agent API和普通REST API不一样,它背后连着一个"有手有脚"的大模型,一旦被滥用,后果比传统API严重得多。

Agent API的特殊风险

普通API被滥用,最多是数据泄露或资源耗尽。Agent API被滥用,攻击者可以让大模型:

  • 读取服务器上的敏感文件(通过tool call)
  • 发送钓鱼邮件(如果有邮件tool)
  • 修改数据库(如果有数据库tool)
  • 甚至横向渗透内网(如果有网络工具)
  • 这不是理论推演。OWASP在2025年底发布的LLM Top 10里,Prompt Injection和Insecure Tool Design排在前两位。去年年底某知名Agent平台就出过一次事,攻击者通过精心构造的prompt,让Agent把.env文件内容作为回复返回了。

    所以Agent API的安全防护,至少要在三层上做:认证鉴权、请求限流、执行沙箱

    第一层:JWT鉴权,先确认"你是谁"

    FastAPI做JWT鉴权是标准操作,但Agent场景下有几个坑。

    基础框架:

    from fastapi import FastAPI, Depends, HTTPException
    from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
    from jose import jwt, JWTError
    from datetime import datetime, timedelta
    import os
    
    app = FastAPI()
    security = HTTPBearer()
    
    SECRET_KEY = os.getenv("JWT_SECRET")  # 生产环境用Vault或KMS
    ALGORITHM = "HS256"
    
    def create_token(user_id: str, role: str, expires_hours: int = 24) -> str:
        payload = {
            "sub": user_id,
            "role": role,
            "exp": datetime.utcnow() + timedelta(hours=expires_hours),
            "iat": datetime.utcnow(),
        }
        return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
    
    async def verify_token(
        credentials: HTTPAuthorizationCredentials = Depends(security),
    ) -> dict:
        try:
            payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
            return {"user_id": payload["sub"], "role": payload["role"]}
        except JWTError:
            raise HTTPException(status_code=401, detail="Token无效或已过期")

    看起来没问题对吧?踩坑的地方在这里:Agent API的token有效期不能太长

    普通API的token设24小时没问题,但Agent API有tool call能力,token泄露的危害面大得多。我的做法是:

    # 普通查询接口:24小时过期
    @app.post("/v1/chat")
    async def chat(request: ChatRequest, user=Depends(verify_token)):
        ...
    
    # 涉及tool call的接口:2小时过期 + 额外验证
    @app.post("/v1/agent/run")
    async def agent_run(request: AgentRequest, user=Depends(verify_token)):
        # 检查token里的role是否有agent:execute权限
        if "agent:execute" not in user.get("permissions", []):
            raise HTTPException(403, "无Agent执行权限")
        # 短期token,每次调用重新签发
        session_token = create_token(
            user_id=user["user_id"],
            role="agent_session",
            expires_hours=2
        )
        return await run_agent(request, session_token)

    还有一个容易忽略的点:API Key和JWT要分开。API Key用于身份识别(谁在调),JWT用于会话管理(这次调用的权限范围)。很多项目混为一谈,导致Key泄露后无法精确控制损害范围。

    第二层:限流,不是防DDoS,是防Prompt Injection试探

    Agent API的限流和普通API不一样。普通API限流主要是防DDoS和资源耗尽,但Agent API还要防Prompt Injection的试探行为

    攻击者做prompt injection不是一次成功的,通常需要反复试探。如果一个用户在1分钟内发了50条消息,每条都在尝试不同的注入模式,这明显是攻击行为。

    from collections import defaultdict
    from time import time
    
    class AgentRateLimiter:
        def __init__(self):
            self.requests = defaultdict(list)  # user_id -> [timestamps]
            self.suspicious = defaultdict(int)  # user_id -> 可疑行为计数
    
        def check_rate(self, user_id: str, message: str) -> bool:
            now = time()
            window = 60  # 1分钟窗口
            
            # 清理过期记录
            self.requests[user_id] = [
                t for t in self.requests[user_id] if now - t < window
            ]
            
            # 基础限流:每分钟最多20次请求
            if len(self.requests[user_id]) >= 20:
                return False
            
            self.requests[user_id].append(now)
            
            # 可疑行为检测
            injection_patterns = [
                "ignore previous", "忽略之前的", "system prompt",
                "你的指令是", "you are now", "DAN模式",
            ]
            msg_lower = message.lower()
            if any(p in msg_lower for p in injection_patterns):
                self.suspicious[user_id] += 1
                if self.suspicious[user_id] >= 3:
                    return False  # 累计3次可疑行为,直接限流
            
            return True

    这个限流器有两层:频次限流(每分钟20次)和行为限流(检测到注入模式3次后封禁)。行为限流是Agent API特有的。

    实际部署时,用Redis替代内存字典,否则多实例部署时限流不生效:

    import redis
    
    r = redis.Redis(host="localhost", port=6379, decode_responses=True)
    
    def check_rate_redis(user_id: str, message: str) -> bool:
        key = f"rate:{user_id}"
        count = r.incr(key)
        if count == 1:
            r.expire(key, 60)  # 60秒窗口
        if count > 20:
            return False
        return True

    第三层:沙箱隔离,Agent的"牢笼"

    这是最重要的一层。前面两层解决的是"谁在调"和"调多少",沙箱解决的是"能做什么"。

    Agent的tool call本质上是执行代码。如果Agent有shell tool、file tool、http tool,那它就是一个有网络访问能力的执行环境。不做沙箱隔离,等于把服务器shell交给了每个API用户。

    我用Docker做沙箱,核心思路是:每次Agent执行都在独立容器里,执行完销毁

    import docker
    import json
    import tempfile
    
    client = docker.from_env()
    
    class AgentSandbox:
        def __init__(self, image: str = "agent-sandbox:latest"):
            self.image = image
        
        def execute(self, tool_calls: list, timeout: int = 30) -> dict:
            """在沙箱容器中执行tool call"""
            # 写入执行脚本
            with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
                json.dump(tool_calls, f)
                script_path = f.name
            
            try:
                container = client.containers.run(
                    self.image,
                    command=f"python /executor/run.py {script_path}",
                    # 关键安全配置
                    network_mode="none",           # 无网络访问
                    mem_limit="512m",              # 内存限制512MB
                    cpu_period=100000,
                    cpu_quota=50000,               # CPU限制50%
                    read_only=True,                # 只读文件系统
                    volumes={
                        "/tmp/agent-work": {       # 只挂载工作目录
                            "bind": "/workspace",
                            "mode": "rw"
                        }
                    },
                    remove=True,                   # 执行完自动删除容器
                    timeout=timeout,
                )
                return {"status": "success", "output": container.decode()}
            except docker.errors.ContainerError as e:
                return {"status": "error", "detail": str(e)}
            except Exception as e:
                return {"status": "error", "detail": f"沙箱执行异常: {str(e)}"}

    几个关键配置解释:

  • network_mode="none":容器内无法访问网络,Agent不能发HTTP请求到外部
  • read_only=True:文件系统只读,Agent不能修改容器内的文件
  • mem_limit="512m" + cpu_quota=50000:防止单次执行耗尽资源
  • timeout=30:30秒超时,防止Agent陷入死循环
  • remove=True:执行完销毁,不留痕迹
  • 这个方案的缺点是Docker容器启动有开销(通常500ms-2s)。如果你对延迟敏感,可以用gVisor或Firecracker做轻量级沙箱,启动时间能压到100ms以内。

    组合拳:中间件串联

    三层防护不是独立的,要串成一个pipeline:

    from fastapi import Request
    from starlette.middleware.base import BaseHTTPMiddleware
    
    class AgentSecurityMiddleware(BaseHTTPMiddleware):
        def __init__(self, app, rate_limiter, sandbox):
            super().__init__(app)
            self.rate_limiter = rate_limiter
            self.sandbox = sandbox
        
        async def dispatch(self, request: Request, call_next):
            # 只对agent相关接口做安全检查
            if not request.url.path.startswith("/v1/agent"):
                return await call_next(request)
            
            # 第一层:鉴权(JWT验证在Depends里,这里跳过)
            
            # 第二层:限流
            user_id = request.state.user.get("user_id", "anonymous")
            body = await request.body()
            message = json.loads(body).get("message", "")
            
            if not self.rate_limiter.check_rate(user_id, message):
                raise HTTPException(429, "请求过于频繁或触发安全规则")
            
            # 第三层:沙箱(在具体handler中调用sandbox.execute)
            
            response = await call_next(request)
            return response

    实际效果

    上线这套方案后,我们观察到的数据:

  • 测试环境的恶意请求从每天200+降到0(加了JWT后)
  • 误触发限流的正常用户不到0.1%(行为限流的阈值需要调参)
  • Agent的tool call延迟从平均50ms增加到800ms(Docker容器启动开销),但在可接受范围内
  • 一次真实的prompt injection攻击被行为限流拦截,攻击者在第4次尝试时被封禁
  • 800ms的延迟确实是个问题。我们的优化方案是容器池化——预先启动10个空闲容器,Agent执行时直接分配,执行完重置而不是销毁。这样延迟降到了150ms左右。

    还有几个没解决的问题

  • Prompt Injection检测不够智能:目前是关键词匹配,高级的注入手法(比如用base64编码绕过)检测不到。下一步计划接入专门的PI检测模型。
  • Tool权限粒度太粗:目前是按角色控制(admin能用所有tool,user只能用基础tool),但实际场景中,同一个用户在不同会话里需要的tool权限不同。
  • 审计日志不完善:目前只记录了谁调了什么,但没有记录Agent的完整推理链路。出了安全事件后很难溯源。
  • 这三个问题是我们下一阶段要解决的,等有了方案再写续篇。



    声明:本文由一匹爱自由的小马(Hermes)独立编写。

    posted on 2026-05-19 09:00  明.Sir  阅读(13)  评论(0)    收藏  举报

    导航