资讯详情

资讯详情

提示词加密传输实战:从mTLS到AES-GCM的完整方案

前阵子我们给内部一套大模型问答系统做安全改造团队争得最多的不是模型效果而是提示词走公网时怎么保证不被人截走、改掉。问题听起来挺基础但真正要把提示工程加密传输机制落地牵扯到证书体系、应用层加密、防重放、日志脱敏每一环都有不少细节。这篇文章就围绕这套机制从原理到工具再到实战案例把能直接抄作业的部分都摊开讲。如果你正在做AI应用开发、模型API对接或者需要跨网络传递用户上下文这套机制的核心逻辑是通用的。和普通接口加密最大的区别在于提示词不只是一段数据它会被模型直接当成指令执行。所以除了机密性完整性和请求来源可信性同样重要。接下来不会讲太多理论主要是我在实际改造中验证过的方案以及那些不踩一次很难发现的坑。1. 先理解风险提示词在传输环节到底怕什么1.1 提示词比普通接口数据更敏感因为它是“可执行的上下文”很多人觉得提示词就是一段自然语言加密不加密无所谓。但真实业务里的提示词往往由系统指令、用户问题、检索到的知识片段、工具调用参数、历史对话拼接而成。里面可能藏着内部系统名称、数据库字段、外部接口的key、风控规则甚至用户的手机号和地址。一旦明文泄露不只是用户隐私问题还等于把系统的“模型用法说明书”交给了攻击者。更关键的是提示词具备可执行性。攻击者如果在传输过程中篡改一个词比如把“只允许查询本部门数据”改成“允许查询全公司数据”模型输出的权限边界就完全变了。这个影响比篡改普通JSON字段要直接得多因为提示词会驱动模型做决策、调工具、访问外部系统。所以防篡改不是可选项是必须项。1.2 四条攻击路径决定了加密方案的设计边界设计加密方案之前先要搞清楚到底在防谁。我通常把风险归成四类每一类对应一种防护机制。窃听明文提示词在链路中被第三方读取比如WiFi嗅探、路由劫持、机房链路监听。防护手段是传输层加密和应用层加密叠加。篡改中间人修改提示词中的关键指令、阈值、工具参数。防护手段是AEAD密文认证和签名校验。重放攻击者截获一个合法请求原封不动重新发送造成重复计费、重复任务、重复审批。防护手段是请求时间戳加Nonce缓存。身份冒用攻击者伪装成合法客户端调用模型服务获取模型能力和敏感应答。防护手段是mTLS双向证书校验和API密钥绑定。这四条路径直接决定方案结构不能只靠一个HTTPS证书解决。对提示词这类高价值流量我建议按“传输层mTLS 应用层AES-GCM封装 请求唯一性校验”三层来做。下面逐个拆开讲。2. 加密体系拆解从TLS到应用层每一层都在防什么2.1 传输层加密TLS不是终点mTLS才是公网调用标准普通HTTPS只是客户端验证服务端证书确保连接不被打洞但服务端并不知道客户端是谁。在这个基础上任何拿到公网入口地址的人都可以发起加密连接然后触发模型调用消耗你的算力甚至通过试错构造恶意提示词。所以公网场景下我坚持使用mTLS双向认证。mTLS做的事情是客户端携带自己的证书服务端除了校验TLS证书还要求客户端证书由受信任的CA签发。网关在握手阶段直接拒绝没有证书或证书非法的连接。对于内部系统之间我会用自建CA签发所有节点证书而不是直接买公网HTTPS证书。这样信任根完全掌握在自己手里证书吊销和轮换也更好控制。证书链和信任根是mTLS最容易出错的地方。客户端需要配置完整CA链服务端必须把客户端证书对应的根CA放进信任列表。验证时不能只验证叶子证书要把整条链追到自建CA。下面是一段生成自签CA和服务端证书的常用命令实测足够支撑测试环境。# 生成自签CA私钥要严格保管 openssl req -x509 -newkey rsa:4096 -keyout ca.key -out ca.crt -days 3650 -nodes -subj /CNInternal CA # 为网关生成服务端证书并用CA签发 openssl req -newkey rsa:2048 -keyout server.key -out server.csr -nodes -subj /CNapi.internal.local openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.crt -CAcreateserial -out server.crt -days 730 # 为客户端生成证书client-a代表一方调用方 openssl req -newkey rsa:2048 -keyout client-a.key -out client-a.csr -nodes -subj /CNclient-a openssl x509 -req -in client-a.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client-a.crt -days 730这里有个实测心得CA私钥的权限一定要收得非常紧只允许少数运维人员通过堡垒机操作。如果CA私钥泄露整个信任体系都要重建成本极高。2.2 应用层加密让网关和日志都看不到明文传输层加密能保证数据在链路上不被看到但到了网关这一层TLS会话会被终止。如果网关要转发请求到模型服务或者审计系统要记录请求内容那么提示词在网关进程内就是明文状态。有些框架会在调试日志里自动打印请求body一旦开了debug明文提示词就进了日志系统。这时候就需要应用层加密。应用层加密的思路俗称“信封加密”。客户端每次请求随机生成一个临时对称密钥用这个密钥加密提示词再用服务端的公钥把临时密钥包起来。服务端收到包后先用私钥解出临时密钥再用临时密钥解出提示词。整个过程只有持有私钥的服务端能解开网关如果只做透传看到的始终是密文。对称加密我推荐AES-256-GCM。GCM是AEAD模式加密时同时生成认证标签解密时如果密文被改过认证直接失败。这天然满足提示词防篡改的需求。临时密钥虽然用RSA包了一层但RSA只负责解出对称密钥真正的数据加解密开销很小性能上可以接受。下面是封装好的加解密模块基于Python的cryptography库实现。import os import json import time import base64 from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding class PromptCrypto: def __init__(self, public_key_path, private_key_pathNone, key_idkek-2025-01): with open(public_key_path, rb) as f: self.public_key serialization.load_pem_public_key(f.read()) self.key_id key_id self.private_key None if private_key_path: with open(private_key_path, rb) as f: self.private_key serialization.load_pem_private_key(f.read(), passwordNone) def encrypt(self, plaintext: str) - bytes: # 每次请求随机生成临时对称密钥避免密钥复用 aes_key os.urandom(32) nonce os.urandom(12) ciphertext AESGCM(aes_key).encrypt(nonce, plaintext.encode(utf-8), None) # 用服务端公钥包裹临时密钥 encrypted_key self.public_key.encrypt( aes_key, padding.OAEP( mgfpadding.MGF1(algorithmhashes.SHA256()), algorithmhashes.SHA256(), labelNone, ), ) body { version: 1, key_id: self.key_id, algorithm: AES-256-GCM, encrypted_key: base64.b64encode(encrypted_key).decode(), nonce: base64.b64encode(nonce).decode(), ciphertext: base64.b64encode(ciphertext).decode(), timestamp: int(time.time()), } return json.dumps(body).encode(utf-8) def decrypt(self, packet: bytes) - str: body json.loads(packet) encrypted_key base64.b64decode(body[encrypted_key]) nonce base64.b64decode(body[nonce]) ciphertext base64.b64decode(body[ciphertext]) aes_key self.private_key.decrypt( encrypted_key, padding.OAEP( mgfpadding.MGF1(algorithmhashes.SHA256()), algorithmhashes.SHA256(), labelNone, ), ) plaintext AESGCM(aes_key).decrypt(nonce, ciphertext, None) return plaintext.decode(utf-8)这个模块在测试环境跑起来很顺但用到生产前有几个细节必须改。key_id不能是写死的字符串要从KMS或配置中心动态获取。nonce要确保随机且无规律如果出现重复nonce同一个AES密钥下会直接破坏加密安全。服务端解密时还要校验请求时间戳超过允许的窗口期就拒绝否则重放攻击仍然有效。2.3 端到端加密在推理场景的边界密文推理还不划算有人会说既然要安全干脆做到真正的端到端加密让模型服务方也看不到明文。想法很好但实际情况是模型推理必须拿到明文提示词才能生成结果。我们不可能把密文直接喂给Transformer至少目前的通用模型做不到。同态加密和多方安全计算可以让模型在密文上做计算但性能开销极大离生产还有距离。我的建议是务实一点做“可信边界内的端到端”。也就是客户端到安全接入层之间做应用层加密接入层到模型服务之间用mTLS加固整个链路任何一环都不允许明文落盘。如果某些字段特别敏感可以再做字段级加密。比如用户手机号、身份证号在拼进提示词之前先脱敏或加密模型看到的是脱敏后的占位符输出结果再映射回真实值。这种做法的好处是即使提示词被完整截获核心敏感字段也无法直接使用。3. 实操选型加密库、密钥管理、安全网关怎么配3.1 加密库与密钥管理比选算法更难的是选密钥托管方式算法本身没什么好纠结的AES-256-GCM和RSA-OAEP是成熟组合。真正决定方案成败的是密钥放哪、怎么轮换。把私钥硬编码在代码库里加密等于白做攻击者拿到源码就等于拿到所有密钥。我建议主密钥托管到KMS或HSM程序里只保存一个key_id。需要加密数据时调用KMS生成数据密钥KMS返回明文数据密钥和密文版本。解密时用key_id告诉KMS需要哪把密钥。KMS通常都支持密钥版本概念轮换时可以保留旧版本继续解密历史数据新数据用新版本加密平滑过渡。信封加密的密钥层级是这样最高层级是主密钥通常存在KMS或HSM里永远不离开加密机中间层级是数据密钥由KMS生成用于实际加解密最底层是临时会话密钥就是代码里每次随机生成的AES-256密钥。KMS生成的密钥如果不需要持久化可以每次调用如果需要性能优化可以缓存一段时间但一定要控制在合理时间范围内。密钥轮换策略我踩过坑。以前是每季度手动换一次证书结果某次轮换后忘记更新客户端的CA信任列表服务直接断连三小时。后来改成自动化轮换证书两天前自动检查有效期到期前自动签发客户端通过配置中心动态拉取信任链不需要人工干预。轮换期间保留新老密钥双读窗口直到确认老密钥不再有流量。3.2 安全网关与接入层统一收口但绝不记录明文安全网关是整套机制里适合统一收口的地方。客户端证书校验、限流、黑白名单、审计全都可以在这里集中做。网关负责终止TLS也负责验证客户端发来的应用层密文包格式是否合法甚至可以在这里做提示词注入特征的粗粒度检测但我不建议让网关解密业务数据除非你对网关的安全性有绝对把握。网关到模型服务之间如果走内网很多团队会直接用明文HTTP。我对此的建议是能加密就加密。内网不代表绝对安全尤其是多租户机房或云上VPC内部配置错误、横向移动都是现实风险。gRPC调用直接启用TLS代价很小收益很明确。下面是安全网关的TLS配置片段。这里只展示和加密传输直接相关的部分限流、路由等其他配置按正常方式加。server { listen 443 ssl http2; server_name api.internal.local; # 服务端证书与私钥 ssl_certificate /etc/ssl/certs/server.crt; ssl_certificate_key /etc/ssl/private/server.key; # 客户端证书链校验必须开启不能是optional ssl_client_certificate /etc/ssl/certs/internal-ca.crt; ssl_verify_client on; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; # 密文包会比原始提示词大需要调整body限制 client_max_body_size 4m; }注意一个细节ssl_verify_client如果配成on客户端没有证书时握手直接失败。如果配成optional网关不会拒绝无证书请求而是把证书校验结果放到请求头里由业务逻辑自行判断。很多事故就是这里用了optional下游又没做二次校验导致“看着开了mTLS实际等于没开”。3.3 一个可复用的轻量级加解密模块别用网上跑不通的伪代码上一节已经给出了PromptCrypto的核心代码这里再说下接入时的注意点。加密模块要同时支持客户端和服务端两类使用场景。客户端只需要加载服务端公钥服务端才需要加载私钥。所以类的设计上public_key_path是必选参数private_key_path是可选参数服务端实例化时传入私钥路径客户端不传。在正式项目里我不建议直接读本地文件。公钥可以存KMS的公开区域或通过配置中心下发。私钥只能在服务端受保护目录存在且需要严格权限控制。文件权限至少是600属主是运行服务的账户。解密接口还需要额外加两个逻辑。一个是时间戳校验请求包里的timestamp离当前时间超过60秒直接拒绝防止老包重放。另一个是request_id去重用Redis或本地缓存记录最近五分钟内处理过的request_id重复请求直接丢弃。这两个逻辑一起才能把重放攻击挡住。下面的伪代码展示了服务端中间件里最关键的两步判断。def check_replay(body): if abs(time.time() - body[timestamp]) 60: raise ReplayError(timestamp expired) if request_id_seen(body[request_id]): raise ReplayError(duplicate request_id) remember(body[request_id], expire300)3.4 调试与抓包验证确认链路上真的没有明文方案部署完不能只靠“我觉得没问题”要实际验证。我最常用的验证命令是openssl s_client它能测试TLS握手是否成功、客户端证书是否被接受、对方的证书链是否完整。openssl s_client -connect api.internal.local:443 \ -cert client-a.crt -key client-a.key \ -CAfile ca.crt -verify_return_error如果握手成功说明mTLS这部分通了。接着用curl实际发一个加密请求同时在网关所在机器或客户端机器上抓包。只要在链路上抓到的HTTP层是空的、TLS应用层数据是密文就说明传输层加密生效。应用层加密是否生效则在网关侧打解密前后的日志对比确认网关在解密前看到的body是一串无法读懂的base64字符。抓包工具有时候会尝试解密TLS前提是要提供私钥。我建议不要在正式环境做这种事私钥一旦交给第三方工具等于把加密体系的根基交出去。真要验证应用层密文直接在网关进程内打印收到的原始body即可不要用抓包工具去解TLS。4. 实战案例模拟项目X的提示词加密改造全记录4.1 需求梳理与架构设计从“能不能跑”到“安全地跑”以模拟项目X为例它是一套内部知识问答系统客户端部署在几个分支机房模型服务集中在总部机房中间跨公网。改造前提示词直接走HTTPS没有mTLS也没有应用层加密。接到安全审计通知时问题清单列了一大串日志里能看到完整提示词、客户端证书没有做校验、请求可以无限重放。需求梳理成四条硬指标第一传输链路上不允许出现明文提示词第二只有模型服务侧的解密模块可以拿到明文第三客户端身份必须可信且可审计第四重放请求必须被拒绝。架构上选择加一层安全网关客户端不直接连模型服务。整体链路是“客户端SDK - 安全网关 - 模型服务”三部分之间全部走TLS并且客户端到安全网关之间额外做应用层信封加密。为什么不在客户端直接连模型服务因为模型服务团队不希望改造核心推理接口而且统一网关更便于做限流和审计。网关只做证书校验、基础检测和转发不承担业务解码逻辑。解密动作放在模型服务入口的统一中间件里这样模型服务的内部代码不需要关注加密细节。4.2 关键配置与改造步骤一步一动每步都可回滚第一步是生成证书体系。内部自建CA签发三个角色的证书网关服务端证书、客户端A证书、客户端B证书。生产环境不能使用示例里的同一个CA私钥继续签应该单独生成新CA并把CA私钥托管到专用权限环境。第二步是网关侧启用mTLS。这里踩了一个很典型的坑网关版本升级后新配置里多了ssl_verify_client optional的默认值老配置迁移时没有仔细检查导致无证书客户端也能握手成功。排查了很久才发现所以现在每次改完配置都会用无证书请求测试一遍确认握手失败。第三步是在客户端封装加密SDK。SDK暴露的接口和原来的HTTP客户端保持一致业务方改动很小。SDK内部完成JSON序列化、随机生成AES密钥、RSA包裹密钥、AES-GCM加密、时间戳和request_id填充。这个阶段主要验证加密后的包体结构是否符合预期。第四步是在模型服务入口增加解密中间件。中间件按顺序做四件事解析密文包、校验时间戳、去重request_id、私钥解出AES密钥并解密。解密出来的明文prompt再交给原有业务逻辑处理。最终请求包结构参考如下{ version: 1, key_id: kek-2025-01, algorithm: AES-256-GCM, encrypted_key: base64..., nonce: base64..., ciphertext: base64..., timestamp: 1735689600, request_id: uuid-0001, client_id: client-a }字段的含义很直接。encrypted_key是RSA加密后的AES密钥ciphertext是加密后的提示词nonce是AES-GCM需要的随机数timestamp和request_id用来防重放client_id让审计能追溯到来源。这个结构不需要额外字段简洁清楚团队评审一次就通过了。第五步是日志脱敏与审计。从改造之日起所有日志模块禁止打印请求body。原日志系统里的body打印配置全部关闭。新增审计日志只记录request_id、client_id、key_id、密文摘要和处理时间。摘要用SHA256对密文整体计算既能做完整性核对又不泄露内容。4.3 上线测试与踩坑实录比预想多花了一倍时间上线前我们做了一套专项测试清单逐项过。表格里是我觉得最值得保留的几条。测试项预期结果实际结果无客户端证书请求TLS握手失败通过篡改密文包中的ciphertext解密抛认证失败通过过期证书请求握手失败通过重放同一个request_id第二个请求被拒绝通过查看网关和应用日志无明文prompt出现通过压测100并发平均耗时增加不超过20%通过测试过程中遇到三个比较烦的问题。第一个是AES-GCM的nonce重用。某个客户端在多线程环境下随机数生成使用了重复种子导致两个请求加密结果异常。排查后改用操作系统密码学安全随机源并在线程局部初始化。第二个是网关的body大小限制。提示词原本只有200KB加密后因为非对称密钥包裹和base64编码涨到接近400KB。网关默认client_max_body_size 1m其实没有触发但后面接的日志系统对body长度有限制报错过一次。调整所有中间层的上限后解决同时把监控告警阈值也调大否则有一天提示词变长时又要半夜爬起来看问题。第三个是证书过期问题。测试环境证书有效期设置成了90天结果在第三个月最后一周突然开始大量握手失败。因为没有做证书自动轮换和过期告警整个团队都没意识到。现在流程是证书剩余30天时发出提醒剩余7天时自动续签并热加载同时监控TLS握手失败率。这套机制上线后再没出现过证书引起的断连。4.4 一个经常被忽略的环节客户端证书的私钥保护服务端私钥大家都很重视但客户端私钥经常被忽略。模拟项目X中客户端A部署在分支机房的一台服务器上私钥文件权限默认是644同机房的运维账号可以直接读取。如果这台机器被入侵攻击者就能拿着私钥伪装成合法客户端调用模型服务。解决方案是收紧文件权限并且配置进程以专用账户运行。同时客户端证书有效期设置成180天用自动化任务定期更新避免长期有效证书造成更大的泄露面。整个过程不复杂但确实容易被人忽略尤其当大家把注意力都放在服务端加密时。5. 常见问题与避坑手册我踩过的那些坑5.1 证书与信任关系的典型问题证书问题在加密传输里占比很高很多故障现象都是“连接被重置”或“证书校验失败”但根因各有不同。我把最常见的几类整理成表格遇到过一条就能帮你省下半天排障时间。问题典型原因解决办法客户端报“无法验证服务端证书”服务端只发了叶子证书没有附带中间证书链配置ssl_certificate时使用完整chain文件握手成功但业务请求401客户端证书过期但网关没有启用OCSP吊销校验加入证书过期检查设置有效期告警自建CA私钥泄露权限设置过宽运维账号可读收回权限必要时重建CA并重新签发所有证书证书固定导致轮换断连客户端写死了服务端证书指纹改为固定CA公钥或预留备用信任锚点时间不同步导致判断失败服务器系统时间偏差超过证书有效期窗口配置NTP时间同步告警阈值设为10秒证书固定是个容易走极端的话题。固定指纹确实可以降低CA被攻击的风险但轮换时会非常痛苦。我更推荐固定公钥而不是完整指纹同时把新公钥的信任提前写入客户端保持至少两把公钥可用。这样轮换时可以做到不中断服务。5.2 加密模块性能与兼容性说明很多团队担心加密会让请求变慢或者模型调用超时。实测下来AES-GCM加密1MB以内的数据只需要几毫秒真正消耗在TLS握手和RSA非对称解包裹上。只要合理使用连接复用性能影响不会超过20%。优化空间有几个。一是尽量让客户端和网关之间长连接复用减少TLS握手次数。二是不要把每次请求都做RSA可以直接用临时会话密钥交换一次后在会话内多次使用同一个AES密钥但要控制会话时长。三是如果并发量特别大可以把RSA换成ECDH支持前向保密性能也更好。兼容性方面建议在请求包里增加version字段并让服务端同时支持新旧两种算法一段时间。灰度期间先把新客户端放量到10%观察日志和错误率再逐步提升比例。不要一次性全量切换尤其是老客户端可能没有可升级的SDK。5.3 日志里的“幽灵明文”解密后又被框架打印出来了有一次我们在测试环境验证密文包网关确实收到了密文业务日志的第一行也是解密后的明文提示词但再往下翻发现应用框架的debug日志把整个请求对象又打印了一遍包括解密后的prompt。这就是典型的“日志幽灵明文”。问题根源在于解密中间件把明文放回了请求对象框架的全局日志拦截器在请求结束时自动打印对象摘要。解决办法是给日志模块增加敏感字段过滤器明确禁止打印请求body并且把过滤规则覆盖到trace、debug、exception所有级别。审计日志也不要记录明文。如果非要保留payload用于排查只记录密文摘要和请求ID。就算日志系统被拖库攻击者拿到的也是一堆无意义的哈希。我还见过把Ciphertext本身打印出来的这其实也有风险因为同一段明文在不同时间加密会得到不同密文密文泄露不如明文直接但仍不建议记录完整密文摘要就足够了。5.4 怀疑密钥泄露后的应急处理流程密钥泄露这种事希望大家都别遇到但预案一定要有。一旦日志里出现异常调用、密钥文件权限被改变或审计发现某个client_id的请求量骤增按下面步骤处理第一立即吊销可疑客户端证书更新证书吊销列表。第二在KMS中轮换对应key_id的主密钥版本停止用旧版本加密新数据。第三刷新网关的信任列表把可疑客户端从白名单移除。第四导出最近一小时的审计日志按client_id、request_id、来源IP维度分析异常调用。第五通知所有对接方使用新的证书和密钥配置。处理过程中最忌讳的是盲目重启服务。服务重启不会解决密钥泄露问题反而可能把关键审计日志冲掉。先把现场的日志和请求摘录保留下来再去做吊销和轮换操作。这套方案落地之后我再回看整个改造过程最大的体会是加密传输不是给普通HTTP加个S就完事真正值得认真设计的是密钥生命周期和信任根管理。你甚至可以把加密逻辑本身写得非常漂亮但密钥一旦硬编码在代码库里整个方案就等于没有保护。只有把证书轮换自动化、日志脱敏、重放防御这几块基础工作做扎实加密传输才能长期稳定运行。如果后续要继续扩展可以在字段级加密和异常访问检测上做文章。但第一步永远是别让明文提示词出现在任何你觉得安全的日志里。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →