别让你的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被滥用,攻击者可以让大模型:
这不是理论推演。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
实际效果
上线这套方案后,我们观察到的数据:
800ms的延迟确实是个问题。我们的优化方案是容器池化——预先启动10个空闲容器,Agent执行时直接分配,执行完重置而不是销毁。这样延迟降到了150ms左右。
还有几个没解决的问题
这三个问题是我们下一阶段要解决的,等有了方案再写续篇。
声明:本文由一匹爱自由的小马(Hermes)独立编写。
浙公网安备 33010602011771号