LibreFang 安全架构
安全联系方式: 请通过 GitHub 私密漏洞报告功能提交:
https://github.com/librefang/librefang/security/advisories/new。
本文档为 LibreFang Agent 操作系统中的每个安全子系统提供完整的技术参考。 所有结构体名称、函数签名、常量值和算法描述均直接来源于源代码。
目录
- 安全概述
- 基于能力的安全模型
- WASM 双重计量
- Merkle 哈希链审计追踪
- 信息流污点追踪
- Ed25519 清单签名
- SSRF 防护
- 密钥零化
- OFP 双向认证
- 安全响应头
- GCRA 速率限制器
- 路径遍历防护
- 子进程沙箱
- 提示注入扫描器
- 循环守卫
- 会话修复
- 健康端点信息脱敏
- Dashboard 认证
- TOTP 二次验证
- 安全配置
- 安全相关依赖
1. 安全概述
LibreFang 采用纵深防御安全策略。系统不依赖任何单一机制作为唯一 保护手段,而是由 16 个独立的安全子系统形成相互重叠的防护层,确保任何 单层失效都能被其他层捕获。
| # | 系统 | 所属 Crate | 防护目标 |
|---|---|---|---|
| 1 | 基于能力的安全模型 | librefang-types | Agent 未授权操作 |
| 2 | WASM 双重计量 | librefang-runtime | 无限循环、CPU 拒绝服务 |
| 3 | Merkle 审计追踪 | librefang-runtime | 审计日志篡改 |
| 4 | 污点追踪 | librefang-types | 提示注入、数据外泄 |
| 5 | Ed25519 清单签名 | librefang-types | 供应链攻击 |
| 6 | SSRF 防护 | librefang-runtime | 服务端请求伪造 |
| 7 | 密钥零化 | librefang-runtime、librefang-channels | 内存取证、密钥泄露 |
| 8 | OFP 双向认证 | librefang-wire | 未授权的对等节点连接 |
| 9 | 安全响应头 | librefang-api | XSS、点击劫持、MIME 嗅探 |
| 10 | GCRA 速率限制器 | librefang-api | API 滥用、拒绝服务 |
| 11 | 路径遍历防护 | librefang-runtime | 目录遍历攻击 |
| 12 | 子进程沙箱 | librefang-runtime | 子进程泄露密钥 |
| 13 | 提示注入扫描器 | librefang-skills | 恶意技能提示词 |
| 14 | 循环守卫 | librefang-runtime | Agent 工具调用死循环 |
| 15 | 会话修复 | librefang-runtime | LLM 对话历史损坏 |
| 16 | 健康端点信息脱敏 | librefang-api | 信息泄露 |
| 17 | TOTP 二次验证 | librefang-kernel | 被攻破的渠道自我审批 |
2. 基于能力的安全模型
源码: librefang-types/src/capability.rs
LibreFang 采用基于能力的安全模型。Agent 只能执行被明确授权的操作。 能力在 Agent 创建后不可变,并在内核层面强制执行。
2.1 能力变体
Capability 枚举定义了所有权限类型:
pub enum Capability {
// Filesystem
FileRead(String), // Glob pattern, e.g. "/data/*"
FileWrite(String),
// Network
NetConnect(String), // Host:port pattern, e.g. "*.openai.com:443"
NetListen(u16),
// Tools
ToolInvoke(String), // Specific tool ID
ToolAll, // All tools (dangerous)
// LLM
LlmQuery(String),
LlmMaxTokens(u64),
// Agent interaction
AgentSpawn,
AgentMessage(String),
AgentKill(String),
// Memory
MemoryRead(String),
MemoryWrite(String),
// Shell
ShellExec(String),
EnvRead(String),
// OFP Wire Protocol
OfpDiscover,
OfpConnect(String),
OfpAdvertise,
// Economic
EconSpend(f64),
EconEarn,
EconTransfer(String),
}
2.2 模式匹配
capability_matches(granted, required) 函数实现了 glob 风格的匹配:
- 精确匹配:
"api.openai.com:443"匹配"api.openai.com:443" - 全通配符:
"*"匹配任意值 - 前缀通配符:
"*.openai.com:443"匹配"api.openai.com:443" - 后缀通配符:
"api.*"匹配"api.openai.com" - 中间通配符:
"api.*.com"匹配"api.openai.com" - ToolAll 特殊情况:
ToolAll可授权任意ToolInvoke(_) - 数值边界:
LlmMaxTokens(10000)可授权LlmMaxTokens(5000)(授予值 >= 请求值)
2.3 执行检查点
在 WASM 沙箱中,每个宿主调用在执行之前都会通过 host_functions.rs
中的 check_capability() 进行检查:
fn check_capability(
capabilities: &[Capability],
required: &Capability,
) -> Result<(), serde_json::Value> {
for granted in capabilities {
if capability_matches(granted, required) {
return Ok(());
}
}
Err(json!({"error": format!("Capability denied: {required:?}")}))
}
如果没有任何已授予的能力与所需能力匹配,操作会立即返回 JSON 错误——工具永远不会被调用。
2.4 能力继承
当 Agent 生成子 Agent 时,validate_capability_inheritance() 会确保
子 Agent 的能力是父 Agent 能力的子集。这可以防止权限提升:
pub fn validate_capability_inheritance(
parent_caps: &[Capability],
child_caps: &[Capability],
) -> Result<(), String> {
for child_cap in child_caps {
let is_covered = parent_caps
.iter()
.any(|parent_cap| capability_matches(parent_cap, child_cap));
if !is_covered {
return Err(format!(
"Privilege escalation denied: child requests {:?} \
but parent does not have a matching grant",
child_cap
));
}
}
Ok(())
}
host_functions.rs 中的 host_agent_spawn() 函数调用
kernel.spawn_agent_checked(manifest_toml, Some(&state.agent_id), &state.capabilities),
在子 Agent 创建之前执行此验证。
3. WASM 双重计量
源码: librefang-runtime/src/sandbox.rs
不受信任的 WASM 模块在 Wasmtime 沙箱中运行,同时启用两个独立的 计量机制。
3.1 燃料计量(确定性)
燃料计量统计 WASM 指令数。引擎为每条执行的指令扣除燃料。当预算耗尽时,
执行会以 Trap::OutOfFuel 中断。
// SandboxConfig defaults
pub fuel_limit: u64, // Default: 1_000_000
// Applied at execution time
if config.fuel_limit > 0 {
store.set_fuel(config.fuel_limit)?;
}
执行结束后会报告燃料消耗量:
let fuel_remaining = store.get_fuel().unwrap_or(0);
let fuel_consumed = config.fuel_limit.saturating_sub(fuel_remaining);
3.2 纪元中断(墙钟时间)
看门狗线程休眠指定的超时时长,然后递增引擎纪元。当纪元超过 store 的
截止期限时,执行会以 Trap::Interrupt 中断。
store.set_epoch_deadline(1);
let engine_clone = engine.clone();
let timeout = config.timeout_secs.unwrap_or(30);
let _watchdog = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(timeout));
engine_clone.increment_epoch();
});
3.3 为什么需要两种机制?
| 属性 | 燃料 | 纪元 |
|---|---|---|
| 度量指标 | 指令数 | 墙钟时间 |
| 精确度 | 确定性,可复现 | 非确定性 |
| 可捕获 | CPU 密集型循环 | 宿主调用阻塞、I/O 等待 |
| 可规避 | 可在宿主调用中浪费时间 | 可廉价地忙等循环 |
两者组合形成完整防御:燃料捕获计算密集型循环,纪元捕获宿主调用滥用或 环境延迟。
3.4 SandboxConfig
pub struct SandboxConfig {
pub fuel_limit: u64, // Default: 1_000_000
pub max_memory_bytes: usize, // Default: 16 MB
pub capabilities: Vec<Capability>,
pub timeout_secs: Option<u64>, // Default: 30 seconds
}
3.5 错误类型
pub enum SandboxError {
Compilation(String),
Instantiation(String),
Execution(String),
FuelExhausted, // Trap::OutOfFuel
AbiError(String),
}
4. Merkle 哈希链审计追踪
源码: librefang-runtime/src/audit.rs
每个安全关键操作都会追加到一条防篡改的 Merkle 哈希链中,类似于区块链。 每个条目包含其自身内容与前一个条目哈希值拼接后的 SHA-256 哈希。
4.1 可审计的操作
pub enum AuditAction {
ToolInvoke,
CapabilityCheck,
AgentSpawn,
AgentKill,
AgentMessage,
MemoryAccess,
FileAccess,
NetworkAccess,
ShellExec,
AuthAttempt,
WireConnect,
ConfigChange,
}
4.2 条目结构
pub struct AuditEntry {
pub seq: u64, // Monotonically increasing sequence number
pub timestamp: String, // ISO-8601
pub agent_id: String,
pub action: AuditAction,
pub detail: String, // e.g. tool name, file path
pub outcome: String, // "ok", "denied", error message
pub prev_hash: String, // SHA-256 of previous entry (or 64 zeros)
pub hash: String, // SHA-256 of this entry + prev_hash
}
4.3 哈希计算
每个条目的哈希值由其所有字段与前一条目的哈希值拼接后计算得出:
fn compute_entry_hash(
seq: u64, timestamp: &str, agent_id: &str,
action: &AuditAction, detail: &str,
outcome: &str, prev_hash: &str,
) -> String {
let mut hasher = Sha256::new();
hasher.update(seq.to_string().as_bytes());
hasher.update(timestamp.as_bytes());
hasher.update(agent_id.as_bytes());
hasher.update(action.to_string().as_bytes());
hasher.update(detail.as_bytes());
hasher.update(outcome.as_bytes());
hasher.update(prev_hash.as_bytes());
hex::encode(hasher.finalize())
}
4.4 链完整性验证
AuditLog::verify_integrity() 遍历整条链并重新计算每个哈希值。如果
任何条目被篡改,重新计算的哈希值将与存储的哈希值不匹配,或者
prev_hash 链接将断裂:
pub fn verify_integrity(&self) -> Result<(), String> {
let entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
let mut expected_prev = "0".repeat(64); // Genesis sentinel
for entry in entries.iter() {
if entry.prev_hash != expected_prev {
return Err(format!(
"chain break at seq {}: expected prev_hash {} but found {}",
entry.seq, expected_prev, entry.prev_hash
));
}
let recomputed = compute_entry_hash(/* ... */);
if recomputed != entry.hash {
return Err(format!(
"hash mismatch at seq {}: expected {} but found {}",
entry.seq, recomputed, entry.hash
));
}
expected_prev = entry.hash.clone();
}
Ok(())
}
4.5 线程安全
AuditLog 使用 Mutex<Vec<AuditEntry>> 和 Mutex<String> 来保护
链尾哈希。两个锁都使用 unwrap_or_else(|e| e.into_inner()) 从中毒的
互斥锁中恢复,确保审计日志在 panic 之后仍然可用。
4.6 API
| 方法 | 描述 |
|---|---|
AuditLog::new() | 创建空日志,初始哨兵为 "0" * 64 |
record(agent_id, action, detail, outcome) | 追加条目,返回其哈希值 |
verify_integrity() | 验证整条链的完整性 |
tip_hash() | 返回最新条目的哈希值 |
len() / is_empty() | 条目计数 |
recent(n) | 返回最近 n 个条目(克隆) |
5. 信息流污点追踪
源码: librefang-types/src/taint.rs
LibreFang 实现了基于格的污点传播模型,防止被污染的值在未经显式解密的 情况下流入敏感汇聚点。此机制可防范提示注入、数据外泄和混淆代理攻击。
5.1 污点标签
pub enum TaintLabel {
ExternalNetwork, // Data from external network requests
UserInput, // Direct user input
Pii, // Personally identifiable information
Secret, // API keys, tokens, passwords
UntrustedAgent, // Data from sandboxed/untrusted agents
}
5.2 被污染的值
pub struct TaintedValue {
pub value: String, // The payload
pub labels: HashSet<TaintLabel>, // Attached taint labels
pub source: String, // Human-readable origin
}
关键方法:
| 方法 | 描述 |
|---|---|
TaintedValue::new(value, labels, source) | 创建带标签的值 |
TaintedValue::clean(value, source) | 创建无标签的值(未污染) |
merge_taint(&mut self, other) | 标签取并集(用于拼接场景) |
check_sink(&self, sink) | 检查值是否可以流入指定汇聚点 |
declassify(&mut self, label) | 移除特定标签(显式安全决策) |
is_tainted(&self) -> bool | 如果存在任何标签则返回 true |
5.3 污点汇聚点
TaintSink 定义了哪些标签被阻止到达该汇聚点:
| 汇聚点 | 被阻止的标签 | 原因 |
|---|---|---|
TaintSink::shell_exec() | ExternalNetwork、UntrustedAgent、UserInput | 防止命令注入 |
TaintSink::net_fetch() | Secret、Pii | 防止数据外泄 |
TaintSink::agent_message() | Secret | 防止密钥泄露给其他 Agent |
5.4 违规处理
当 check_sink() 发现被阻止的标签时,会返回 TaintViolation:
pub struct TaintViolation {
pub label: TaintLabel, // The offending label
pub sink_name: String, // "shell_exec", "net_fetch", etc.
pub source: String, // Where the tainted value came from
}
输出示例:taint violation: label 'Secret' from source 'env_var' is not allowed to reach sink 'net_fetch'
5.5 解密
解密是一个显式的安全决策。调用方声明该值已经过净化处理:
tainted.declassify(&TaintLabel::ExternalNetwork);
tainted.declassify(&TaintLabel::UserInput);
// After declassification, value can flow to shell_exec
assert!(tainted.check_sink(&TaintSink::shell_exec()).is_ok());
5.6 污点传播
当两个值被组合(拼接、插值)时,结果必须携带两者标签集的并集:
let mut combined = TaintedValue::new(/* ... */);
combined.merge_taint(&other_value);
// combined.labels is now the union of both
6. Ed25519 清单签名
源码: librefang-types/src/manifest_signing.rs
Agent 清单定义了 Agent 的能力、工具和配置。被篡改的清单可以授予 提升的权限。本模块提供基于 Ed25519 的加密签名。
6.1 签名方案
- 计算清单内容(原始 TOML 文本)的 SHA-256 哈希。
- 使用 Ed25519(通过
ed25519-dalek)对哈希进行签名。 - 将签名、公钥和内容哈希打包到
SignedManifest信封中。
6.2 SignedManifest 结构
pub struct SignedManifest {
pub manifest: String, // Raw TOML content
pub content_hash: String, // Hex SHA-256 of manifest
pub signature: Vec<u8>, // Ed25519 signature (64 bytes)
pub signer_public_key: Vec<u8>, // Ed25519 public key (32 bytes)
pub signer_id: String, // Human-readable signer ID
}
6.3 签名过程
let signing_key = SigningKey::generate(&mut OsRng);
let signed = SignedManifest::sign(manifest_toml, &signing_key, "admin@org.com");
内部实现:
pub fn sign(manifest: impl Into<String>, signing_key: &SigningKey, signer_id: impl Into<String>) -> Self {
let manifest = manifest.into();
let content_hash = hash_manifest(&manifest); // SHA-256
let signature = signing_key.sign(content_hash.as_bytes());
let verifying_key = signing_key.verifying_key();
Self {
manifest,
content_hash,
signature: signature.to_bytes().to_vec(),
signer_public_key: verifying_key.to_bytes().to_vec(),
signer_id: signer_id.into(),
}
}
6.4 验证过程
两阶段验证:
- 哈希校验: 重新计算
manifest的 SHA-256 并与content_hash比较。 - 签名校验: 使用
signer_public_key验证content_hash上的 Ed25519 签名。
pub fn verify(&self) -> Result<(), String> {
let recomputed = hash_manifest(&self.manifest);
if recomputed != self.content_hash {
return Err("content hash mismatch: ...");
}
let verifying_key = VerifyingKey::from_bytes(&pk_bytes)?;
let signature = Signature::from_bytes(&sig_bytes);
verifying_key.verify(self.content_hash.as_bytes(), &signature)
.map_err(|e| format!("signature verification failed: {}", e))
}
6.5 篡改检测
- 签名后修改清单内容会导致内容哈希不匹配。
- 替换公钥为不同的密钥会导致签名验证失败。
- 两种攻击都会被
verify()捕获。
7. SSRF 防护
源码: librefang-runtime/src/host_functions.rs
host_net_fetch 函数(用于网络请求的 WASM 宿主调用)包含全面的
服务端请求伪造防护。
7.1 协议验证
仅允许 http:// 和 https:// 协议。所有其他协议(file://、
gopher://、ftp://)会被立即阻止:
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err(json!({"error": "Only http:// and https:// URLs are allowed"}));
}
7.2 主机名黑名单
在 DNS 解析之前,以下主机名会被阻止:
localhostmetadata.google.internalmetadata.aws.internalinstance-data169.254.169.254(AWS/GCP 元数据端点)
7.3 DNS 解析检查
在主机名黑名单检查之后,函数会将主机名解析为 IP 地址,并检查每个 解析出的 IP 是否属于私有地址范围。这可以防御 DNS 重绑定攻击:
let socket_addr = format!("{hostname}:{port}");
if let Ok(addrs) = socket_addr.to_socket_addrs() {
for addr in addrs {
let ip = addr.ip();
if ip.is_loopback() || ip.is_unspecified() || is_private_ip(&ip) {
return Err(json!({"error": format!(
"SSRF blocked: {hostname} resolves to private IP {ip}"
)}));
}
}
}
7.4 私有 IP 检测
is_private_ip() 函数覆盖以下范围:
IPv4:
10.0.0.0/8-- RFC 1918172.16.0.0/12-- RFC 1918192.168.0.0/16-- RFC 1918169.254.0.0/16-- 链路本地(AWS 元数据)
IPv6:
fc00::/7-- 唯一本地地址fe80::/10-- 链路本地
fn is_private_ip(ip: &std::net::IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => {
let octets = v4.octets();
matches!(
octets,
[10, ..] | [172, 16..=31, ..] | [192, 168, ..] | [169, 254, ..]
)
}
IpAddr::V6(v6) => {
let segments = v6.segments();
(segments[0] & 0xfe00) == 0xfc00 || (segments[0] & 0xffc0) == 0xfe80
}
}
}
7.5 主机提取
extract_host_from_url() 解析 URL 以提取 host:port,同时用于 SSRF
检查和能力匹配:
https://api.openai.com/v1/chat -> api.openai.com:443
http://localhost:8080/api -> localhost:8080
http://example.com -> example.com:80
7.6 SSRF 白名单(自托管 / K8s)
自托管和 Kubernetes 部署通常需要 Agent 访问内部服务(例如企业代理后面的
私有 API)。[tools.web_fetch] ssrf_allowed_hosts 配置项允许运维人员
显式豁免原本会被私有 IP 检查拦截的 CIDR 或主机名。
云元数据地址段(169.254.0.0/16、100.64.0.0/10)无条件拦截,
无法通过白名单豁免。
配置示例
[tools.web_fetch]
ssrf_allowed_hosts = [
"10.0.0.0/8", # CIDR — 整个 RFC-1918 /8 段
"172.16.0.0/12", # CIDR — RFC-1918 /12 段
"*.internal.example.com", # glob 前缀通配符
"svc.cluster.local", # 字面主机名
"192.168.1.100", # 字面 IP
]
匹配规则
| 条目格式 | 示例 | 匹配对象 |
|---|---|---|
| CIDR | "10.0.0.0/8" | 目标主机的解析 IP |
| glob 前缀 | "*.internal.example.com" | URL 中的主机名(后缀匹配) |
| 字面 IP | "192.168.1.100" | 目标主机的解析 IP |
| 字面主机名 | "svc.cluster.local" | URL 中的主机名(大小写不敏感) |
仍然会被拦截的地址
即使白名单中有匹配条目,以下地址始终会被拒绝:
169.254.0.0/16— 链路本地 / AWS EC2 实例元数据100.64.0.0/10— CGNAT(包括阿里云 IMDS100.100.100.200)- 主机名黑名单中的所有条目(
localhost、metadata.google.internal等)
8. 密钥零化
源码: 所有 LLM 驱动模块、通道适配器和网络搜索模块。
LibreFang 在所有持有密钥材料的字段上使用 zeroize crate 提供的
Zeroizing<String>。当值被销毁时,其内存会被零覆盖,防止密钥在
内存中残留。
8.1 工作原理
Zeroizing<T> 是 zeroize crate 提供的智能指针包装器。它实现了
Deref<Target=T> 以实现透明使用,以及 Drop 以实现自动零化:
// On Drop, the inner String's buffer is overwritten with zeros
let key = Zeroizing::new("sk-secret-key".to_string());
// Use key transparently via Deref
client.post(url).header("authorization", format!("Bearer {}", &*key));
// When key goes out of scope, memory is zeroed
8.2 使用零化的字段
LLM 驱动 (librefang-runtime/src/drivers/):
| 驱动 | 字段 |
|---|---|
AnthropicDriver | api_key: Zeroizing<String> |
GeminiDriver | api_key: Zeroizing<String> |
OpenAiCompatDriver | api_key: Zeroizing<String> |
通道适配器 (librefang-channels/src/):
| 适配器 | 字段 |
|---|---|
DiscordAdapter | token: Zeroizing<String> |
EmailAdapter | password: Zeroizing<String> |
BlueskyAdapter | app_password: Zeroizing<String> |
DingTalkAdapter | access_token: Zeroizing<String>、secret: Zeroizing<String>、client_id: Zeroizing<String>、client_secret: Zeroizing<String> |
FeishuAdapter | app_secret: Zeroizing<String> |
FlockAdapter | bot_token: Zeroizing<String> |
GitterAdapter | token: Zeroizing<String> |
GotifyAdapter | app_token: Zeroizing<String>、client_token: Zeroizing<String> |
网络搜索 (librefang-runtime/src/web_search.rs):
fn resolve_api_key(env_var: &str) -> Option<Zeroizing<String>> {
std::env::var(env_var).ok().filter(|k| !k.is_empty()).map(Zeroizing::new)
}
嵌入 (librefang-runtime/src/embedding.rs):
| 结构体 | 字段 |
|---|---|
EmbeddingClient | api_key: Zeroizing<String> |
8.3 为什么重要
如果不进行零化,密钥在使用后会一直留在内存中,直到操作系统回收该页面。
具有核心转储、交换文件或内存取证工具访问权限的攻击者可以恢复 API 密钥。
Zeroizing<String> 确保密钥在不再需要时立即被覆盖。
9. OFP 双向认证
源码: librefang-wire/src/peer.rs
LibreFang 线路协议(OFP)在 TCP 连接上使用基于 HMAC-SHA256 的 nonce 双向认证。
9.1 预共享密钥要求
OFP 在没有 shared_secret 的情况下拒绝启动:
if config.shared_secret.is_empty() {
return Err(WireError::HandshakeFailed(
"OFP requires shared_secret. Set [network] shared_secret in config.toml".into(),
));
}
9.2 HMAC 函数
type HmacSha256 = Hmac<Sha256>;
fn hmac_sign(secret: &str, data: &[u8]) -> String {
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.expect("HMAC accepts any key size");
mac.update(data);
hex::encode(mac.finalize().into_bytes())
}
fn hmac_verify(secret: &str, data: &[u8], signature: &str) -> bool {
let expected = hmac_sign(secret, data);
subtle::ConstantTimeEq::ct_eq(expected.as_bytes(), signature.as_bytes()).into()
}
常数时间比较(subtle::ConstantTimeEq)防止时序侧信道攻击。
9.3 握手协议
发起方(客户端):
- 生成随机 UUID nonce。
- 计算
auth_data = nonce + node_id。 - 计算
auth_hmac = hmac_sign(shared_secret, auth_data)。 - 发送
Handshake { node_id, node_name, protocol_version, agents, nonce, auth_hmac }。
响应方(服务端):
- 接收
Handshake消息。 - 验证传入的 HMAC:
hmac_verify(shared_secret, nonce + node_id, auth_hmac)。 - 如果验证失败,返回错误码 403。
- 为确认消息生成新的 UUID nonce。
- 计算
ack_auth_data = ack_nonce + self.node_id。 - 计算
ack_hmac = hmac_sign(shared_secret, ack_auth_data)。 - 发送
HandshakeAck { node_id, node_name, protocol_version, agents, nonce: ack_nonce, auth_hmac: ack_hmac }。
发起方(验证):
- 接收
HandshakeAck。 - 验证:
hmac_verify(shared_secret, ack_nonce + node_id, ack_hmac)。 - 如果验证失败,返回
WireError::HandshakeFailed。
9.4 安全属性
| 属性 | 实现方式 |
|---|---|
| 双向认证 | 双方都证明拥有共享密钥 |
| 重放保护 | 每次握手使用随机 UUID nonce |
| 时序攻击抵抗 | HMAC 比较使用 subtle::ConstantTimeEq |
| 强制密钥 | 空 shared_secret 时 OFP 拒绝启动 |
| 消息大小限制 | MAX_MESSAGE_SIZE = 16 MB 防止内存 DoS |
| 协议版本检查 | PROTOCOL_VERSION 不匹配时返回 WireError::VersionMismatch |
9.5 链路保密性
OFP 在线路上传输的帧是明文的。上述 HMAC 帧机制证明对端身份、防止
篡改与重放,但不加密有效载荷。保密性由部署层提供,不在
librefang-wire 内部重新实现。完整部署建议(WireGuard / Tailscale /
SSH 隧道 / 服务网格 mTLS)、HMAC 已覆盖的威胁、以及重新评估的条件,
见 OFP 链路加密 页面。
10. 安全响应头
源码: librefang-api/src/middleware.rs
security_headers 中间件应用于所有 API 响应:
pub async fn security_headers(request: Request<Body>, next: Next) -> Response<Body> {
let mut response = next.run(request).await;
let headers = response.headers_mut();
headers.insert("x-content-type-options", "nosniff".parse().unwrap());
headers.insert("x-frame-options", "DENY".parse().unwrap());
headers.insert("x-xss-protection", "1; mode=block".parse().unwrap());
headers.insert("content-security-policy", /* CSP policy */);
headers.insert("referrer-policy", "strict-origin-when-cross-origin".parse().unwrap());
headers.insert("cache-control", "no-store, no-cache, must-revalidate".parse().unwrap());
response
}
| 响应头 | 值 | 防护目标 |
|---|---|---|
X-Content-Type-Options | nosniff | MIME 类型嗅探攻击 |
X-Frame-Options | DENY | 通过 iframe 进行的点击劫持 |
X-XSS-Protection | 1; mode=block | 反射型 XSS(旧版浏览器) |
Content-Security-Policy | 见下文 | XSS、代码注入、数据外泄 |
Referrer-Policy | strict-origin-when-cross-origin | Referrer 泄露 |
Cache-Control | no-store, no-cache, must-revalidate | 敏感数据缓存 |
10.1 CSP 详细说明
| 指令 | 值 | 用途 |
|---|---|---|
default-src | 'self' | 默认拒绝所有外部资源 |
script-src | 'self' 'unsafe-inline' 'unsafe-eval' cdn.jsdelivr.net | 允许来自自身和 CDN 的脚本 |
style-src | 'self' 'unsafe-inline' cdn.jsdelivr.net fonts.googleapis.com | 允许来自自身、CDN、Google Fonts 的样式 |
img-src | 'self' data: | 允许来自自身和 data URI 的图片 |
connect-src | 'self' ws: wss: | 允许 WebSocket 连接 |
font-src | 'self' cdn.jsdelivr.net fonts.gstatic.com | 允许来自 CDN 的字体 |
object-src | 'none' | 阻止所有插件(Flash、Java 等) |
base-uri | 'self' | 防止 base 标签劫持 |
form-action | 'self' | 限制表单提交目标 |
11. GCRA 速率限制器
源码: librefang-api/src/rate_limiter.rs
LibreFang 通过 governor crate 使用通用信元速率算法(GCRA)实现
成本感知的 API 速率限制。
11.1 算法
GCRA 是漏桶算法的一种变体,为每个键追踪一个"虚拟调度时间" (TAT -- Theoretical Arrival Time)。每个请求消耗的令牌数与其成本 成正比。桶以恒定速率补充。
预算: 每个 IP 地址每分钟 500 个令牌。
pub fn create_rate_limiter() -> Arc<KeyedRateLimiter> {
Arc::new(RateLimiter::keyed(Quota::per_minute(NonZeroU32::new(500).unwrap())))
}
11.2 操作成本
每个 API 操作都有可配置的令牌成本:
pub fn operation_cost(method: &str, path: &str) -> NonZeroU32 {
match (method, path) {
(_, "/api/health") => 1,
("GET", "/api/status") => 1,
("GET", "/api/version") => 1,
("GET", "/api/tools") => 1,
("GET", "/api/agents") => 2,
("GET", "/api/skills") => 2,
("GET", "/api/peers") => 2,
("GET", "/api/config") => 2,
("GET", "/api/usage") => 3,
("GET", p) if p.starts_with("/api/audit") => 5,
("GET", p) if p.starts_with("/api/marketplace")=> 10,
("POST", "/api/agents") => 50,
("POST", p) if p.contains("/message") => 30,
("POST", p) if p.contains("/run") => 100,
("POST", "/api/skills/install") => 50,
("POST", "/api/skills/uninstall") => 10,
("POST", "/api/migrate") => 100,
("PUT", p) if p.contains("/update") => 10,
_ => 5,
}
}
成本分层是有意设计的:只读的健康检查消耗 1 个令牌,而昂贵的工作流运行 消耗 100 个令牌,这意味着客户端每分钟可以执行 500 次健康检查,但只能 执行 5 次工作流运行。
11.3 中间件
pub async fn gcra_rate_limit(
State(limiter): State<Arc<KeyedRateLimiter>>,
request: Request<Body>,
next: Next,
) -> Response<Body> {
let ip = /* extract from ConnectInfo, default 127.0.0.1 */;
let cost = operation_cost(&method, &path);
if limiter.check_key_n(&ip, cost).is_err() {
tracing::warn!(ip, cost, path, "GCRA rate limit exceeded");
return Response::builder()
.status(StatusCode::TOO_MANY_REQUESTS)
.header("retry-after", "60")
.body(/* JSON error */)
.unwrap_or_default();
}
next.run(request).await
}
11.4 速率限制器类型
pub type KeyedRateLimiter = RateLimiter<IpAddr, DashMapStateStore<IpAddr>, DefaultClock>;
DashMapStateStore 提供并发的每 IP 状态管理,并自动清理过期条目。
12. 路径遍历防护
源码: librefang-runtime/src/host_functions.rs
两个函数提供纵深防御以抵御目录遍历攻击。
12.1 safe_resolve_path(用于读取)
用于 fs_read 和 fs_list 操作,要求目标文件必须存在:
fn safe_resolve_path(path: &str) -> Result<std::path::PathBuf, serde_json::Value> {
let p = Path::new(path);
// Phase 1: Reject any path with ".." components
for component in p.components() {
if matches!(component, Component::ParentDir) {
return Err(json!({"error": "Path traversal denied: '..' components forbidden"}));
}
}
// Phase 2: Canonicalize to resolve symlinks and normalize
std::fs::canonicalize(p)
.map_err(|e| json!({"error": format!("Cannot resolve path: {e}")}))
}
12.2 safe_resolve_parent(用于写入)
用于 fs_write 操作,目标文件可能尚不存在:
fn safe_resolve_parent(path: &str) -> Result<std::path::PathBuf, serde_json::Value> {
let p = Path::new(path);
// Phase 1: Reject ".." in any component
for component in p.components() {
if matches!(component, Component::ParentDir) {
return Err(json!({"error": "Path traversal denied: '..' components forbidden"}));
}
}
// Phase 2: Canonicalize the parent directory
let parent = p.parent().filter(|par| !par.as_os_str().is_empty())
.ok_or_else(|| json!({"error": "Invalid path: no parent directory"}))?;
let canonical_parent = std::fs::canonicalize(parent)?;
// Phase 3: Belt-and-suspenders check on filename
let file_name = p.file_name()
.ok_or_else(|| json!({"error": "Invalid path: no file name"}))?;
if file_name.to_string_lossy().contains("..") {
return Err(json!({"error": "Path traversal denied in file name"}));
}
Ok(canonical_parent.join(file_name))
}
12.3 执行顺序
- 能力检查首先使用原始路径执行。
- 路径遍历检查其次执行。
- 操作仅在两者都通过后才执行。
这种顺序确保即使能力被错误配置为宽泛的模式(如 "*"),路径遍历仍然
会被阻止。
13. 子进程沙箱
源码: librefang-runtime/src/subprocess_sandbox.rs
当运行时生成子进程(例如用于 shell 工具或技能执行)时,必须清除继承的 环境变量以防止意外的密钥泄露。
13.1 环境清除
pub fn sandbox_command(cmd: &mut tokio::process::Command, allowed_env_vars: &[String]) {
cmd.env_clear(); // Remove ALL inherited env vars
// Re-add platform-independent safe vars
for var in SAFE_ENV_VARS {
if let Ok(val) = std::env::var(var) {
cmd.env(var, val);
}
}
// Re-add Windows-specific safe vars (on Windows)
#[cfg(windows)]
for var in SAFE_ENV_VARS_WINDOWS { /* ... */ }
// Re-add caller-specified allowed vars
for var in allowed_env_vars { /* ... */ }
}
13.2 安全环境变量
所有平台:
pub const SAFE_ENV_VARS: &[&str] = &[
"PATH", "HOME", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "TERM",
];
仅限 Windows:
pub const SAFE_ENV_VARS_WINDOWS: &[&str] = &[
"USERPROFILE", "SYSTEMROOT", "APPDATA", "LOCALAPPDATA",
"COMSPEC", "WINDIR", "PATHEXT",
];
不在这些列表中且不在 allowed_env_vars 中的变量永远不会传递给
子进程。这意味着 OPENAI_API_KEY、GEMINI_API_KEY、数据库凭据以及
所有其他密钥都会被清除。
13.3 可执行文件路径验证
pub fn validate_executable_path(path: &str) -> Result<(), String> {
let p = Path::new(path);
for component in p.components() {
if let std::path::Component::ParentDir = component {
return Err(format!(
"executable path '{}' contains '..' component which is not allowed",
path
));
}
}
Ok(())
}
这可以防止 Agent 通过构造的路径(如 ../../bin/dangerous)逃离其
工作目录。
13.4 Shell 注入防护
host_shell_exec 函数使用 Command::new(command).args(&args),
这不会调用 shell。每个参数直接传递给进程,防止通过 ;、|、
&& 等元字符进行 shell 注入。
14. 提示注入扫描器
源码: librefang-skills/src/verify.rs
SkillVerifier 提供两个扫描函数:security_scan() 用于技能清单,
scan_prompt_content() 用于技能提示文本(SKILL.md 正文)。
14.1 清单安全扫描
SkillVerifier::security_scan(manifest) 检查技能声明的需求:
| 检查项 | 严重级别 | 触发条件 |
|---|---|---|
| Node.js 运行时 | Warning | runtime_type == SkillRuntime::Node |
| Shell 执行能力 | Critical | 能力包含 shellexec 或 shell_exec |
| 不受限的网络访问 | Warning | 能力包含 netconnect(*) |
| Shell 工具 | Critical | 工具为 shell_exec 或 bash |
| 文件系统写入工具 | Warning | 工具为 file_write 或 file_delete |
| 工具过多 | Info | 需要超过 10 个工具 |
14.2 提示注入扫描
SkillVerifier::scan_prompt_content(content) 检测技能提示文本中的
常见攻击模式:
Critical -- 提示覆盖尝试:
"ignore previous instructions", "ignore all previous",
"disregard previous", "forget your instructions",
"you are now", "new instructions:", "system prompt override",
"ignore the above", "do not follow", "override system"
Warning -- 数据外泄模式:
"send to http", "send to https", "post to http", "post to https",
"exfiltrate", "forward all", "send all data",
"base64 encode and send", "upload to"
Warning -- Shell 命令引用:
"rm -rf", "chmod ", "sudo "
Info -- 内容过长:
超过 50,000 字节的内容会触发信息级别的警告,提示可能影响 LLM 性能。
14.3 SHA256 校验和验证
pub fn verify_checksum(data: &[u8], expected_sha256: &str) -> bool {
let actual = Self::sha256_hex(data);
actual == expected_sha256.to_lowercase()
}
从 ClawHub 安装的技能会根据已知的 SHA256 哈希验证其内容,以检测 下载过程中的篡改。
14.4 警告结构
pub struct SkillWarning {
pub severity: WarningSeverity, // Info, Warning, Critical
pub message: String,
}
15. 循环守卫
源码: librefang-runtime/src/loop_guard.rs
LoopGuard 追踪单次 Agent 循环执行中的工具调用,以检测 Agent 是否
陷入重复调用同一工具的状态。
15.1 配置
pub struct LoopGuardConfig {
pub warn_threshold: u32, // Default: 3
pub block_threshold: u32, // Default: 5
pub global_circuit_breaker: u32, // Default: 30
}
15.2 检测算法
- 对每次工具调用,计算
tool_name + "|" + serialized_params的 SHA-256 哈希。 - 在
HashMap<String, u32>中递增该哈希的计数。 - 递增
total_calls。 - 返回分级裁决:
pub fn check(&mut self, tool_name: &str, params: &serde_json::Value) -> LoopGuardVerdict {
self.total_calls += 1;
// Global circuit breaker
if self.total_calls > self.config.global_circuit_breaker {
return LoopGuardVerdict::CircuitBreak(/* ... */);
}
let hash = Self::compute_hash(tool_name, params);
let count = self.call_counts.entry(hash).or_insert(0);
*count += 1;
if *count >= self.config.block_threshold {
LoopGuardVerdict::Block(/* ... */)
} else if *count >= self.config.warn_threshold {
LoopGuardVerdict::Warn(/* ... */)
} else {
LoopGuardVerdict::Allow
}
}
15.3 裁决类型
| 裁决 | 含义 | 动作 |
|---|---|---|
Allow | 正常操作 | 运行工具 |
Warn(msg) | 同一调用重复 >= 3 次 | 运行,并在结果中附加警告 |
Block(msg) | 同一调用重复 >= 5 次 | 跳过执行,返回错误 |
CircuitBreak(msg) | 总调用次数 > 30 | 终止整个 Agent 循环 |
15.4 哈希计算
fn compute_hash(tool_name: &str, params: &serde_json::Value) -> String {
let mut hasher = Sha256::new();
hasher.update(tool_name.as_bytes());
hasher.update(b"|");
let params_str = serde_json::to_string(params).unwrap_or_default();
hasher.update(params_str.as_bytes());
hex::encode(hasher.finalize())
}
注意:serde_json::to_string 生成确定性输出(对象键已排序),确保
语义相同的参数产生相同的哈希值。
15.5 关键特性
使用不同参数的调用会被分别追踪。一个 Agent 使用 10 个不同查询调用
web_search 不会触发守卫,但使用 web_search({"query": "test"}) 调用
5 次将被阻止。
16. 会话修复
源码: librefang-runtime/src/session_repair.rs
在将消息历史发送给 LLM 之前,本模块会验证并修复常见的结构性问题, 避免 API 错误。
16.1 三阶段修复
pub fn validate_and_repair(messages: &[Message]) -> Vec<Message>
阶段 1 -- 收集 ToolUse ID:
扫描所有消息中的 ContentBlock::ToolUse { id, .. } 块,将其 ID 收集
到 HashSet<String> 中。
阶段 2 -- 过滤孤立条目和空消息:
- 孤立的 ToolResult:
ContentBlock::ToolResult { tool_use_id, .. }块中tool_use_id不在 ToolUse ID 集合中的会被丢弃。 - 空消息: 文本为空或没有内容块的消息会被丢弃。
阶段 3 -- 合并连续的同角色消息:
Anthropic API 要求严格的角色交替(user、assistant、user、 assistant...)。如果两条连续消息具有相同的角色,它们会被合并为一条 包含组合内容块的消息。
16.2 每种修复的必要性
| 问题 | 原因 | 不修复的后果 |
|---|---|---|
| 孤立的 ToolResult | 压缩或截断删除了 ToolUse | API 错误:"tool_use_id not found" |
| 空消息 | 取消的生成、空的用户提交 | API 错误:内容为空 |
| 连续的同角色消息 | 手动编辑历史、会话修复本身 | API 错误:角色交替违规 |
16.3 内容合并
合并连续的同角色消息时,两条消息都被转换为块格式并拼接:
fn merge_content(dst: &mut MessageContent, src: MessageContent) {
let dst_blocks = content_to_blocks(std::mem::replace(dst, MessageContent::Text(String::new())));
let src_blocks = content_to_blocks(src);
let mut combined = dst_blocks;
combined.extend(src_blocks);
*dst = MessageContent::Blocks(combined);
}
17. 健康端点信息脱敏
源码: librefang-api/src/routes.rs
LibreFang 提供两个信息级别不同的健康端点。
17.1 公开端点:GET /api/health
无需认证。 仅返回存活信息:
{
"status": "ok",
"version": "0.1.0"
}
该端点不会暴露 Agent 数量、数据库详情、配置警告、运行时间或任何内部 系统信息。适用于负载均衡器健康检查。
17.2 详细端点:GET /api/health/detail
需要认证。 返回完整诊断信息:
{
"status": "ok",
"version": "0.1.0",
"uptime_seconds": 3600,
"panic_count": 0,
"restart_count": 2,
"agent_count": 15,
"database": "connected",
"config_warnings": []
}
17.3 本地回环回退
当未配置 API 密钥时,auth 中间件将所有非健康端点限制为仅允许回环
地址访问:
if api_key.is_empty() {
let is_loopback = request.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|ci| ci.0.ip().is_loopback())
.unwrap_or(false);
if !is_loopback {
return Response::builder()
.status(StatusCode::FORBIDDEN)
.body(/* "No API key configured. Remote access denied." */)
...;
}
}
18. Dashboard 认证
LibreFang 支持为 Web Dashboard 配置可选的用户名/密码认证。
18.1 配置
# config.toml
dashboard_user = "admin"
dashboard_pass = "vault:dashboard_password"
当两个字段都设置后,Dashboard 会显示登录页面。如果未配置凭据,
Dashboard 无需登录即可访问(或者在设置了 api_key 的情况下受其保护)。
18.2 凭据来源(优先级顺序)
| 优先级 | 来源 | 示例 |
|---|---|---|
| 1 | 环境变量 | LIBREFANG_DASHBOARD_PASS=secret |
| 2 | 加密 Vault | dashboard_pass = "vault:dashboard_password" |
| 3 | 配置文件明文 | dashboard_pass = "my-password" |
18.3 Vault 存储(推荐)
# Store password in encrypted vault
librefang vault set dashboard_password
# Reference in config.toml
dashboard_pass = "vault:dashboard_password"
Vault 使用 AES-256-GCM 加密,主密钥存储在操作系统密钥链中(macOS
Keychain / Windows Credential Manager / Linux Secret Service),或
通过 LIBREFANG_VAULT_KEY 环境变量指定。
18.4 安全属性
- 常数时间比较:使用
subtle::ConstantTimeEq比较凭据,防止时序攻击。 - HMAC 会话令牌:登录成功后,使用 HMAC-SHA256 从凭据派生确定性令牌,作为后续 API 请求的 Bearer token。
- 无服务端会话状态:令牌是确定性的,服务端无需存储会话状态。
- 公开端点:
/api/auth/dashboard-login和/api/auth/dashboard-check无需认证即可访问。
19. TOTP 二次验证
19.1 威胁模型
审批门将确认请求发送到通知渠道(Telegram、Slack 等)。如果发起渠道被攻破,攻击者可以同时发起和批准危险操作——审批门无法提供任何保护。
TOTP(基于时间的一次性密码,RFC 6238)添加了一个带外第二因子:TOTP 密钥存储在操作员设备上的验证器应用中,完全独立于消息渠道。仅攻破 Telegram 账号是不够的——攻击者还需要验证器应用。
19.2 架构
┌──────────────────┐
Agent requests │ Authenticator │
shell_exec │ App (phone) │
│ │ │
▼ │ TOTP Secret ──┐ │
┌─────────────┐ └────────────────│─┘
│ ApprovalMgr │◄── resolve(Approved, totp_verified)│
│ │ ▲ │
│ TOTP gate │─────────┤ │
│ Grace cache │ ┌────────────┐ │
└─────────────┘ │ API/Channel│ verify_totp_code()
│ Layer │◄─── 6-digit code─┘
└────────────┘
▲
User types code
关键组件:
| 组件 | 位置 | 作用 |
|---|---|---|
SecondFactor 枚举 | librefang-types/approval.rs | 配置项:none 或 totp |
ApprovalManager::resolve() | librefang-kernel/approval.rs | TOTP 门控——未通过验证的 Approved 一律拒绝 |
ApprovalManager::verify_totp_code() | librefang-kernel/approval.rs | RFC 6238 验证(SHA-1,6 位,30 秒步长,±1 时间窗) |
ApprovalManager::generate_totp_secret() | librefang-kernel/approval.rs | 注册时生成密钥 |
| 宽限期缓存 | ApprovalManager::totp_grace | HashMap<user_id, Instant>——窗口内跳过重复验证 |
| 保管库存储 | librefang-extensions/vault.rs | TOTP 密钥以 totp_secret 键存于 AES-256-GCM 保管库 |
19.3 注册流程
1. POST /api/approvals/totp/setup
→ 生成随机 TOTP 密钥
→ 存储在加密保管库(vault.enc)中
→ 返回 base32 密钥 + otpauth:// URI
2. 用户将密钥添加到验证器应用(Google Authenticator、1Password 等)
3. POST /api/approvals/totp/confirm { "code": "123456" }
→ 验证用户输入的验证码
→ 在保管库中设置 "totp_confirmed" = "true"
4. 在配置中设置 second_factor = "totp"(支持热重载)
→ ApprovalManager.requires_totp() 返回 true
→ 后续所有审批都要求 TOTP 验证
也可以在 Dashboard 的 设置 > 安全 中完成注册。
19.4 启用后的审批流程
1. Agent 调用 shell_exec → 需要审批
2. 内核发送交互通知到渠道
→ 消息包含:"TOTP required. Reply: /approve <id> <6位验证码>"
→ 隐藏批准按钮(验证码必须手动输入)
3. 用户回复:/approve abc123 654321
4. 渠道桥调用 ApprovalManager::verify_totp_code(secret, "654321")
5. 验证码有效 → resolve(Approved, totp_verified=true, user_id="channel_user")
6. 为该 user_id 记录宽限期
7. Agent 解除阻塞 → 工具执行
19.5 宽限期
为避免每次审批都要输入验证码,可配置宽限期(totp_grace_period_secs,默认 300 秒)。
- 按
user_id追踪,非按来源(如 "api" 或 "channel") - 设为
0禁用宽限——每次审批都需要验证码 - 最大值:3600 秒(1 小时)
- 宽限缓存仅存在于内存中——重启守护进程会重置
19.6 安全属性
| 属性 | 保证 |
|---|---|
| 密钥存储 | AES-256-GCM 加密保管库 + Argon2id 密钥派生 |
| 算法 | TOTP(RFC 6238),SHA-1,6 位,30 秒步长 |
| 时钟偏差容忍 | ±1 窗口(±30 秒) |
| 验证层 | API/渠道层在调用 resolve() 之前完成验证 |
| 内核强制 | 启用 TOTP 时 resolve() 拒绝任何 totp_verified=true 之外的 Approved |
| 拒绝放行 | 拒绝/驳回决定不需要 TOTP(拒绝始终安全) |
| 批量限制 | TOTP 启用时批量批准被阻止 |
20. 安全配置
20.1 config.toml 参考
# API Authentication
api_key = "your-secret-api-key" # Empty = localhost-only mode
# OFP Wire Protocol
[network]
shared_secret = "your-pre-shared-key" # Required for OFP
# WASM Sandbox
[sandbox]
fuel_limit = 1000000 # CPU instruction budget per execution
timeout_secs = 30 # Wall-clock timeout per execution
max_memory_bytes = 16777216 # 16 MB max WASM memory
# Rate Limiting
# 500 tokens/minute/IP (not currently configurable via config.toml)
# Web Search SSRF Protection
[web]
# SSRF protection is always on and cannot be disabled
18.2 密钥相关环境变量
| 变量 | 用途 |
|---|---|
OPENAI_API_KEY | OpenAI 兼容驱动 |
ANTHROPIC_API_KEY | Anthropic 驱动 |
GEMINI_API_KEY 或 GOOGLE_API_KEY | Gemini 驱动 |
DEEPSEEK_API_KEY | DeepSeek 提供商 |
GROQ_API_KEY | Groq 提供商 |
BRAVE_API_KEY | Brave 网络搜索 |
TAVILY_API_KEY | Tavily 网络搜索 |
PERPLEXITY_API_KEY | Perplexity 网络搜索 |
所有环境变量中的 API 密钥在加载到驱动结构体时都会被包装为
Zeroizing<String>。
18.3 能力声明(Agent 清单)
能力在 Agent 的 TOML 清单中声明:
[agent]
name = "my-agent"
[[capabilities]]
type = "FileRead"
value = "/data/*"
[[capabilities]]
type = "NetConnect"
value = "*.openai.com:443"
[[capabilities]]
type = "ToolInvoke"
value = "web_search"
[[capabilities]]
type = "LlmMaxTokens"
value = 4096
18.4 循环守卫调优
LoopGuardConfig 的默认值:
| 参数 | 默认值 | 描述 |
|---|---|---|
warn_threshold | 3 | 相同调用多少次后发出警告 |
block_threshold | 5 | 相同调用多少次后阻止 |
global_circuit_breaker | 30 | 总调用多少次后熔断 |
18.5 子进程沙箱白名单
要将特定环境变量传递给子进程:
sandbox_command(&mut cmd, &["MY_CUSTOM_VAR".to_string()]);
只有明确列在 allowed_env_vars 中的变量(加上安全默认值)才会被
子进程继承。
20. 安全相关依赖
| Crate | 用途 |
|---|---|
sha2 | SHA-256 哈希(审计追踪、循环守卫、SSRF、校验和) |
hmac | HMAC-SHA256 用于 OFP 认证 |
hex | 哈希和签名的十六进制编解码 |
subtle | 常数时间比较(ConstantTimeEq)用于 HMAC 验证 |
ed25519-dalek | Ed25519 签名/验证用于清单签名 |
rand | 密码学安全随机数生成器用于密钥生成(OsRng) |
zeroize | Zeroizing<T> 包装器用于自动密钥内存擦除 |
governor | GCRA 速率限制算法 |
wasmtime | WASM 沙箱,带燃料 + 纪元计量 |
uuid | OFP 握手的 nonce 生成 |
chrono | 审计条目的 ISO-8601 时间戳 |
reqwest | HTTP 客户端(在受 SSRF 保护的 host_net_fetch 中使用) |
19.1 为什么选择这些 Crate
- sha2/hmac: 属于 RustCrypto 项目,经过审计,在生产环境的 Rust 项目中广泛使用。
- ed25519-dalek: Rust 生态中事实上的 Ed25519 标准库,经过广泛审计。
- subtle: 提供常数时间操作以防止时序侧信道攻击。
- zeroize: RustCrypto 官方的密钥零化方案,与
Droptrait 集成。 - governor: 久经考验的 GCRA 实现,使用
DashMap支持的并发状态。
威胁模型总结
| 威胁 | 缓解措施 |
|---|---|
| Agent 请求未授权的文件访问 | 基于能力的安全模型(第 2 节) |
| Agent 生成具有提升权限的子 Agent | 能力继承验证(第 2.4 节) |
| WASM 技能运行无限循环 | 双重计量:燃料 + 纪元(第 3 节) |
| 攻击者篡改审计日志 | Merkle 哈希链(第 4 节) |
| 通过外部数据进行提示注入 | 污点追踪(第 5 节) |
| 通过 LLM 进行数据外泄 | 污点汇聚点阻止 Secret/PII 流向 net_fetch(第 5.3 节) |
| 被篡改的 Agent 清单 | Ed25519 签名(第 6 节) |
| SSRF 访问云元数据 | 私有 IP + 主机名阻止 + DNS 检查(第 7 节) |
| 从内存转储恢复 API 密钥 | Zeroizing<String>(第 8 节) |
| 未授权的对等节点连接 | HMAC-SHA256 双向认证(第 9 节) |
| API 上的 XSS / 点击劫持 | 安全响应头(第 10 节) |
| API 暴力破解 / 拒绝服务 | GCRA 速率限制器(第 11 节) |
通过 ../ 进行路径遍历 | safe_resolve_path / safe_resolve_parent(第 12 节) |
| 子进程泄露密钥 | env_clear() + 白名单(第 13 节) |
| 来自 ClawHub 的恶意技能 | 提示注入扫描器 + SHA256 校验和(第 14 节) |
| Agent 陷入工具调用循环 | LoopGuard 分级响应(第 15 节) |
| LLM 会话历史损坏 | 会话修复(第 16 节) |
| 健康端点信息泄露 | 脱敏的公开端点(第 17 节) |
| 针对 HMAC 验证的时序攻击 | subtle::ConstantTimeEq(第 9.2 节) |
| 通过元字符进行 Shell 注入 | Command::new(无 shell)+ env_clear(第 13.4 节) |
| DNS 重绑定绕过 SSRF | 检查解析后的 IP,而非主机名(第 7.3 节) |