网络与 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 解析之前,以下主机名会被阻止:

  • localhost
  • metadata.google.internal
  • metadata.aws.internal
  • instance-data
  • 169.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 1918
  • 172.16.0.0/12 -- RFC 1918
  • 192.168.0.0/16 -- RFC 1918
  • 169.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)防止时序侧信道攻击。

握手协议

发起方(客户端):

  1. 生成随机 UUID nonce。
  2. 计算 auth_data = nonce + node_id
  3. 计算 auth_hmac = hmac_sign(shared_secret, auth_data)
  4. 发送 Handshake { node_id, node_name, protocol_version, agents, nonce, auth_hmac }

响应方(服务端):

  1. 接收 Handshake 消息。
  2. 验证传入的 HMAC:hmac_verify(shared_secret, nonce + node_id, auth_hmac)
  3. 如果验证失败,返回错误码 403。
  4. 为确认消息生成新的 UUID nonce。
  5. 计算 ack_auth_data = ack_nonce + self.node_id
  6. 计算 ack_hmac = hmac_sign(shared_secret, ack_auth_data)
  7. 发送 HandshakeAck { node_id, node_name, protocol_version, agents, nonce: ack_nonce, auth_hmac: ack_hmac }

发起方(验证):

  1. 接收 HandshakeAck
  2. 验证:hmac_verify(shared_secret, ack_nonce + node_id, ack_hmac)
  3. 如果验证失败,返回 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-OptionsnosniffMIME 类型嗅探攻击
X-Frame-OptionsDENY通过 iframe 进行的点击劫持
X-XSS-Protection1; mode=block反射型 XSS(旧版浏览器)
Content-Security-Policy见下文XSS、代码注入、数据外泄
Referrer-Policystrict-origin-when-cross-originReferrer 泄露
Cache-Controlno-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加密 Vaultdashboard_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 无需认证即可访问。