fix(gateway): harden client identity and bound key stores

This commit is contained in:
fettpl 2026-02-17 22:09:38 +01:00 committed by Chummy
parent b1c04d8f88
commit c507856710
2 changed files with 267 additions and 48 deletions

View file

@ -479,9 +479,22 @@ pub struct GatewayConfig {
#[serde(default = "default_webhook_rate_limit")]
pub webhook_rate_limit_per_minute: u32,
/// Trust proxy-forwarded client IP headers (`X-Forwarded-For`, `X-Real-IP`).
/// Disabled by default; enable only behind a trusted reverse proxy.
#[serde(default)]
pub trust_forwarded_headers: bool,
/// Maximum distinct client keys tracked by gateway rate limiter maps.
#[serde(default = "default_gateway_rate_limit_max_keys")]
pub rate_limit_max_keys: usize,
/// TTL for webhook idempotency keys.
#[serde(default = "default_idempotency_ttl_secs")]
pub idempotency_ttl_secs: u64,
/// Maximum distinct idempotency keys retained in memory.
#[serde(default = "default_gateway_idempotency_max_keys")]
pub idempotency_max_keys: usize,
}
fn default_gateway_port() -> u16 {
@ -504,6 +517,14 @@ fn default_idempotency_ttl_secs() -> u64 {
300
}
fn default_gateway_rate_limit_max_keys() -> usize {
10_000
}
fn default_gateway_idempotency_max_keys() -> usize {
10_000
}
fn default_true() -> bool {
true
}
@ -518,7 +539,10 @@ impl Default for GatewayConfig {
paired_tokens: Vec::new(),
pair_rate_limit_per_minute: default_pair_rate_limit(),
webhook_rate_limit_per_minute: default_webhook_rate_limit(),
trust_forwarded_headers: false,
rate_limit_max_keys: default_gateway_rate_limit_max_keys(),
idempotency_ttl_secs: default_idempotency_ttl_secs(),
idempotency_max_keys: default_gateway_idempotency_max_keys(),
}
}
}
@ -2946,7 +2970,10 @@ channel_id = "C123"
);
assert_eq!(g.pair_rate_limit_per_minute, 10);
assert_eq!(g.webhook_rate_limit_per_minute, 60);
assert!(!g.trust_forwarded_headers);
assert_eq!(g.rate_limit_max_keys, 10_000);
assert_eq!(g.idempotency_ttl_secs, 300);
assert_eq!(g.idempotency_max_keys, 10_000);
}
#[test]
@ -2974,7 +3001,10 @@ channel_id = "C123"
paired_tokens: vec!["zc_test_token".into()],
pair_rate_limit_per_minute: 12,
webhook_rate_limit_per_minute: 80,
trust_forwarded_headers: true,
rate_limit_max_keys: 2048,
idempotency_ttl_secs: 600,
idempotency_max_keys: 4096,
};
let toml_str = toml::to_string(&g).unwrap();
let parsed: GatewayConfig = toml::from_str(&toml_str).unwrap();
@ -2983,7 +3013,10 @@ channel_id = "C123"
assert_eq!(parsed.paired_tokens, vec!["zc_test_token"]);
assert_eq!(parsed.pair_rate_limit_per_minute, 12);
assert_eq!(parsed.webhook_rate_limit_per_minute, 80);
assert!(parsed.trust_forwarded_headers);
assert_eq!(parsed.rate_limit_max_keys, 2048);
assert_eq!(parsed.idempotency_ttl_secs, 600);
assert_eq!(parsed.idempotency_max_keys, 4096);
}
#[test]
@ -3622,6 +3655,9 @@ default_model = "legacy-model"
assert!(g.require_pairing);
assert!(!g.allow_public_bind);
assert!(g.paired_tokens.is_empty());
assert!(!g.trust_forwarded_headers);
assert_eq!(g.rate_limit_max_keys, 10_000);
assert_eq!(g.idempotency_max_keys, 10_000);
}
// ── Peripherals config ───────────────────────────────────────