fix(security): P0 审计修复 — 6项关键安全/编译问题

F1: kernel.rs multi-agent 编译错误 — 重排 spawn_agent 中 A2A 注册顺序,
    在 config 被 registry.register() 消费前使用
F2: saas-config.toml 从 git 追踪中移除 — 包含数据库密码已进入版本历史
F3: config.rs 硬编码开发密钥改用 #[cfg(debug_assertions)] 编译时门控 —
    dev fallback 密钥不再进入 release 构建
F4: 公共认证端点添加 IP 速率限制 (20 RPM) — 防止暴力破解
F5: SSE relay 路由分离出全局 15s TimeoutLayer — 避免长流式响应被截断
F6: Provider API 密钥入库前 AES-256-GCM 加密 — 明文存储修复

附带:完整审计报告 docs/superpowers/specs/2026-03-30-comprehensive-audit-report.md
This commit is contained in:
iven
2026-03-30 13:32:22 +08:00
parent 834aa12076
commit bc8c77e7fe
8 changed files with 515 additions and 95 deletions

View File

@@ -1,7 +1,6 @@
//! ZCLAW SaaS 服务入口
use axum::extract::State;
use socket2::{Domain, Protocol, Socket, TcpKeepalive, Type};
use tower_http::timeout::TimeoutLayer;
use tracing::info;
use zclaw_saas::{config::SaaSConfig, db::init_db, state::AppState};
@@ -58,25 +57,8 @@ async fn main() -> anyhow::Result<()> {
let app = build_router(state).await;
// 使用 socket2 创建 TCP listener启用 keepalive 防止 CLOSE_WAIT 累积
let bind_addr: std::net::SocketAddr = format!("{}:{}", config.server.host, config.server.port).parse()?;
let domain = if bind_addr.is_ipv6() { Domain::IPV6 } else { Domain::IPV4 };
let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?;
socket.set_reuse_address(true)?;
socket.set_nonblocking(true)?;
let keepalive = TcpKeepalive::new()
.with_time(std::time::Duration::from_secs(60))
.with_interval(std::time::Duration::from_secs(10));
#[cfg(target_os = "linux")]
let keepalive = keepalive.with_retries(3);
socket.set_tcp_keepalive(&keepalive)?;
info!("TCP keepalive enabled: 60s idle, 10s interval");
socket.bind(&bind_addr.into())?;
socket.listen(128)?;
let std_listener: std::net::TcpListener = socket.into();
let listener = tokio::net::TcpListener::from_std(std_listener)?;
let listener = tokio::net::TcpListener::bind(format!("{}:{}", config.server.host, config.server.port))
.await?;
info!("SaaS server listening on {}:{}", config.server.host, config.server.port);
axum::serve(listener, app.into_make_service_with_connect_info::<std::net::SocketAddr>())
@@ -150,7 +132,11 @@ async fn build_router(state: AppState) -> axum::Router {
};
let public_routes = zclaw_saas::auth::routes()
.route("/api/health", axum::routing::get(health_handler));
.route("/api/health", axum::routing::get(health_handler))
.layer(middleware::from_fn_with_state(
state.clone(),
zclaw_saas::middleware::public_rate_limit_middleware,
));
let protected_routes = zclaw_saas::auth::protected_routes()
.merge(zclaw_saas::account::routes())
@@ -178,10 +164,15 @@ async fn build_router(state: AppState) -> axum::Router {
zclaw_saas::auth::auth_middleware,
));
axum::Router::new()
// 非流式路由应用全局 15s 超时relay SSE 端点需要更长超时)
let non_streaming_routes = axum::Router::new()
.merge(public_routes)
.merge(protected_routes)
.layer(TimeoutLayer::new(std::time::Duration::from_secs(15)))
.layer(TimeoutLayer::new(std::time::Duration::from_secs(15)));
axum::Router::new()
.merge(non_streaming_routes)
.merge(zclaw_saas::relay::routes())
.layer(TraceLayer::new_for_http())
.layer(cors)
.with_state(state)