fix(security): require runtime API auth by default

This commit is contained in:
Hunter Bown
2026-05-08 14:34:45 -05:00
parent 6248dc0508
commit 9ee3b51582
2 changed files with 114 additions and 10 deletions
+4
View File
@@ -438,6 +438,9 @@ struct ServeArgs {
/// `DEEPSEEK_RUNTIME_TOKEN` when omitted.
#[arg(long = "auth-token", value_name = "TOKEN")]
auth_token: Option<String>,
/// Disable runtime API auth when no token is configured. Only use on a trusted loopback.
#[arg(long = "insecure")]
insecure_no_auth: bool,
}
#[derive(Subcommand, Debug, Clone)]
@@ -749,6 +752,7 @@ async fn main() -> Result<()> {
workers: args.workers.clamp(1, 8),
cors_origins,
auth_token: args.auth_token,
insecure_no_auth: args.insecure_no_auth,
},
)
.await
+110 -10
View File
@@ -76,6 +76,8 @@ pub struct RuntimeApiOptions {
/// Optional bearer token required for `/v1/*` routes. If omitted here,
/// `run_http_server` also checks `DEEPSEEK_RUNTIME_TOKEN`.
pub auth_token: Option<String>,
/// Allow `/v1/*` routes without auth when no token is configured.
pub insecure_no_auth: bool,
}
impl Default for RuntimeApiOptions {
@@ -86,10 +88,55 @@ impl Default for RuntimeApiOptions {
workers: 2,
cors_origins: Vec::new(),
auth_token: None,
insecure_no_auth: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ResolvedRuntimeAuth {
token: Option<String>,
generated: bool,
}
fn resolve_runtime_auth(
cli_token: Option<String>,
env_token: Option<String>,
insecure_no_auth: bool,
) -> ResolvedRuntimeAuth {
if let Some(token) = first_nonblank_token(cli_token).or_else(|| first_nonblank_token(env_token))
{
return ResolvedRuntimeAuth {
token: Some(token),
generated: false,
};
}
if insecure_no_auth {
return ResolvedRuntimeAuth {
token: None,
generated: false,
};
}
ResolvedRuntimeAuth {
token: Some(generate_runtime_token()),
generated: true,
}
}
fn first_nonblank_token(token: Option<String>) -> Option<String> {
token
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
}
fn generate_runtime_token() -> String {
format!(
"dst_{}{}",
uuid::Uuid::new_v4().simple(),
uuid::Uuid::new_v4().simple()
)
}
#[derive(Debug, Deserialize)]
struct StreamTurnRequest {
prompt: String,
@@ -348,11 +395,12 @@ pub async fn run_http_server(
.map(|h| h.join(".deepseek").join("sessions"))
.unwrap_or_else(|| PathBuf::from(".deepseek").join("sessions"))
});
let runtime_token = options
.auth_token
.clone()
.or_else(|| std::env::var("DEEPSEEK_RUNTIME_TOKEN").ok())
.filter(|token| !token.trim().is_empty());
let resolved_auth = resolve_runtime_auth(
options.auth_token.clone(),
std::env::var("DEEPSEEK_RUNTIME_TOKEN").ok(),
options.insecure_no_auth,
);
let runtime_token = resolved_auth.token.clone();
let auth_enabled = runtime_token.is_some();
let skill_state = SkillStateStore::load_default().unwrap_or_else(|err| {
tracing::warn!(
@@ -370,7 +418,7 @@ pub async fn run_http_server(
sessions_dir,
mcp_config_path: config.mcp_config_path(),
automations,
runtime_token,
runtime_token: runtime_token.clone(),
skill_state: Arc::new(Mutex::new(skill_state)),
auth_required: auth_enabled,
bind_host: options.host.clone(),
@@ -386,6 +434,17 @@ pub async fn run_http_server(
.with_context(|| format!("Failed to bind {addr}"))?;
println!("Runtime API listening on http://{addr}");
if resolved_auth.generated {
if let Some(token) = runtime_token.as_deref() {
println!("Runtime API auth: generated bearer token for this process.");
println!(" Authorization: Bearer {token}");
println!(" Set DEEPSEEK_RUNTIME_TOKEN or pass --auth-token for a stable token.");
}
} else if auth_enabled {
println!("Runtime API auth: bearer token required for /v1/* routes.");
} else {
println!("Runtime API auth: disabled by explicit insecure mode.");
}
let is_loopback = options.host == "127.0.0.1" || options.host == "::1";
if is_loopback {
println!("Security: this server is local-first. Do not expose it to untrusted networks.");
@@ -396,7 +455,7 @@ pub async fn run_http_server(
);
if !auth_enabled {
println!(
" WARNING: --auth-token (or DEEPSEEK_RUNTIME_TOKEN) is unset. Anyone on the network can call /v1/* without authentication."
" WARNING: auth is disabled. Anyone on the network can call /v1/* without authentication."
);
}
println!(
@@ -406,9 +465,6 @@ pub async fn run_http_server(
auth = auth_enabled,
);
}
if auth_enabled {
println!("Runtime API auth: bearer token required for /v1/* routes.");
}
let serve_result = axum::serve(listener, app)
.await
.map_err(|e| anyhow!("Runtime API server error: {e}"));
@@ -1838,6 +1894,50 @@ mod tests {
}
}
#[test]
fn runtime_auth_generates_token_by_default() {
let auth = resolve_runtime_auth(None, None, false);
assert!(auth.generated);
let token = auth.token.expect("generated token");
assert!(token.starts_with("dst_"));
assert!(token.len() > 32);
}
#[test]
fn runtime_auth_requires_explicit_insecure_for_no_token() {
let auth = resolve_runtime_auth(None, None, true);
assert_eq!(
auth,
ResolvedRuntimeAuth {
token: None,
generated: false,
}
);
}
#[test]
fn runtime_auth_prefers_cli_token_over_env_token() {
let auth = resolve_runtime_auth(
Some(" cli-token ".to_string()),
Some("env-token".to_string()),
false,
);
assert_eq!(
auth,
ResolvedRuntimeAuth {
token: Some("cli-token".to_string()),
generated: false,
}
);
}
#[test]
fn runtime_auth_ignores_blank_configured_tokens() {
let auth = resolve_runtime_auth(Some(" ".to_string()), Some("\t".to_string()), false);
assert!(auth.generated);
assert!(auth.token.is_some());
}
async fn spawn_test_server_with_root(
root: PathBuf,
sessions_dir: PathBuf,