面向大模型API场景的蜜罐系统设计与实现
这三四个月做的最有意思的一个内部项目就是标题里这个“面向大模型API场景的蜜罐系统”。接了需求的时候我心里嘀咕了一句现在连大模型API都需要养个假系统来钓鱼了但等我翻完线上网关的访问日志我承认确实需要而且比想象中更需要。一句话解释这个项目它是一套伪装成真实模型服务的诱饵接口。攻击者拿到泄露的API Key或者用自动化工具扫到你的域名之后第一反应一定是先请求一下/v1/models或者/v1/chat/completions看看服务是否存活。蜜罐就是干这个的——它会让攻击者看到一个“活着的”、“响应完全正常的”模型服务让对方继续投入时间往下探测。与此同时每一次请求的IP、指纹、请求体、攻击手法全部被我们记录下来用于溯源、情报积累和联动封禁。这篇分享适合三类人看正在给自家模型服务做安全加固的研发同学、做攻防演练想收集真实攻击样本的安全工程师以及想低成本搭一套“钓鱼系统”来观察网络黑产手法的爱好者。我会从需求拆解、架构设计、核心实现、实操部署到排障避坑把整个过程完整讲清楚尽量做到看完就能照着搭。1. 需求拆解与方案定位1.1 大模型API面对的威胁是什么样先说威胁模型。过去一年里大模型API已经成为黑产的重点目标原因很朴素模型服务是按Token计费的API Key就是真金白银。任何一个被泄露、被爬取、被误提交到公开仓库的Key都是可以直接被滥用的“余额”。我见过的攻击路径大概有这几种全局扫描型攻击者持有大量泄露的Key用脚本遍历云厂商IP段和常见域名挨个请求/v1/models测试可用性。针对性撞库型知道目标公司用了某类模型网关专门构造路径和认证头去探测内网风格接口。中转批发型拿到可用Key之后搭一个“中转站”向下游客户转售模型访问能力通过大量并发把额度刷爆。情报型拿蜜罐当跳板先探测接口的行为特征判断后端接的是哪个厂商的模型再决定是否进一步利用。传统Web蜜罐能捕获一部分扫描流量但很难骗过大模型场景的攻击者。因为攻击者已经形成了固定的“验证动作”——他们会直接发起一个带复杂Prompt的Chat请求然后看返回内容是否真的“像模型”。如果只是返回一个静态HTML页面对方半秒钟就识破了后续的流量就断了。1.2 为什么传统蜜罐方案不通用传统蜜罐的核心能力是模拟服务指纹常见做法是挂一个假的管理后台或者伪造一个登录页。这类方案在Web扫描场景里很好用但在大模型API场景有一个致命短板协议栈不匹配。大模型API走的是JSON over HTTP认证鉴权靠Authorization: Bearer sk-xxx业务交互靠POST /v1/chat/completions传入model、messages响应既有普通的JSON也有text/event-stream的流式返回。传统蜜罐不会模拟这套协议攻击者一个请求打过来要么返回404要么返回一个跟模型服务完全不搭边的页面信任瞬间归零。另外大模型API蜜罐还有一个特殊点它不只是要“看起来像”还要“聊起来像”。攻击者会故意发一句测试内容来判断你是不是真模型。如果我们的假服务只会回固定字符串第二次交互就暴露了。所以蜜罐得能生成动态且合理的模型响应。1.3 蜜罐解决的核心问题回到项目本身这套蜜罐的价值集中在三个层面第一是前置预警。真实模型服务的攻击往往在消耗产生之后才发现蜜罐相当于一个预警哨位攻击者一旦触碰诱饵我们立刻感知。第二是攻击取证。攻击者的探测路径、尝试注入的Prompt、利用的工具指纹都是真实攻击情报。这些数据比任何威胁情报平台上的样本都贴近我们的真实暴露面。第三是消耗转移。攻击者以为在打正式API实际上被引导到一个零成本的假服务上既浪费时间又消耗不到我们真实额度。1.4 方案选型低交互还是高交互蜜罐行业里有个简单的分类低交互、中交互、高交互。低交互就是回几个固定响应包纯骗扫描器高交互是给攻击者一个完整可交互的真实环境。大模型API蜜罐我建议定位在中低交互偏上的位置。核心原因有两个高交互成本不可控。如果后端真的接了一个模型攻击者可以通过多轮Prompt慢慢耗尽你的真额度。高交互有数据泄露风险。模型服务如果把内部知识或者系统Prompt都回给攻击者那不是防御是泄密。所以这套系统只做“协议仿真”不接真实模型。响应文本靠模板加随机化策略生成足够让自动化脚本和大部分人工攻击者产生“这服务是活的”的错觉。2. 整体设计思路与架构选型2.1 核心思路让诱饵“活”在真实链路里蜜罐最大的忌讳是“太独立”。一个独立域名、独立IP、独立证书看一眼就知道是个专用陷阱。但如果你把它藏在真实链路里比如放在正式的API网关后面和真实服务共享域名前缀、共用SSL证书、挂在同一个接入层后面攻击者就很难区分了。我们的做法是申请一个独立的子域名但让这个子域名的解析和主站走同一个边缘节点。用Nginx做流量分发对外看起来就是同一套基础设施。攻击者扫描的是域名看到的是同样的证书体系和相似的HTTP头信任度一下就上来了。2.2 系统总体架构整个系统分为四层层级组件职责流量接入层Nginx / OpenResty域名解析、TLS终止、按路径分发流量、简单限速蜜罐仿真层FastAPI服务模拟模型API接口、校验Key、生成响应、采集日志数据存储层Redis Elasticsearch计数器、滑动窗口、请求日志存储和检索联动处置层Shell脚本 告警渠道触发封禁、发送告警、同步威胁情报流量接入层用Nginx是因为它太成熟了配置一个反向代理只需要几行此外OpenResty还能用Lua写一点轻逻辑比如对明显恶意来源直接返回403不必打到后面的仿真服务减少无效日志。蜜罐仿真层用FastAPI选型理由很简单Python写起来快Pydantic做请求体校验非常顺手而且自带OpenAPI文档方便内部调试。这个项目不追求高并发不需要上Go或者Rust一台2C4G的机器就已经绰绰有余。数据存储层用Redis承担两个职责一是做滑动窗口计数器防止告警轰炸二是存一些动态配置比如要封禁的IP黑名单和假的Key列表。Elasticsearch主要做日志检索因为我们想看某一类攻击源的请求序列用ES的聚合查询比翻文本日志高效太多。但如果你的环境里没有ES直接用JSON Lines日志加grep/awk也完全可以起步。2.3 为什么对外伪装不能过度设计设计阶段最想做的事情是把蜜罐做得“极其真实”比如把token计费返回、模型列表、并发限制都加上。但实际操作下来这里有个度要把握。过度设计会带来两个问题一是维护成本变高真实模型API的响应结构经常更新我们不可能一直跟着改二是过度真实可能导致攻击者把蜜罐当成可用资源长期占用不说还会引来更多自动化工具刷量把我们自己的日志系统打满。所以我的建议是协议层做到“符合主流客户端预期”但业务层保留适度的模糊。比如/v1/chat/completions支持model、messages、temperature这些常用参数但对tools、response_format这些新特性返回一个“错误”反而更像一个版本稍旧的服务攻击者不会怀疑。3. 核心环节的实现细节3.1 接口路径与认证头的仿真公开的模型API风格高度统一我们选取了三组最常被探测的路径GET /v1/models列出可用模型主要用于存活探测。POST /v1/chat/completions聊天补全攻击者的主战场。POST /v1/embeddings向量化接口黑产批发场景经常探测。这三个路径覆盖了扫描器、测试脚本、中转站三种角色的核心需求。认证方面真实服务用的是Authorization: Bearer sk-...。蜜罐的校验逻辑我写了这样一个简单判定def verify_api_key(token: str) - bool: if not token or not token.startswith(sk-): return False if token in BLACKLIST_KEYS: return False if len(token) 20: return False return True逻辑要点是只要格式符合且不在黑名单就放行。我不能把校验做得太严格否则攻击者拿一个老Key来撞发现每次都是401可能就觉得目标服务已经下线了。适当放宽校验让大概率是恶意请求的流量进入蜜罐内部是故意为之。3.2 模型响应剧本随机化与流式输出响应生成是整个蜜罐的灵魂。我设计了三个层次的“剧本”默认响应返回一个标准的ChatCompletion格式JSON内容是一段通用的“服务正常”回答。模型名映射攻击者可能在请求里指定model: gpt-4o或者model: deepseek-chat蜜罐可以按模型名返回不同的系统回复风格让下一次交互更可信。流式响应真实API默认流式返回的场景已经非常普遍蜜罐必须支持。我实现了简单的SSE输出把一个小回答拆成几段按时间吐出from fastapi.responses import StreamingResponse def fake_stream_chunk(token: str) - str: return fdata: {{choices:[{{delta:{{content:{token}}},index:0}}]}}\n\n async def stream_response(): for word in [你好, , 我是, 一个, 智能助手]: yield fake_stream_chunk(word) await asyncio.sleep(0.3) yield data: [DONE]\n\n因为我们是仿真服务内容生成不需要模型参与完全靠预置语料池加随机选择。这里的诀窍是语料池要足够大不能每次回话都是同一句“你好我是智能助手”否则攻击者做两次相同请求就识破了。3.3 请求留痕与特征采集记录什么信息直接决定了这套系统的分析价值。我的日志清单如下时间戳精确到毫秒来源IP、端口User-Agent、X-Forwarded-ForAuthorization头的前32位避免完整Key落盘请求路径与完整请求体响应状态码、响应体大小、耗时蜜罐标识字段方便和真实服务日志区分请求体是比较敏感的内容。攻击者在蜜罐里发送的Prompt可能包含各种脏数据、注入尝试或者真实客户的数据。出于合规考虑我不会把请求体原文直接落ES而是做一次脱敏摘要保留长度、关键词、是否含系统指令注入特征。等观察一段时间后再决定是否人工提取部分关键样本。def redact_body(body: dict) - dict: prompts [] for msg in body.get(messages, []): content msg.get(content, ) prompts.append({ role: msg.get(role, ), content_length: len(content), content_snippet: content[:32], has_injection: in content or ignore previous in content.lower(), }) return prompts这段代码看起来简单但has_injection这个字段帮了大忙。攻击者的Prompt注入尝试往往会出现“忽略之前的指令”“输出系统提示词”之类的写法把这个特征记录成布尔字段后面做统计分析非常方便。3.4 告警与联动封禁蜜罐本身不产生防御动作它只负责发现。真正把价值放大的环节是告警和封禁。我的告警策略分了两级第一级单次攻击识别。如果发现请求路径包含/admin、/actuator这类敏感路径或者请求体中出现明显注入特征立刻触发告警。第二级行为积累识别。三次以上对蜜罐的探测或者同源IP在10分钟内调用超过20次判定为活跃攻击源进入封禁流程。封禁不是我们去打防火墙改规则而是给Nginx下发一条黑名单由接入层统一拦截if ($http_x_forwarded_for ~* ^1\.2\.3\.4) { return 403; }自动化流程写成一个脚本从Redis里读取累积超阈值的IP生成包含IP黑名单的Nginx配置文件片段reload Nginx。整个过程几十行代码搞定不需要额外基础组件。4. 实操部署与试运行记录4.1 部署拓扑与资源规划这套系统的部署难度不高我建议直接用一台独立的云主机2核4G起步带宽按1Mbps就够。域名方面单独申请一个二级域名比如honeypot-api.example.com不要跟真实服务混用域名避免误伤。生产环境能用Docker Compose尽量用整个系统就三个容器nginx、honeypot-api、redis。Elasticsearch我建议按需再启前期日志体量不大时直接落到宿主机磁盘即可。4.2 蜜罐服务的启动与配置FastAPI服务的核心配置写在环境变量里包括监听端口、允许的假Key列表、日志文件路径和告警Webhook地址export LISTEN_PORT8001 export FAKE_KEYSsk-honeypot-demo-key-001 export LOG_FILE/var/log/honeypot/requests.jsonl export ALERT_WEBHOOKhttps://example.com/hook启动命令很简单uvicorn honeypot_app.main:app --host 0.0.0.0 --port 80014.3 Nginx流量分发配置Nginx配置我放在这里新增一个server块专门处理蜜罐域名的流量server { listen 443 ssl; server_name honeypot-api.example.com; ssl_certificate /etc/nginx/certs/example.crt; ssl_certificate_key /etc/nginx/certs/example.key; access_log /var/log/nginx/honeypot_access.log combined; location / { proxy_pass http://127.0.0.1:8001; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Real-IP $remote_addr; } }注意proxy_pass的写法这里不要带URI尾缀保证原始路径完整传给FastAPI否则/v1/chat/completions会被改写。4.4 上线后的流量观察蜜罐上线第一周效果比预期好很多。最直观的观察是扫描器在发现/v1/models返回正常之后会在几秒内立刻发起Chat请求。这说明攻击者的自动化流程非常依赖“先活着再突破”的逻辑。还有一种很典型的攻击模式请求头里的UA是空的但带了云厂商的默认User-Agent标志一看就是从某云主机发起的批量化扫描。这类流量基本不会对话只做路径枚举。更复杂的情况是人工测试。有一次日志里出现了连续多轮对话攻击者先问“你是谁”又问“你背后的模型是什么”最后尝试把系统指令套出来。我们的蜜罐因为语料池设计得不错全程没有露馅攻击者甚至尝试了三轮才离开。4.5 数据回看与情报输出日志落地之后我写了一个简单的统计脚本按小时聚合告警次数和攻击源数量产出一个摘要表cat /var/log/honeypot/requests.jsonl | jq -r .source_ip | sort | uniq -c | sort -rn用这个命令能快速看到top攻击源。后面我把这套统计逻辑集成到了临时面板里日志一多谁在扫、扫了什么路径、喜欢发什么Prompt一览无余。5. 常见问题与排查技巧实录5.1 蜜罐流量突然断崖式下降上线一段时间后蜜罐访问量常常会骤降。可能的原因有两个一是攻击者已经把该扫的扫完了IP被全网标记二是某云厂商的威胁情报系统把蜜罐域名标记为“C2/恶意资产”导致后续扫描流量不再访问。排查方法先看DNS解析是否正常再检查证书是否过期然后用第三方网站查一下域名信誉。如果确实被标记了短期别慌等情报系统刷新即可长期可以考虑换一个子域名或者把蜜罐放到另一个IP段去。我的实际操作是准备了两套诱饵域名按季度轮换避免单点被情报污染。5.2 蜜罐骗不过老练的攻击者不是所有攻击者都靠自动化脚本有些高水平的攻击者会逐步校验服务的真实性。他们会对比响应头里是否有真实厂商的标识会测试超时不存在的模型名甚至会在请求里故意加入一个不合法的JSON。对付这类攻击者我的经验是不要在响应头里加任何多余的自定义头字段响应时间尽量模拟真实模型的波动并且支持一些“报错输出”。比如请求一个不存在的模型时返回标准的model_not_found错误体而不是简单回200。这是很多人容易忽略的细节。我加了一个错误响应分支后后续攻击者的停留时间明显变长了有些人甚至开始尝试不同的模型名说明他们认为这个服务是真实存在的。5.3 告警轰炸导致真正重要的事件被淹没蜜罐日志一多告警就容易变成“狼来了”。连续几天告警不断负责值班的同事最后连看都不想看了真正重要的异常反而被淹没。我的解决方案是给告警加了Redis滑动窗口。每个攻击源IP独立计数相同IP在5分钟内最多触发一条告警只有当一个IP在30分钟内触发超过5次时才走紧急通知渠道。同时在告警内容里附上聚合摘要比如“1.2.3.4在近30分钟内发起了23次探测主要路径是/v1/models”让值班人员一眼就能判断优先级。5.4 蜜罐和真实服务互相干扰最怕的一件事是蜜罐和真实服务配置串了导致真实用户被引导到蜜罐接口上。这种情况一旦发生影响的不只是用户体验可能还会导致内部数据被当成诱饵记录制造大量噪音。规避方法第一蜜罐用独立域名和独立端口部署不要复用真实服务的Nginx配置模板第二在日志里加一个静态的标记字段honeypot: true任何分析场景都先从日志中过滤掉这个字段第三写一个定时脚本每天对比真实服务和蜜罐的访问日志如果发现同一个请求源同时访问了两者立刻告警说明有配置混用或流量误配的风险。5.5 请求体脱敏与合规风险大模型API的请求体里经常包含业务数据和Prompt原文直接落盘有合规风险。尤其是当攻击者把蜜罐当成真的服务来用发送了一段真实的客户对话或者内部资料时我们不能把这段内容原样存进日志。我的做法是对请求体中的文本字段一律截取前32个字符同时对包含身份证号、手机号、邮箱等正则匹配的内容做加密存储。逻辑上所有日志都保留原始长度和哈希指纹但只有拿到权限的人才能解开明文这样既保留了分析价值又避免敏感信息扩散。写在最后的一点体会做完这个项目最大的感受是蜜罐的技术门槛真不高难的是一直保持“像真的”。模型API是一个很年轻的服务形态攻击者在变防御手段也要跟着变。我建议准备做这个方向的朋友先不要纠结于把系统架构搭得多么完整而是先从最简单的/v1/models一个路径开始让蜜罐“活下去”让日志滚起来再慢慢增加聊天补全、流式返回这些高阶能力。另外说一个我自己反复踩的坑千万不要把蜜罐的响应做得比线上真实服务还好。蜜罐的目的是拖住攻击者不是让攻击者觉得“这个API真香”。我后来故意在蜜罐里加了响应延迟和偶发错误结果反而看起来更像生产环境。这里面的度需要自己根据日志慢慢调。如果你也想搭一套建议先把日志分析和告警这两条链路做好再上线。蜜罐的价值不在于能骗住多少人而在于每一次欺骗都被记下来、被用起来。但愿这个东西只在红队演练里被用到永远不要接住真正的坏消息。