网络与 API 安全
本页覆盖传输层、对等连接、HTTP 中间件和 Dashboard 边界上的安全保护。
包含的主题
- SSRF 防护
- OFP 双向认证
- 安全响应头
- GCRA 速率限制器
- 健康端点信息脱敏
- Dashboard 认证
SSRF 防护
源码: librefang-runtime/src/host_functions.rs
host_net_fetch 函数(用于网络请求的 WASM 宿主调用)包含全面的
服务端请求伪造防护。
协议验证
仅允许 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"}));
}
主机名黑名单
在 DNS 解析之前,以下主机名会被阻止:
localhostmetadata.google.internalmetadata.aws.internalinstance-data169.254.169.254(AWS/GCP 元数据端点)
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}"
)}));
}
}
}
私有 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
}
}
}
主机提取
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
OFP 双向认证
源码: librefang-wire/src/peer.rs
LibreFang 线路协议(OFP)在 TCP 连接上使用基于 HMAC-SHA256 的 nonce 双向认证。
预共享密钥要求
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(),
));
}
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)防止时序侧信道攻击。
握手协议
发起方(客户端):
- 生成随机 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。
安全属性
| 属性 | 实现方式 |
|---|---|
| 双向认证 | 双方都证明拥有共享密钥 |
| 重放保护 | 每次握手使用随机 UUID nonce |
| 时序攻击抵抗 | HMAC 比较使用 subtle::ConstantTimeEq |
| 强制密钥 | 空 shared_secret 时 OFP 拒绝启动 |
| 消息大小限制 | MAX_MESSAGE_SIZE = 16 MB 防止内存 DoS |
| 协议版本检查 | PROTOCOL_VERSION 不匹配时返回 WireError::VersionMismatch |
安全响应头
源码: 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 | 敏感数据缓存 |
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' | 限制表单提交目标 |
GCRA 速率限制器
源码: librefang-api/src/rate_limiter.rs
LibreFang 通过 governor crate 使用通用信元速率算法(GCRA)实现
成本感知的 API 速率限制。
算法
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())))
}
操作成本
每个 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 次工作流运行。
中间件
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
}
速率限制器类型
pub type KeyedRateLimiter = RateLimiter<IpAddr, DashMapStateStore<IpAddr>, DefaultClock>;
DashMapStateStore 提供并发的每 IP 状态管理,并自动清理过期条目。
健康端点信息脱敏
源码: librefang-api/src/routes.rs
LibreFang 提供两个信息级别不同的健康端点。
公开端点:GET /api/health
无需认证。 仅返回存活信息:
{
"status": "ok",
"version": "0.1.0"
}
该端点不会暴露 Agent 数量、数据库详情、配置警告、运行时间或任何内部 系统信息。适用于负载均衡器健康检查。
详细端点: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": []
}
本地回环回退
当未配置 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." */)
...;
}
}
Dashboard 认证
LibreFang 支持为 Web Dashboard 配置可选的用户名/密码认证。
配置
# config.toml
dashboard_user = "admin"
dashboard_pass = "vault:dashboard_password"
当两个字段都设置后,Dashboard 会显示登录页面。如果未配置凭据,
Dashboard 无需登录即可访问(或者在设置了 api_key 的情况下受其保护)。
凭据来源(优先级顺序)
| 优先级 | 来源 | 示例 |
|---|---|---|
| 1 | 环境变量 | LIBREFANG_DASHBOARD_PASS=secret |
| 2 | 加密 Vault | dashboard_pass = "vault:dashboard_password" |
| 3 | 配置文件明文 | dashboard_pass = "my-password" |
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 环境变量指定。
安全属性
- 常数时间比较:使用
subtle::ConstantTimeEq比较凭据,防止时序攻击。 - HMAC 会话令牌:登录成功后,使用 HMAC-SHA256 从凭据派生确定性令牌,作为后续 API 请求的 Bearer token。
- 无服务端会话状态:令牌是确定性的,服务端无需存储会话状态。
- 公开端点:
/api/auth/dashboard-login和/api/auth/dashboard-check无需认证即可访问。