0xkaz/nanoguard

GitHub: 0xkaz/nanoguard

用 Rust 编写的超轻量 LLM 护栏代理,在微秒级完成提示注入拦截和 PII 脱敏,无需 GPU 或云端 API 调用,适合离线和边缘部署。

Stars: 1 | Forks: 0

# nanoguard [![CI](https://static.pigsec.cn/wp-content/uploads/repos/2026/05/71595d4b34141745.svg)](https://github.com/0xkaz/nanoguard/actions/workflows/ci.yml) [![Release](https://static.pigsec.cn/wp-content/uploads/repos/2026/05/ec61e298a8141752.svg)](https://github.com/0xkaz/nanoguard/actions/workflows/release.yml) [![ghcr.io](https://img.shields.io/badge/ghcr.io-nanoguard-blue)](https://github.com/0xkaz/nanoguard/pkgs/container/nanoguard) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) **LLM 护栏代理。Rust 编写。单一二进制文件。无需 GPU。无云端护栏调用。亚微秒级字面量拦截。** nanoguard 位于您的应用程序与任何兼容 OpenAI 的 LLM 后端之间, 在进程内过滤提示词和响应,无需外部护栏 API。 ``` [Your App / Robot / Edge Device / Open WebUI] ↓ POST /v1/chat/completions ┌──────────────────────────────────────┐ │ nanoguard │ │ Input Guardrails (<1 µs literal) │ ← keyword / PII regex / injection patterns │ Backend Router │ ← Ollama / OpenAI / Anthropic / any │ Output Guardrails (~4 µs) │ ← sensitive word masking, streaming │ Audit Log │ ← JSONL, SHA-256 hash-only mode └──────────────────────────────────────┘ ↓ [LLM Backend] ``` ## 问题所在 当您拥有可靠的互联网连接,并且能够容忍每个请求增加的云端往返延迟时,云端护栏 API(如 AWS Bedrock Guardrails、Azure AI Content Safety、Google Cloud)运作良好。对于网络聊天机器人来说,这没问题。 但在其他情况下,这是一种严重的架构不匹配: - **机器人和自主系统** 通常会暴露自然语言的操作命令、任务规划和车队控制接口,在这些接口中进行云端过滤可能会增加不可接受的延迟,或在连接不良时导致失败。 - **工业物联网和工厂车间** 通常在设计上运行在无出站互联网访问的隔离网络中。 - **医疗和政府系统** 出于政策原因,无论延迟如何,都无法将患者或机密数据路由通过外部 API。 - **边缘设备** —— 从仓库机器人到车载系统 —— 面临间歇性连接是一个物理现实,而不是边缘情况。 对于延迟敏感或离线的应用程序,安全过滤必须在本地运行 —— 对于那些无法停下来等待的系统来说,云端护栏是一个单点故障。 nanoguard 是一个确定性的、支持离线的护栏层,可以在任何能运行二进制文件的地方运行。 ## 设计决策 ### 为什么选择 Rust Rust 提供了无需垃圾回收器的确定性延迟,以及无需虚拟机的内存安全性。发布产物是一个紧凑的单一二进制文件 —— 没有解释器,没有包管理器,部署时无需更新任何内容。 Go 本会是一个合理的替代方案:相似的部署体验,更快的迭代速度。权衡之处在于 GC 暂停的可预测性,以及安全边界能够从编译时内存安全保证中获益的事实。对于一个位于每个 LLM 请求路径上的代理来说,延迟尾部(latency tail)和攻击面都非常重要。 ### 为什么选择确定性规则,而不是第二个 LLM 基于 LLM 的内容过滤很具吸引力 —— 它能处理关键字列表遗漏的细微差别。代价是:每次调用显著增加的延迟、对模型可用性的依赖,以及概率性而非可审计的判定。“护栏模型说允许”并不是一份合规记录。 对于大多数企业策略要求 —— 提示注入模式、PII 类别、偏离主题的领域、禁用关键词 —— 一个精心策划的规则集是足够的、确定性的,并且可以逐行审计。nanoguard 将该方法作为默认方案。基于 LLM 的过滤计划作为一项可选功能。 ### 为什么选择 Aho-Corasick + 正则表达式规则 大多数朴素的关键字过滤器复杂度为 O(N × M):每个模式扫描一次。nanoguard 使用 [aho-corasick](https://docs.rs/aho-corasick/) 处理字面量规则,因此许多提示注入短语、PII 术语和策略关键字在单次 O(N) 遍历中即可匹配。正则表达式规则针对需要结构的模式(例如电子邮件地址、SSN 和 API token 形式)单独编译。 默认引擎是 `aho-corasick`;较旧的 `iword-rs` 引擎仍可通过配置使用,以便进行比较和兼容。 ## 对比 | | nanoguard | AWS Bedrock Guardrails | Azure AI Content Safety | LLM Guard | LiteLLM | |---|---|---|---|---|---| | 部署方式 | 单一二进制文件 | 云端 API | 云端 API / 嵌入式† | Python 库 | Python 应用 | | 离线 | ✅ | ❌ | ❌ / △† | ✅ | ✅ | | 无需 GPU | ✅ | N/A | N/A | 取决于扫描器 | ✅ | | 过滤延迟 | ~0.2–65 µs* | 云端往返 | 云端往返 | 取决于模型 | 不适用 | | 外部护栏调用 | ❌ 从不 | ✅ 必需 | ✅ 必需 | ❌ 从不 | 视情况而定 | | 审计日志 | ✅ JSONL | ✅ CloudWatch | ✅ Azure Monitor | △ | ✅ (企业版) | \* 在本地测量;参见 [性能](#performance) 部分。 † Azure 嵌入式内容安全需要单独审批,且未全面提供。 LiteLLM 和多提供商网关解决的是不同的问题(跨提供商的路由、可观测性)。nanoguard 旨在作为专用策略边界部署在这些工具之前,而不是取代它们。 ## 快速开始 ### Docker(推荐) ``` # Architecture 自动检测:arm64 / amd64 docker run -p 8080:8080 \ -e BACKEND_ENDPOINT=http://host.docker.internal:11434 \ ghcr.io/0xkaz/nanoguard:latest ``` 带配置文件: ``` docker run -p 8080:8080 \ -v $(pwd)/nanoguard.toml:/app/nanoguard.toml:ro \ ghcr.io/0xkaz/nanoguard:latest ``` ### 使用 Ollama(从源码构建) ``` git clone https://github.com/0xkaz/nanoguard cd nanoguard make run # pulls qwen3:0.6b, starts nanoguard on :8080 ``` ``` # 适用于任何 OpenAI client 的 Drop-in curl http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"qwen3:0.6b","messages":[{"role":"user","content":"Hello!"}]}' ``` ### 使用 OpenAI ``` export OPENAI_API_KEY=sk-... export BACKEND_PROVIDER=openai export BACKEND_ENDPOINT=https://api.openai.com export BACKEND_MODEL=gpt-4o-mini make run ``` ## 端点 | 端点 | 描述 | |----------|-------------| | `POST /v1/chat/completions` | 兼容 OpenAI 的聊天 | | `POST /v1/messages` | 兼容 Anthropic 的聊天(文本 + `tool_use`;暂不支持视觉和流式传输) | | `GET /v1/models` | 代理到后端模型列表 | | `GET /health` | 健康检查 | | `GET /v1/admin/budget/:api_key` | Token 使用量 + 限制(需要管理员密钥) | | `PUT /v1/admin/budget/:api_key` | 设置限制 `{"limit": 100000}` | | `DELETE /v1/admin/budget/:api_key/reset` | 重置使用量计数器 | ## 护栏 ### 输入(发送到 LLM 之前) 字面量规则使用 Aho-Corasick 扫描;正则表达式规则在启动时编译一次。被阻止的请求永远不会到达后端。 | 结果 | 动作 | |--------|--------| | **Blocked** | 立即拒绝,LLM 从不被调用 | | **Alert** | 记录日志,转发 | | **Flagged** | 记录日志,转发 | 默认被阻止的模式(完全可配置): ``` ignore previous instructions · disregard your instructions · jailbreak · dan mode · you are now ``` #### 转发前的 PII 脱敏 当 `input.pii.enabled = true` 时,系统会使用一组可配置的命名实体模式扫描请求。默认集涵盖电子邮件、美国 SSN、信用卡、AWS 访问密钥、GitHub PAT、OpenAI/Anthropic/Stripe/Google API 密钥、JWT、Slack token 以及通用的极高熵 token 兜底规则。您可以通过 `input.pii.dict_paths` 添加自己的规则。 | 动作 | 行为 | |---|---| | `mask`(默认) | **在转发到 LLM 之前**,将请求体中的每个匹配项替换为 `[]` 占位符(例如 `[EMAIL]`、`[AWS_ACCESS_KEY_ID]`、`[JWT]`)。LLM 接收到脱敏后的提示词;原始内容从不会发送。 | | `reject` | 如果有任何模式匹配,则完全阻止该请求。 | | `log` | 原样转发,但记录一条 ALERT 日志。 | 在请求离开进程之前,`/v1/chat/completions`(字符串内容和 `parts[].text` 数组)与 `/v1/messages`(Anthropic 的 `Text` 和 `Blocks` 形式)都会被脱敏。 用户字典遵循标准格式,第二列为实体名称: ``` # dicts/my-org-pii.txt /\bSEC-\d{6}\b/ INTERNAL_TICKET /\bEMP-[A-Z0-9]{8}\b/ EMPLOYEE_ID ``` ``` [input.pii] enabled = true action = "mask" dict_paths = ["dicts/my-org-pii.txt"] placeholder_style = "bare" # "bare" | "indexed" | "llm_guard" ``` `placeholder_style` 控制匹配项的格式化方式。默认的 `bare` 样式输出 `[EMAIL]`,速度最快,但会失去不同值之间的区别。`indexed` 输出 `[EMAIL_1]`、`[EMAIL_2]`,以便 LLM 能区分同一提示词中的两封不同电子邮件 —— 这也是迈向基于 Vault 的逆向匿名化的垫脚石。`llm_guard` 输出 `[REDACTED_EMAIL_1]`,以保持与遵循 LLM Guard 约定的提示词的线路兼容性。 #### 按实体动作覆盖 全局 `action` 设置默认应用于每个实体,但您可以按实体进行覆盖。`reject` 始终优先:如果任何 reject 类别的实体匹配,请求将在任何掩码处理运行之前被阻止。 ``` [input.pii] action = "mask" # default for entities not listed below [input.pii.entities] AWS_ACCESS_KEY_ID = "reject" ANTHROPIC_KEY = "reject" GITHUB_PAT = "reject" JWT = "reject" EMAIL = "mask" PHONE = "log" ``` 脱敏器中不存在的实体(例如拼写错误)将被静默忽略。实体名称区分大小写,并与脱敏器在占位符中输出的名称匹配。 #### 可逆脱敏(Vault 往返) 当 `reversible = true` 时,掩码类别的匹配项将记录在每个请求的 Vault 中,并从 LLM 的响应中恢复。模型永远不会看到原始内容;客户端看到的是答案中保留的原始提示上下文。 ``` [input.pii] enabled = true action = "mask" reversible = true deanonymize_strategy = "exact" # "exact" (default) | "case_insensitive" ``` 往返示例: ``` client → nanoguard: "My email is alice@example.com" nanoguard → LLM: "My email is [EMAIL_1]" LLM → nanoguard: "You said: My email is [EMAIL_1]" nanoguard → client: "You said: My email is alice@example.com" ``` Vault 作用域是 **每个请求** 的 —— 在一个请求中设置的占位符对另一个请求永远不可见。会话作用域的 Vault(使得第 2 轮能看到第 1 轮的 `[EMAIL_1]`)计划在未来作为可选功能提供。 流式响应通过一个状态机进行去匿名化处理,该状态机缓冲从 `[` 到 `]` 的内容,因此跨 SSE 块拆分的占位符也能被正确还原。 `deanonymize_strategy = "fuzzy"` 和 `"combined"` 是保留名称,目前会带警告地回退到 `exact`。 #### 混淆处理 输入在扫描之前会被标准化。默认启用两种转换 —— 它们对 ASCII 输入几乎没有性能损耗(NFKC 通过快速路径跳过),并且只捕获那些会漏网的攻击: - **NFKC Unicode 标准化** —— 将全角字母(如 `jailbreak`)在匹配前折叠为 ASCII。 - **零宽字符剥离** —— `j​ailb​reak` 变成 `jailbreak`。 另外两种转换是可选的,因为它们可能在合法文本上产生误报: - **`separators = true`** —— 折叠由 `-` `.` `_` `*` `~` 连接的单个字母序列(例如 `j-a-i-l-b-r-e-a-k`)。≥4 长度的阈值保留了常见的带连字符词汇,如 `co-op` 和 `e-mail`。 - **`leet = true`** —— 将数字和符号折叠为字母(`j41lbr34k` → `jailbreak`)。默认禁用,因为它会影响合法文本,例如 `3D printer` 或 `S3 bucket`。 ### 输出(返回给客户端之前) 响应在到达您的应用之前会被过滤。敏感词会被替换为 `***`。 流式 SSE 响应跨块边界缓冲,并在过滤每个事件的 `delta.content` 之前,按 SSE 事件终止符进行拆分。这处理了将多个事件打包到单个 TCP 块中或将单个事件拆分跨多个块的后端情况。非数据行(`event:`、注释、`[DONE]`)原样传递。 ### 聚光灯标记(间接注入防御) 聚光灯标记会标记不受信任的内容 —— 通常是 `tool` 或 `function` 角色消息中传递的 RAG 块 —— 以便提醒 LLM 将其视为数据而非指令。nanoguard 提供三种转换: - `datamarking`(默认):用标记字符(例如 `^`)替换不受信任内容中的空格,使该区域在视觉上明显为预处理数据。 - `delimiting`:使用 `<>`...`<>` 标记包裹内容。 - `encoding`:对内容进行 base64 编码(最强的隔离,最低的回答质量 —— 可选功能)。 系统附加指令(rider)会自动注入,以便模型知道这些标记的含义。如果没有附加指令,包裹就只是一种安全假象。 ``` [input.spotlight] enabled = true method = "datamarking" # "datamarking" | "delimiting" | "encoding" untrusted_roles = ["tool"] ``` 用户和系统消息保持不变;只转换列出的角色。聚光灯标记在 PII 脱敏之后运行,因此在应用数据标记时,可逆脱敏的占位符已经就位。 ### 影子模式 设置 `[input] shadow = true` 可扫描并审计每个请求,而不实际阻止。*本该* 被阻止的请求会通过传递到后端,但审计日志会将判定结果记录为 `flag`,并带有前缀为 `shadow_block:` 的匹配规则。这在生产环境中推出新规则集时非常有用 —— 您可以在强制执行前确认误报率。 ``` { "verdict": "flag", "matched_rule": "shadow_block:jailbreak", ... } ``` ### 审计日志 启用后,每个请求会写入一行 JSONL: ``` { "request_id": "000000000000000018adfc678fe51118", "timestamp": "2026-05-09T19:29:09.607304+00:00", "api_key": "default", "model": "qwen3:0.6b", "prompt_hash": "d4c067494508331c38975ab3356c6f213c1f571969d3d2c1ad8d1c45d1bc20db", "verdict": "block", "matched_rule": "jailbreak", "latency_us": 102 } ``` `hash_only =`(默认):仅记录提示词的 SHA-256 哈希 —— 不存储原始文本。 适用于限制保留用户输入的合规环境。 ## 配置 `nanoguard.toml`: ``` [nanoguard] listen = "0.0.0.0:8080" log_level = "info" [backend] provider = "ollama" endpoint = "http://localhost:11434" # api_key = "sk-..." # model = "llama3.2" [input] enabled = true shadow = false # set true to log "would-block" without enforcing — see Shadow mode [input.keyword] engine = "aho-corasick" # "aho-corasick" (default) or "iword-rs" dict_paths = [] # additional .txt word list files inline_block = ["ignore previous instructions", "jailbreak"] inline_alert = ["password", "api_key"] inline_flag = ["bitcoin", "crypto"] [input.keyword.normalize] nfkc = true # full-width → half-width, combining char folding zero_width = true # strip U+200B/C/D, U+FEFF separators = false # opt-in: collapse "j-a-i-l-b-r-e-a-k" → "jailbreak" leet = false # opt-in: 3→e, 0→o, 1→i, 4→a, 5→s, 7→t, @→a, $→s [input.pii] enabled = true action = "mask" # mask | reject | log [output] enabled = true [budget] enabled = false db_path = "nanoguard.db" # admin_api_key = "your-secret-key" [audit] enabled = false path = "nanoguard-audit.jsonl" hash_only = true ``` ### 环境变量 | 变量 | 默认值 | 描述 | |----------|---------|-------------| | `NANOGUARD_CONFIG` | `nanoguard.toml` | 配置文件路径 | | `BACKEND_PROVIDER` | `ollama` | `ollama` / `openai` / 任何兼容 OpenAI 的提供商 | | `BACKEND_ENDPOINT` | `http://localhost:11434` | 后端基础 URL | | `BACKEND_API_KEY` | — | API 密钥(也会读取 `OPENAI_API_KEY`) | | `BACKEND_MODEL` | — | 默认模型名称 | | `RUST_LOG` | `info` | 日志级别 | | `ADMIN_API_KEY` | — | 启用 `/v1/admin/budget/*` 端点 | ### 预算跟踪 预算跟踪使用 OpenAI 的 `user` 字段作为预算密钥标识符,当省略 `user` 时回退到 `default`。使用量从包含兼容 OpenAI 的 `usage.prompt_tokens` 和 `usage.completion_tokens` 的非流式后端响应中记录。 当客户端设置 OpenAI 的 `stream_options: {"include_usage": true}` 时,流式响应也会记录 token 使用量 —— 最后一个块携带 `usage`,nanoguard 会在流完成后捕获并转发给预算存储。省略该选项的后端或客户端将继续流式传输,而无需进行支出核算。 ## 构建 ``` make # release binary → target/release/nanoguard make dev # debug build + run make test # cargo test make check # clippy + fmt make bench # criterion benchmarks → target/criterion/ ``` 要求:Rust 1.75+ ## 字典文件 nanoguard 字典格式 —— 以制表符分隔: ``` # word key weight confidential 0 # BLOCK internal only 0 5.0 # BLOCK, weighted project_xyz 1 # ALERT ``` ``` [input.keyword] dict_paths = ["dicts/company.txt", "dicts/prompt_injection.txt"] ``` ## 性能 在 Apple M 系列、单核、release 构建、热缓存上测量。 **这些仅为护栏成本 —— 不包括 HTTP 开销和 LLM 往返时间。** | 操作 | 耗时 | |---|---| | 输入检查 — 干净 | ~0.5 µs | | 输入检查 — 被阻止(提前退出) | ~0.2 µs | | 输入检查 — 长文本干净(约 2,700 字符) | ~65 µs | | 输出过滤 — 无匹配 | ~4 µs | | 输出过滤 — 掩码 SSN + 信用卡 | ~7 µs | 对于字面量规则,输入扫描的复杂度与提示词长度呈 O(N) 关系。正则表达式规则在字面量 BLOCK 检查之后进行评估。 ## 安全性 nanoguard 在设计上最小化了外部依赖。 所有的过滤都在进程内运行,没有网络调用,运行时也没有动态代码。 发布二进制文件是静态链接的 —— 您审计的内容即运行的内容。 ### 局限性 nanoguard 是一个 **策略执行层**,而不是一个抗对抗性攻击的安全边界。 默认的标准化(NFKC + 零宽字符剥离)和可选的转换(`separators`, `leet`)可以捕获常见的混淆模式,但基于规则的过滤在面对使用释义、 base64、多语种变体或新颖编码的且有足够动机的攻击者时总会失败。它专为以下场景设计: - 合规与审计追踪 - 防止意外误用(来自不受信任内容的提示注入) - 强制执行组织关于 LLM 使用的策略 它 **不** 旨在击败试图主动绕过过滤器的对抗性用户。 对于包含有动机攻击者的威胁模型,请将 nanoguard 与其他控制措施结合使用。 流式输出过滤是按 SSE 事件进行的。后端真正跨两个事件拆分的敏感 token(例如先出现 `"ss"` 然后出现 `"n"`)将不会被检测到;对于高保证使用场景,建议将 nanoguard 与全响应扫描结合使用。 ## 致谢 输入字面量匹配使用 [aho-corasick](https://docs.rs/aho-corasick/)。旧的 `iword-rs` 支持仍作为备用引擎可用。
标签:Anthropic, API网关, CISA项目, CIS基准, DLL 劫持, LangChain, LLM, LLM Guardrails, LLM评估, Naabu, Ollama, OpenAI兼容, Petitpotam, PII过滤, Rust, Unmanaged PE, 个人身份信息过滤, 代理, 内容安全, 单文件, 反向代理, 可视化界面, 大模型安全, 大语言模型, 安全网关, 审计日志, 微秒级, 提示注入过滤, 敏感词屏蔽, 数据隐私, 无GPU, 无文件攻击, 本地大模型, 本地推理, 流处理, 离线优先, 纯CPU, 网络安全, 网络机器人, 网络流量审计, 自主系统, 请求拦截, 轻量级, 边缘计算, 通知系统, 隐私保护, 零依赖云