Skip to content

Commit 80a5270

Browse files
authored
Merge pull request #831 from Dstack-TEE/fix/gateway-insecure-localhost-backend
fix(gateway): move localhost routing behind an insecure debug flag
2 parents fcad53d + 912786b commit 80a5270

7 files changed

Lines changed: 32 additions & 8 deletions

File tree

dstack/gateway/dstack-app/builder/entrypoint.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ tls_versions = ["1.2"]
114114
listen_addr = "0.0.0.0"
115115
listen_port = "${PROXY_LISTEN_PORT:-443}"
116116
connect_top_n = 3
117-
localhost_enabled = false
118117
app_address_ns_compat = true
119118
workers = ${PROXY_WORKERS:-32}
120119
max_connections_per_app = ${MAX_CONNECTIONS_PER_APP:-0}

dstack/gateway/gateway.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ insecure_no_auth = false
5656

5757
[core.debug]
5858
insecure_enable_debug_rpc = false
59+
# Route the app address "localhost" to 127.0.0.1 on the gateway host. Off by
60+
# default: the app address also comes from the _dstack-app-address TXT record of
61+
# arbitrary custom domains, so enabling this lets any DNS zone owner reach the
62+
# gateway's own loopback on a port of their choosing, bypassing port_policy.
63+
insecure_localhost_backend = false
5964
insecure_skip_attestation = false
6065
key_file = "debug_key.json"
6166
address = "127.0.0.1:8012"
@@ -80,7 +85,6 @@ agent_port = 8090
8085
buffer_size = 65536
8186
# number of hosts to try to connect to
8287
connect_top_n = 3
83-
localhost_enabled = false
8488
app_address_ns_prefix = "_dstack-app-address"
8589
app_address_ns_compat = true
8690
workers = 32

dstack/gateway/src/config.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@ pub struct ProxyConfig {
129129
/// 1 MiB pipe fed without the syscall rate 8 KiB imposed.
130130
pub buffer_size: usize,
131131
pub connect_top_n: usize,
132-
pub localhost_enabled: bool,
133132
pub workers: usize,
134133
/// Run one single-threaded runtime per worker, each with its own
135134
/// `SO_REUSEPORT` listener, instead of one accept thread feeding a shared
@@ -522,6 +521,19 @@ pub struct DebugConfig {
522521
pub insecure_enable_debug_rpc: bool,
523522
#[serde(default)]
524523
pub insecure_skip_attestation: bool,
524+
/// Let the app-address `localhost` resolve to 127.0.0.1, so a hostname can
525+
/// be routed to a service on the gateway host itself.
526+
///
527+
/// This lives under `debug` and carries the `insecure_` prefix because the
528+
/// app address is not only read from the platform's own `<id>.<base_domain>`
529+
/// grammar: it also comes from the `_dstack-app-address` TXT record of an
530+
/// arbitrary custom domain. With this on, anyone who controls any DNS zone
531+
/// can point the gateway at its own loopback -- where the admin and debug
532+
/// listeners bind precisely because being unreachable is their access
533+
/// control -- and pick the port, since the `localhost` shortcut is not a
534+
/// registered instance and so bypasses `port_policy` entirely.
535+
#[serde(default)]
536+
pub insecure_localhost_backend: bool,
525537
/// Path to pre-generated debug key data file (JSON format containing key, quote, event_log, and vm_config)
526538
#[serde(default)]
527539
pub key_file: String,

dstack/gateway/src/main.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use rocket::{
2121
};
2222
use serde::{Deserialize, Serialize};
2323
use std::sync::Arc;
24-
use tracing::info;
24+
use tracing::{info, warn};
2525

2626
use admin_service::AdminRpcHandler;
2727
use main_service::{Proxy, ProxyOptions, RpcHandler};
@@ -245,6 +245,15 @@ async fn main() -> Result<()> {
245245
if config.sync.enabled && config.sync.node_id == 0 {
246246
anyhow::bail!("node_id must be greater than 0");
247247
}
248+
if config.debug.insecure_localhost_backend {
249+
warn!(
250+
"core.debug.insecure_localhost_backend = true; the app address \"localhost\" now \
251+
resolves to 127.0.0.1 on this host. App addresses also come from the \
252+
_dstack-app-address TXT record of arbitrary custom domains, so any DNS zone owner \
253+
can reach this host's loopback on a port of their choosing, bypassing port_policy. \
254+
Never use this outside local development"
255+
);
256+
}
248257
// Before anything reads `proxy.ktls`: the acceptor built later decides
249258
// whether to extract session secrets from it.
250259
proxy::disable_ktls_if_unsupported(&mut config.proxy);

dstack/gateway/src/main_service.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1143,7 +1143,7 @@ impl ProxyState {
11431143
}
11441144

11451145
pub(crate) fn select_top_n_hosts(&mut self, id: &str) -> Result<AddressGroup> {
1146-
if self.config.proxy.localhost_enabled && id == "localhost" {
1146+
if self.config.debug.insecure_localhost_backend && id == "localhost" {
11471147
return Ok(smallvec![AddressInfo {
11481148
ip: Ipv4Addr::new(127, 0, 0, 1),
11491149
counter: Default::default(),

dstack/gateway/test-run/proxy/gwconfig.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def main():
5858
rpc_domain = ""
5959
set_ulimit = false
6060
[core.debug]
61+
insecure_localhost_backend = true
6162
insecure_skip_attestation = true
6263
insecure_enable_debug_rpc = false
6364
[core.admin]
@@ -81,7 +82,6 @@ def main():
8182
[core.proxy]
8283
listen_addr = "127.0.0.1"
8384
listen_port = {o["proxy_port"]}
84-
localhost_enabled = true
8585
base_domain = "{o["base_domain"]}"
8686
cert_chain = "{cert}"
8787
cert_key = "{key}"

dstack/gateway/test-run/test_proxy.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ ORIGIN_PLAIN=$((BASE_PORT + 4))
3535
ORIGIN_TLS=$((BASE_PORT + 5))
3636

3737
BASE_DOMAIN="gwtest.local"
38-
# `localhost_enabled` routes `localhost-<port>[s]` to 127.0.0.1:<port>, which is
39-
# what lets these tests run without registering a CVM.
38+
# `insecure_localhost_backend` routes `localhost-<port>[s]` to 127.0.0.1:<port>,
39+
# which is what lets these tests run without registering a CVM.
4040
SNI_TERMINATE="localhost-$ORIGIN_PLAIN.$BASE_DOMAIN"
4141
SNI_PASSTHROUGH="localhost-${ORIGIN_TLS}s.$BASE_DOMAIN"
4242
ADMIN_TOKEN="proxy-integration-test"

0 commit comments

Comments
 (0)