mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-18 21:00:22 -05:00
fix(sensing-server): refuse a routable HTTP bind when API auth is off
The UDP receiver already refuses a routable bind without a source allowlist (ADR-296), and the Docker entrypoint refuses a routable bind without RUVIEW_API_TOKEN (#864). Running the binary directly with --bind-addr 0.0.0.0 and no auth only logged that auth was off. The server now exits at startup when the HTTP/WebSocket bind is not loopback and API auth (RUVIEW_API_TOKEN or RUVIEW_OAUTH_ISSUER) is off, unless RUVIEW_ALLOW_UNAUTHENTICATED is set, in which case it starts and logs a warning. Loopback binds are unaffected. The conditions match the entrypoint's, so a container that starts today still starts. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
1 parent
03a897938f
commit
fabb0c8233
4 files changed
+172
No files matched your search
@@ -1,5 +1,23 @@
|
||||
# Security notes — wifi-densepose-sensing-server
|
||||
|
||||
## HTTP and WebSocket API bind
|
||||
|
||||
`--bind-addr` (env `SENSING_BIND_ADDR`) defaults to `127.0.0.1`. The server
|
||||
refuses to start when the bind is routable (`0.0.0.0`, `::` or a LAN IP) and
|
||||
API auth is off, because `/api/v1/*` and `/ws/sensing` would then be readable
|
||||
by anyone who can reach the host. Pick one:
|
||||
|
||||
- **Enable auth.** Set `RUVIEW_API_TOKEN=<token>` (or `RUVIEW_OAUTH_ISSUER`).
|
||||
- **Stay on loopback.** Use `--bind-addr 127.0.0.1` and put a reverse proxy in
|
||||
front for remote access.
|
||||
- **Opt in explicitly.** Set `RUVIEW_ALLOW_UNAUTHENTICATED=1` (`true`, `yes`
|
||||
and `on` also work). The server starts and logs a warning. Only do this on an
|
||||
isolated, trusted network.
|
||||
|
||||
This is the same rule the Docker entrypoint already applies (#864), so a
|
||||
container that starts today keeps starting. The refusal exits with status 1;
|
||||
the entrypoint's own check exits with 64 before the binary runs.
|
||||
|
||||
## UDP CSI data plane (ADR-296)
|
||||
|
||||
The sensing server ingests CSI/radar frames over UDP from ESP32, MediaTek,
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//! HTTP/WebSocket bind check: refuse a routable bind with API auth off.
|
||||
//!
|
||||
//! Same rule the Docker entrypoint applies (#864) and the UDP receiver applies
|
||||
//! to its own bind (ADR-296, [`crate::udp_bind::decide_udp_bind`]): loopback is
|
||||
//! always fine, a routable bind needs either API auth (`RUVIEW_API_TOKEN` or
|
||||
//! `RUVIEW_OAUTH_ISSUER`) or the explicit `RUVIEW_ALLOW_UNAUTHENTICATED` opt-in.
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
/// Explicit opt-in to serve the API unauthenticated on a routable address.
|
||||
/// Same variable the Docker entrypoint reads.
|
||||
pub const ALLOW_UNAUTHENTICATED_ENV: &str = "RUVIEW_ALLOW_UNAUTHENTICATED";
|
||||
|
||||
/// Outcome of [`decide_http_bind`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HttpBindDecision {
|
||||
/// Bind is loopback-only (`127.0.0.0/8` / `::1`); not reachable off-host.
|
||||
Loopback,
|
||||
/// Bind is routable and API auth is on.
|
||||
RoutableAuthenticated,
|
||||
/// Bind is routable with API auth off, accepted via
|
||||
/// `RUVIEW_ALLOW_UNAUTHENTICATED`.
|
||||
RoutableUnauthenticated,
|
||||
}
|
||||
|
||||
/// Whether `RUVIEW_ALLOW_UNAUTHENTICATED` holds an opt-in value. Accepts `1`,
|
||||
/// `true`, `yes` and `on` in any case; anything else, including unset, is off.
|
||||
pub fn allow_unauthenticated_opt_in(value: Option<&str>) -> bool {
|
||||
value.is_some_and(|v| {
|
||||
matches!(
|
||||
v.trim().to_ascii_lowercase().as_str(),
|
||||
"1" | "true" | "yes" | "on"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Decide whether the HTTP/WebSocket listeners may bind to `bind`.
|
||||
///
|
||||
/// Pure and socket-free so it can be tested without binding.
|
||||
pub fn decide_http_bind(
|
||||
bind: IpAddr,
|
||||
auth_enabled: bool,
|
||||
allow_unauthenticated: bool,
|
||||
) -> Result<HttpBindDecision, String> {
|
||||
if bind.is_loopback() {
|
||||
return Ok(HttpBindDecision::Loopback);
|
||||
}
|
||||
if auth_enabled {
|
||||
return Ok(HttpBindDecision::RoutableAuthenticated);
|
||||
}
|
||||
if allow_unauthenticated {
|
||||
return Ok(HttpBindDecision::RoutableUnauthenticated);
|
||||
}
|
||||
Err(format!(
|
||||
"Refusing to serve the API on routable address {bind} with API auth off: \
|
||||
/api/v1/* and /ws/sensing would be readable by anyone who can reach this \
|
||||
host. Set RUVIEW_API_TOKEN=<token> (or RUVIEW_OAUTH_ISSUER) to enable \
|
||||
auth, use --bind-addr 127.0.0.1, or set {ALLOW_UNAUTHENTICATED_ENV}=1 on \
|
||||
a trusted network only."
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
fn v4(a: u8, b: u8, c: u8, d: u8) -> IpAddr {
|
||||
IpAddr::V4(Ipv4Addr::new(a, b, c, d))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loopback_starts_without_auth() {
|
||||
for ip in [
|
||||
IpAddr::V4(Ipv4Addr::LOCALHOST),
|
||||
v4(127, 0, 1, 1),
|
||||
IpAddr::V6(Ipv6Addr::LOCALHOST),
|
||||
] {
|
||||
assert_eq!(
|
||||
decide_http_bind(ip, false, false),
|
||||
Ok(HttpBindDecision::Loopback)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routable_without_auth_is_refused() {
|
||||
for ip in [
|
||||
IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
||||
IpAddr::V6(Ipv6Addr::UNSPECIFIED),
|
||||
v4(192, 168, 1, 10),
|
||||
] {
|
||||
let err = decide_http_bind(ip, false, false).unwrap_err();
|
||||
assert!(err.contains("RUVIEW_API_TOKEN"), "{err}");
|
||||
assert!(err.contains(ALLOW_UNAUTHENTICATED_ENV), "{err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routable_with_auth_starts() {
|
||||
assert_eq!(
|
||||
decide_http_bind(v4(0, 0, 0, 0), true, false),
|
||||
Ok(HttpBindDecision::RoutableAuthenticated)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routable_with_opt_in_starts_unauthenticated() {
|
||||
assert_eq!(
|
||||
decide_http_bind(v4(0, 0, 0, 0), false, true),
|
||||
Ok(HttpBindDecision::RoutableUnauthenticated)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opt_in_accepts_entrypoint_spellings_only() {
|
||||
for v in ["1", "true", "TRUE", "yes", "On", " 1 "] {
|
||||
assert!(allow_unauthenticated_opt_in(Some(v)), "{v:?}");
|
||||
}
|
||||
for v in ["", "0", "false", "no", "off", "2", "y"] {
|
||||
assert!(!allow_unauthenticated_opt_in(Some(v)), "{v:?}");
|
||||
}
|
||||
assert!(!allow_unauthenticated_opt_in(None));
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ pub mod discovery;
|
||||
pub mod edge_registry;
|
||||
pub mod error_response;
|
||||
pub mod host_validation;
|
||||
/// #864/ADR-296: refuse a routable HTTP bind with API auth off.
|
||||
pub mod http_bind;
|
||||
/// ADR-297: per-node vs. fused room inference, with deterministic fusion.
|
||||
pub mod inference;
|
||||
pub mod introspection;
|
||||
|
||||
@@ -12386,6 +12386,33 @@ async fn main() {
|
||||
);
|
||||
}
|
||||
|
||||
// #864/ADR-296: a routable bind with auth off is refused, the same rule
|
||||
// the Docker entrypoint and the UDP receiver apply. Loopback is unaffected.
|
||||
{
|
||||
use wifi_densepose_sensing_server::http_bind;
|
||||
let allow_unauthenticated = http_bind::allow_unauthenticated_opt_in(
|
||||
std::env::var(http_bind::ALLOW_UNAUTHENTICATED_ENV)
|
||||
.ok()
|
||||
.as_deref(),
|
||||
);
|
||||
match http_bind::decide_http_bind(
|
||||
bind_ip,
|
||||
bearer_auth_state.is_enabled(),
|
||||
allow_unauthenticated,
|
||||
) {
|
||||
Ok(http_bind::HttpBindDecision::RoutableUnauthenticated) => warn!(
|
||||
"API auth OFF on routable bind {bind_ip} ({} set): /api/v1/* and \
|
||||
/ws/sensing are readable by anyone who can reach this host",
|
||||
http_bind::ALLOW_UNAUTHENTICATED_ENV
|
||||
),
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DNS-rebinding defense: validate the `Host` header against an allowlist
|
||||
// before any handler runs. Default is loopback-only (`localhost`,
|
||||
// `127.0.0.1`, `[::1]`, each with or without a port). Operators extend
|
||||
|
||||
Reference in new issue
Block a user