feat(rpc): add server-side internode v2 signature verification (fail-open, method-path binding groundwork) (#5160)

* feat(rpc): gate internode legacy signature fallback behind a convergence counter and strict env

Add the backlog#1327 Plan-A rollout infrastructure on top of the already-merged v2 target-bound internode gRPC authentication: a rustfs_system_network_internode_signature_v1_fallback_total counter that increments only when a request without any v2 auth headers is accepted through the legacy constant-target signature, and a RUSTFS_INTERNODE_RPC_SIGNATURE_STRICT env (default false, compile-time asserted) that, when enabled later, closes the legacy fallback path. Default behavior is fail-open and byte-identical for legacy-only peers; requests carrying v2 headers are verified as v2 with no downgrade exactly as before.

Refs https://github.com/rustfs/backlog/issues/1327

* ci: fix internode auth test lint failures

* perf(ecstore): cache internode sig strict env

---------

Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
Zhengchao An
2026-07-25 16:17:49 +08:00
committed by GitHub
parent ce41adfa9b
commit ffd1b94e1f
6 changed files with 226 additions and 3 deletions
+42
View File
@@ -2368,6 +2368,48 @@ mod tests {
rustfs_common::set_global_local_node_name(&previous_node_name).await;
}
/// Rolling-upgrade compatibility anchor for <https://github.com/rustfs/backlog/issues/1327>:
/// a legacy-only peer (constant-target signature, no v2 headers) must keep authenticating
/// through the real production path (`check_auth` + `RpcRequestTarget` extension), and every
/// such acceptance must increment the v1-fallback convergence counter exactly once. That
/// counter reading zero fleet-wide is the precondition for ever enabling
/// `RUSTFS_INTERNODE_RPC_SIGNATURE_STRICT`.
#[tokio::test]
#[serial_test::serial]
async fn rpc_auth_accepts_legacy_only_peer_and_counts_v1_fallback() {
use rustfs_io_metrics::internode_metrics::global_internode_metrics;
let _ = rustfs_credentials::set_global_rpc_secret("rpc-http-test-secret".to_string());
let previous_node_name = rustfs_common::get_global_local_node_name().await;
rustfs_common::set_global_local_node_name("127.0.0.1:9000").await;
// Exactly what an old peer sends on the gRPC plane: the legacy constant-target signature
// and timestamp headers, nothing else.
let legacy_headers = storage::gen_signature_headers(TONIC_RPC_PREFIX, &Method::GET).expect("legacy headers should build");
let mut request = Request::new(());
request.metadata_mut().as_mut().extend(legacy_headers);
request.extensions_mut().insert(RpcRequestTarget {
uri: "http://127.0.0.1:9000/node_service.NodeService/Ping"
.parse()
.expect("test RPC URI should parse"),
method: Method::POST,
});
let before = global_internode_metrics().snapshot().signature_v1_fallback_total;
assert!(
check_auth(request).is_ok(),
"a legacy-only peer must keep authenticating during rolling upgrades"
);
let after = global_internode_metrics().snapshot().signature_v1_fallback_total;
assert_eq!(
after,
before + 1,
"an accepted legacy-only request must increment signature_v1_fallback_total exactly once"
);
rustfs_common::set_global_local_node_name(&previous_node_name).await;
}
#[tokio::test]
#[serial_test::serial]
async fn peer_rest_heal_control_uses_production_auth_and_keeps_validation_errors_online() {
+3 -1
View File
@@ -488,7 +488,7 @@ pub(crate) mod ecstore_rpc {
};
#[cfg(test)]
pub(crate) use rustfs_ecstore::api::rpc::{
gen_tonic_signature_headers, set_tonic_canonical_body_digest, verify_tonic_rpc_response_proof,
gen_signature_headers, gen_tonic_signature_headers, set_tonic_canonical_body_digest, verify_tonic_rpc_response_proof,
};
}
@@ -552,6 +552,8 @@ pub(crate) fn try_current_local_node_name() -> Option<String> {
crate::storage::runtime_sources::try_current_local_node_name()
}
#[cfg(test)]
pub(crate) use ecstore_rpc::gen_signature_headers;
#[cfg(test)]
pub(crate) use ecstore_rpc::gen_tonic_signature_headers;
pub(crate) use ecstore_rpc::sign_tonic_rpc_response_proof;
+2 -1
View File
@@ -106,7 +106,8 @@ pub(crate) mod server {
#[cfg(test)]
pub(crate) use crate::storage::storage_api::{
Endpoint, EndpointServerPools, Endpoints, PeerRestClient, PoolEndpoints, gen_tonic_signature_headers,
Endpoint, EndpointServerPools, Endpoints, PeerRestClient, PoolEndpoints, gen_signature_headers,
gen_tonic_signature_headers,
};
pub(crate) mod ecfs {