fix(security): Tor onions for gated ports forward to the gate, not the app
Tor carries no session cookie, so HiddenServicePort → 127.0.0.1:<port> reached the app around the gate — the last transport the gate did not cover. The gate now binds 127.0.0.2 (its own loopback, distinct from the app's 127.0.0.1, so no app needs a second port), and regenerate_torrc forwards declared-gated ports there. Undeclared ports keep today's target: absence of the field is not an instruction. The 127.0.0.2 claim deliberately does not count toward the unprotected audit — a port whose only claim is the Tor loopback is still wide open on the LAN and must keep warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8210ca0a2a
commit
3760a00ea3
@@ -222,6 +222,18 @@ pub(in crate::api::rpc) async fn regenerate_torrc(config: &ServicesConfig) -> Re
|
||||
lines.push("# ControlPort disabled for security".to_string());
|
||||
lines.push(String::new());
|
||||
|
||||
// Ports whose manifests declare `auth: gated` forward to the gate's own
|
||||
// loopback (127.0.0.2, where the app-gate listener binds — see
|
||||
// `appgate::listener::GATE_TOR_UPSTREAM`) instead of the app's 127.0.0.1.
|
||||
// Tor carries no session cookie, so an onion pointed at the app is an
|
||||
// unauthenticated bypass of the gate. Declared-gated ports only: an
|
||||
// undeclared port keeps today's target, because absence of the field is
|
||||
// not an instruction (the v1.7.121 incident rule).
|
||||
let gated_ports: std::collections::HashSet<u16> = crate::appgate::identity::build_port_map()
|
||||
.gated_ports()
|
||||
.map(|g| g.port)
|
||||
.collect();
|
||||
|
||||
for svc in &config.services {
|
||||
if !svc.enabled {
|
||||
continue;
|
||||
@@ -240,7 +252,7 @@ pub(in crate::api::rpc) async fn regenerate_torrc(config: &ServicesConfig) -> Re
|
||||
lines.push("HiddenServicePort 10009 127.0.0.1:10009".to_string());
|
||||
}
|
||||
} else {
|
||||
lines.push(format!("HiddenServicePort 80 127.0.0.1:{}", svc.local_port));
|
||||
lines.push(app_hidden_service_port_line(svc.local_port, &gated_ports));
|
||||
}
|
||||
|
||||
lines.push(String::new());
|
||||
@@ -248,6 +260,24 @@ pub(in crate::api::rpc) async fn regenerate_torrc(config: &ServicesConfig) -> Re
|
||||
|
||||
let content = lines.join("\n");
|
||||
let staging = "/var/lib/archipelago/tor-config/torrc.staged";
|
||||
write_staged_torrc(&content, staging).await
|
||||
}
|
||||
|
||||
/// The `HiddenServicePort` line for an HTTP app onion. Gated ports forward to
|
||||
/// the gate's Tor upstream; everything else to the app itself.
|
||||
fn app_hidden_service_port_line(
|
||||
local_port: u16,
|
||||
gated_ports: &std::collections::HashSet<u16>,
|
||||
) -> String {
|
||||
let upstream = if gated_ports.contains(&local_port) {
|
||||
crate::appgate::listener::GATE_TOR_UPSTREAM.to_string()
|
||||
} else {
|
||||
"127.0.0.1".to_string()
|
||||
};
|
||||
format!("HiddenServicePort 80 {}:{}", upstream, local_port)
|
||||
}
|
||||
|
||||
async fn write_staged_torrc(content: &str, staging: &str) -> Result<()> {
|
||||
let config_dir = Path::new(staging)
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new("/var/lib/archipelago/tor-config"));
|
||||
@@ -256,14 +286,37 @@ pub(in crate::api::rpc) async fn regenerate_torrc(config: &ServicesConfig) -> Re
|
||||
.await
|
||||
.context("Failed to write staged torrc")?;
|
||||
|
||||
debug!(
|
||||
"Staged torrc with {} enabled services",
|
||||
config.services.iter().filter(|s| s.enabled).count()
|
||||
);
|
||||
debug!("Staged torrc ({} bytes)", content.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod torrc_tests {
|
||||
use super::app_hidden_service_port_line;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn gated_port_forwards_to_the_gate_not_the_app() {
|
||||
let gated: HashSet<u16> = [8082u16].into_iter().collect();
|
||||
assert_eq!(
|
||||
app_hidden_service_port_line(8082, &gated),
|
||||
"HiddenServicePort 80 127.0.0.2:8082"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undeclared_port_keeps_the_app_loopback_target() {
|
||||
// Absence of `auth: gated` is not an instruction — the onion keeps
|
||||
// pointing at the app, exactly as before this change.
|
||||
let gated: HashSet<u16> = [8082u16].into_iter().collect();
|
||||
assert_eq!(
|
||||
app_hidden_service_port_line(9100, &gated),
|
||||
"HiddenServicePort 80 127.0.0.1:9100"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Hostname Sync ───────────────────────────────────────────────
|
||||
|
||||
pub(in crate::api::rpc) async fn sync_single_hostname(name: &str, address: &str) {
|
||||
|
||||
Reference in New Issue
Block a user