diff --git a/docs/Distributed.md b/docs/Distributed.md index c5350515e2..16c94c92b2 100644 --- a/docs/Distributed.md +++ b/docs/Distributed.md @@ -278,6 +278,8 @@ of the scheduler and instruct clients to configure their `scheduler_url` with th appropriate `https://` address. The scheduler will verify the server's IP in this configuration by inspecting the `X-Real-IP` header's value, if present. The webserver used in this case should be configured to set this header to the appropriate value. +This verification is enabled by default and can be disabled by setting +`check_server_ip = false` in the scheduler config. Securing communication with the server is performed automatically - HTTPS certificates are generated dynamically on server startup and communicated to the scheduler during @@ -296,6 +298,8 @@ Use the `--config` argument to pass the path to its configuration file to `sccac # The socket address the scheduler will listen on. It's strongly recommended # to listen on localhost and put a HTTPS server in front of it. public_addr = "127.0.0.1:10600" +# Set to false to disable IP verification of build servers. +#check_server_ip = true [client_auth] type = "token" diff --git a/src/bin/sccache-dist/main.rs b/src/bin/sccache-dist/main.rs index 79f89e8d29..76cfa80ac8 100644 --- a/src/bin/sccache-dist/main.rs +++ b/src/bin/sccache-dist/main.rs @@ -167,6 +167,7 @@ fn run(command: Command) -> Result { public_addr, client_auth, server_auth, + check_server_ip, }) => { let check_client_auth: Box = match client_auth { scheduler_config::ClientAuth::Insecure => Box::new(token_check::EqCheck::new( @@ -224,6 +225,7 @@ fn run(command: Command) -> Result { scheduler, check_client_auth, check_server_auth, + check_server_ip, ); http_scheduler.start()?; unreachable!(); diff --git a/src/config.rs b/src/config.rs index ecbe98b8d8..bd88917235 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1444,6 +1444,14 @@ pub mod scheduler { pub public_addr: SocketAddr, pub client_auth: ClientAuth, pub server_auth: ServerAuth, + #[serde(default = "default_check_server_ip")] + /// Whether to verify the build server's IP address matches the address + /// in its auth token. + pub check_server_ip: bool, + } + + fn default_check_server_ip() -> bool { + true } pub fn from_path(conf_path: &Path) -> Result> { diff --git a/src/dist/http.rs b/src/dist/http.rs index 09aa862ed8..1a8f720b46 100644 --- a/src/dist/http.rs +++ b/src/dist/http.rs @@ -545,6 +545,33 @@ mod server { split.next() } + fn verify_server_ip( + server_id: &ServerId, + request: &rouille::Request, + ) -> StdResult<(), rouille::Response> { + let origin_ip = if let Some(header_val) = request.header("X-Real-IP") { + trace!("X-Real-IP: {:?}", header_val); + match header_val.parse() { + Ok(ip) => ip, + Err(err) => { + warn!( + "X-Real-IP value {:?} could not be parsed: {:?}", + header_val, err + ); + return Err(rouille::Response::empty_400()); + } + } + } else { + request.remote_addr().ip() + }; + if server_id.addr().ip() != origin_ip { + trace!("server ip: {:?}", server_id.addr().ip()); + trace!("request ip: {:?}", origin_ip); + return Err(make_401("invalid_bearer_token_mismatched_address")); + } + Ok(()) + } + /// Return `content` as a bincode-encoded `Response`. pub fn bincode_response(content: &T) -> rouille::Response where @@ -658,6 +685,51 @@ mod server { assert!(ja2.verify_token(job_id2, &token2).is_err()); } + #[test] + fn test_verify_server_ip_match_remote_addr() { + let addr: SocketAddr = "127.0.0.1:12345".parse().unwrap(); + let server_id = ServerId::new(addr); + let request = rouille::Request::fake_http("POST", "/", vec![], vec![]); + assert!(verify_server_ip(&server_id, &request).is_ok()); + } + + #[test] + fn test_verify_server_ip_mismatch_remote_addr() { + let server_id = ServerId::new("192.168.1.1:12345".parse().unwrap()); + let request = rouille::Request::fake_http("POST", "/", vec![], vec![]); + let result = verify_server_ip(&server_id, &request); + assert!(result.is_err()); + let resp = result.unwrap_err(); + assert_eq!(resp.status_code, 401); + } + + #[test] + fn test_verify_server_ip_match_x_real_ip() { + let server_id = ServerId::new("10.0.0.5:9000".parse().unwrap()); + let request = rouille::Request::fake_http( + "POST", + "/", + vec![("X-Real-IP".into(), "10.0.0.5".into())], + vec![], + ); + assert!(verify_server_ip(&server_id, &request).is_ok()); + } + + #[test] + fn test_verify_server_ip_mismatch_x_real_ip() { + let server_id = ServerId::new("10.0.0.5:9000".parse().unwrap()); + let request = rouille::Request::fake_http( + "POST", + "/", + vec![("X-Real-IP".into(), "10.0.0.6".into())], + vec![], + ); + let result = verify_server_ip(&server_id, &request); + assert!(result.is_err()); + let resp = result.unwrap_err(); + assert_eq!(resp.status_code, 401); + } + pub struct Scheduler { public_addr: SocketAddr, handler: S, @@ -665,6 +737,8 @@ mod server { check_client_auth: Box, // Do we believe the server is who they appear to be? check_server_auth: ServerAuthCheck, + // Verify the server's IP matches the address in its auth token + check_server_ip: bool, } impl Scheduler { @@ -673,12 +747,14 @@ mod server { handler: S, check_client_auth: Box, check_server_auth: ServerAuthCheck, + check_server_ip: bool, ) -> Self { Self { public_addr, handler, check_client_auth, check_server_auth, + check_server_ip, } } @@ -688,6 +764,7 @@ mod server { handler, check_client_auth, check_server_auth, + check_server_ip, } = self; let requester = SchedulerRequester { client: Mutex::new(new_reqwest_blocking_client()), @@ -697,28 +774,12 @@ mod server { ($request:ident) => {{ match bearer_http_auth($request).and_then(&*check_server_auth) { Some(server_id) => { - let origin_ip = if let Some(header_val) = $request.header("X-Real-IP") { - trace!("X-Real-IP: {:?}", header_val); - match header_val.parse() { - Ok(ip) => ip, - Err(err) => { - warn!( - "X-Real-IP value {:?} could not be parsed: {:?}", - header_val, err - ); - return rouille::Response::empty_400(); - } + if check_server_ip { + if let Err(resp) = verify_server_ip(&server_id, $request) { + return resp; } - } else { - $request.remote_addr().ip() - }; - if server_id.addr().ip() != origin_ip { - trace!("server ip: {:?}", server_id.addr().ip()); - trace!("request ip: {:?}", $request.remote_addr().ip()); - return make_401("invalid_bearer_token_mismatched_address"); - } else { - server_id } + server_id } None => return make_401("invalid_bearer_token"), } diff --git a/tests/harness/mod.rs b/tests/harness/mod.rs index 3ff280845f..747092d592 100644 --- a/tests/harness/mod.rs +++ b/tests/harness/mod.rs @@ -204,6 +204,7 @@ fn sccache_scheduler_cfg() -> sccache::config::scheduler::Config { server_auth: sccache::config::scheduler::ServerAuth::Token { token: DIST_SERVER_TOKEN.to_owned(), }, + check_server_ip: true, } }