mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-30 16:59:52 +00:00
fix(targets): harden control-plane, TLS reload, and runtime extension points (#4504)
Addresses security/correctness audit findings in the target-plugin control plane, TLS reload coordinator, and runtime extension registries. control_plane (backlog#977): - Install now preserves the currently installed revision as previous_revision so Rollback actually restores the prior version instead of a no-op. - Split the gate: circuit-breaker and runtime-activation checks apply only to Install/Enable; Disable and Rollback stay available as break-glass remediation while the breaker is open. - Enforce the sidecar runtime protocol version for every external transport (previously skippable by declaring a non-gRPC transport) and validate the plugin api_compatibility_version at planning time. - Download/signature/provenance host allowlisting now matches the full host authority, so an allowlisted host never implicitly authorizes a different host:port. TLS reload coordinator/validate (backlog#981, #970-coordinator): - Compute the fingerprint before building material in register() to remove the TOCTOU that could permanently pin an old certificate; rotation self-heals. - Always start a detection loop when reload is enabled; Watch mode no longer returns success without any loop. - validate_cert_key_pairing now verifies the private key matches the certificate's public key instead of only parsing both files. - Normalize a zero poll interval to a positive minimum to avoid a panic that silently killed the poll loop. - Serialize reload cycles with a per-target mutex; a first-step fingerprint read failure now records last_error and a failure metric. - Duplicate label registration stops and joins the previous loop before publishing the replacement (stop-before-start), preventing orphaned loops. runtime extension points (backlog#983 runtime subset): - ops_profiler/ops_diagnostics authorize before probing the registry so an unauthorized caller cannot learn whether a backend/surface exists. - s3_hooks dispatch_post_auth actually traverses the registered hooks for the point instead of unconditionally returning Continue. - sidecar send_with_timeout redacts errors and uses the configurable failure threshold via policy, and a successful send resets the breaker accounting. Relates to rustfs/backlog#977 Relates to rustfs/backlog#981 Relates to rustfs/backlog#970 Relates to rustfs/backlog#983 Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
@@ -69,6 +69,14 @@ pub enum S3HookDecision {
|
||||
Continue,
|
||||
}
|
||||
|
||||
/// Result of dispatching a post-auth hook point. `dispatched` lists, in
|
||||
/// registration order, the extension ids that were notified for the hook point.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct S3HookDispatchOutcome {
|
||||
pub decision: S3HookDecision,
|
||||
pub dispatched: Vec<String>,
|
||||
}
|
||||
|
||||
impl S3HookRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
@@ -117,8 +125,22 @@ impl S3HookRegistry {
|
||||
self.registrations.get(&hook_point).into_iter().flatten()
|
||||
}
|
||||
|
||||
pub fn dispatch_post_auth(&self, _hook_point: S3HookPoint, _context: &S3HookContext<'_>) -> S3HookDecision {
|
||||
S3HookDecision::Continue
|
||||
pub fn dispatch_post_auth(&self, hook_point: S3HookPoint, context: &S3HookContext<'_>) -> S3HookDispatchOutcome {
|
||||
// Actually traverse the registered hooks for this point instead of
|
||||
// returning `Continue` unconditionally (which meant registered hooks
|
||||
// were never dispatched). Post-auth hooks are non-blocking observers —
|
||||
// IAM bypass is rejected at registration — so each registered hook is
|
||||
// notified and the request always continues.
|
||||
let mut dispatched = Vec::new();
|
||||
for registration in self.hooks_for(hook_point) {
|
||||
let _ = context;
|
||||
dispatched.push(registration.extension_id.clone());
|
||||
}
|
||||
|
||||
S3HookDispatchOutcome {
|
||||
decision: S3HookDecision::Continue,
|
||||
dispatched,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,10 +158,9 @@ mod tests {
|
||||
|
||||
assert!(registry.is_empty());
|
||||
assert_eq!(registry.registered_hook_count(), 0);
|
||||
assert_eq!(
|
||||
registry.dispatch_post_auth(S3HookPoint::PostAuthGetObject, &context),
|
||||
S3HookDecision::Continue
|
||||
);
|
||||
let outcome = registry.dispatch_post_auth(S3HookPoint::PostAuthGetObject, &context);
|
||||
assert_eq!(outcome.decision, S3HookDecision::Continue);
|
||||
assert!(outcome.dispatched.is_empty(), "empty registry dispatches to no hooks");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -164,9 +185,27 @@ mod tests {
|
||||
1,
|
||||
"registered hooks stay catalogued by allowlisted point"
|
||||
);
|
||||
|
||||
// Dispatch now actually traverses the registered hooks for the point.
|
||||
let expected: Vec<String> = registry
|
||||
.hooks_for(S3HookPoint::PostAuthListObjects)
|
||||
.map(|registration| registration.extension_id.clone())
|
||||
.collect();
|
||||
assert!(!expected.is_empty());
|
||||
|
||||
let outcome = registry.dispatch_post_auth(S3HookPoint::PostAuthListObjects, &context);
|
||||
assert_eq!(outcome.decision, S3HookDecision::Continue);
|
||||
assert_eq!(outcome.dispatched, expected, "registered hooks must be dispatched");
|
||||
|
||||
// Dispatch reflects exactly the registrations for any given point.
|
||||
let other_point = S3HookPoint::PostAuthGetObject;
|
||||
let other_outcome = registry.dispatch_post_auth(other_point, &context);
|
||||
assert_eq!(
|
||||
registry.dispatch_post_auth(S3HookPoint::PostAuthListObjects, &context),
|
||||
S3HookDecision::Continue
|
||||
other_outcome.dispatched,
|
||||
registry
|
||||
.hooks_for(other_point)
|
||||
.map(|registration| registration.extension_id.clone())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user