feat(security): harden gateway boundaries and usage policies

Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change.

Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
This commit is contained in:
elky
2026-09-04 03:45:52 +08:00
parent ddcbeb3ae9
commit 579f2c7cc1
1019 changed files with 190437 additions and 26080 deletions
@@ -7,6 +7,7 @@ use crate::redis::{
};
use crate::{
DataLayerError, RateLimitCheck, RateLimitInput, RateLimitScope, RuntimeSemaphoreError,
UsageLimitCheck, UsageLimitInput, UsageLimitReleaseInput,
};
const RATE_LIMIT_CHECK_AND_CONSUME_SCRIPT: &str = r#"
@@ -51,6 +52,50 @@ end
return {1, 0, 0, remaining}
"#;
const USAGE_LIMIT_CHECK_AND_CONSUME_SCRIPT: &str = r#"
local count = #KEYS
local now = tonumber(ARGV[1])
local event_id = ARGV[2]
for i = 1, count do
local window_ms = tonumber(ARGV[(i - 1) * 3 + 4]) * 1000
local cutoff = now - window_ms
redis.call('ZREMRANGEBYSCORE', KEYS[i], '-inf', cutoff)
end
for i = 1, count do
local limit = tonumber(ARGV[(i - 1) * 3 + 3])
local window_ms = tonumber(ARGV[(i - 1) * 3 + 4]) * 1000
local already_consumed = redis.call('ZSCORE', KEYS[i], event_id)
if not already_consumed then
local current = redis.call('ZCARD', KEYS[i])
if current >= limit then
local earliest = redis.call('ZRANGE', KEYS[i], 0, 0, 'WITHSCORES')
local retry_after = 1
if #earliest >= 2 then
local retry_after_ms = math.max(1, tonumber(earliest[2]) + window_ms - now)
retry_after = math.ceil(retry_after_ms / 1000)
end
return {0, i, limit, retry_after}
end
end
end
for i = 1, count do
local retention = tonumber(ARGV[(i - 1) * 3 + 5])
redis.call('ZADD', KEYS[i], 'NX', now, event_id)
redis.call('EXPIRE', KEYS[i], retention + 1)
end
return {1, 0, 0, 0}
"#;
const USAGE_LIMIT_RELEASE_SCRIPT: &str = r#"
for i = 1, #KEYS do
redis.call('ZREM', KEYS[i], ARGV[1])
end
return 1
"#;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct RedisRuntimeDiagnostics {
pub connected_clients: Option<u64>,
@@ -147,6 +192,37 @@ impl RedisRuntimeRunner {
Ok(())
}
pub(crate) async fn kv_set_if_absent(
&self,
key: &str,
value: String,
ttl: Duration,
) -> Result<bool, DataLayerError> {
let namespaced_key = self.keyspace.key(key);
let ttl_ms = u64::try_from(ttl.as_millis().max(1)).unwrap_or(u64::MAX);
let mut command = cmd("SET");
command
.arg(namespaced_key)
.arg(value)
.arg("NX")
.arg("PX")
.arg(ttl_ms);
let response = self
.query::<Option<String>>(
RedisConnectionLane::Fast,
"runtime kv set if absent",
command,
)
.await?;
match response.as_deref() {
Some("OK") => Ok(true),
None => Ok(false),
Some(value) => Err(DataLayerError::UnexpectedValue(format!(
"unexpected runtime kv set if absent response {value}"
))),
}
}
pub(crate) async fn kv_get_many(
&self,
keys: &[String],
@@ -282,6 +358,105 @@ impl RedisRuntimeRunner {
Ok(RateLimitCheck::Rejected { scope, limit })
}
pub(crate) async fn check_and_consume_usage_limits(
&self,
input: UsageLimitInput<'_>,
) -> Result<UsageLimitCheck, DataLayerError> {
let script = script(USAGE_LIMIT_CHECK_AND_CONSUME_SCRIPT);
let mut invocation = script.prepare_invoke();
for rule in input.rules {
invocation.key(self.keyspace.key(rule.key));
}
invocation.arg(input.now_unix_ms as i64);
invocation.arg(input.event_id);
for rule in input.rules {
invocation.arg(rule.limit as i64);
invocation.arg(rule.window_seconds as i64);
invocation.arg(rule.retention_seconds as i64);
}
let raw = run_lane_with_timeout(
&self.connections,
RedisConnectionLane::Fast,
self.command_timeout_ms,
"runtime usage limit check",
async {
let mut connection = self.connections.connection(RedisConnectionLane::Fast);
invocation
.invoke_async::<Vec<i64>>(&mut connection)
.await
.map_redis_err()
},
)
.await?;
match raw.first().copied() {
Some(1) if raw.len() >= 4 => return Ok(UsageLimitCheck::Allowed),
Some(0) if raw.len() >= 4 => {}
_ => {
return Err(DataLayerError::UnexpectedValue(
"runtime usage limit script returned an invalid response".to_string(),
));
}
}
let rule_index = raw
.get(1)
.copied()
.and_then(|value| usize::try_from(value.saturating_sub(1)).ok())
.filter(|index| *index < input.rules.len())
.ok_or_else(|| {
DataLayerError::UnexpectedValue(
"runtime usage limit script returned an invalid rule index".to_string(),
)
})?;
let limit = raw
.get(2)
.copied()
.and_then(|value| u64::try_from(value).ok())
.filter(|limit| *limit > 0)
.ok_or_else(|| {
DataLayerError::UnexpectedValue(
"runtime usage limit script returned an invalid limit".to_string(),
)
})?;
let retry_after = raw
.get(3)
.copied()
.and_then(|value| u64::try_from(value).ok())
.unwrap_or(1)
.max(1);
Ok(UsageLimitCheck::Rejected {
rule_index,
limit,
retry_after,
})
}
pub(crate) async fn release_usage_limits(
&self,
input: UsageLimitReleaseInput<'_>,
) -> Result<(), DataLayerError> {
let script = script(USAGE_LIMIT_RELEASE_SCRIPT);
let mut invocation = script.prepare_invoke();
for rule in input.rules {
invocation.key(self.keyspace.key(rule.key));
}
invocation.arg(input.event_id);
run_lane_with_timeout(
&self.connections,
RedisConnectionLane::Fast,
self.command_timeout_ms,
"runtime usage limit release",
async {
let mut connection = self.connections.connection(RedisConnectionLane::Fast);
invocation
.invoke_async::<i64>(&mut connection)
.await
.map_redis_err()
},
)
.await?;
Ok(())
}
pub(crate) async fn set_add(&self, key: &str, member: &str) -> Result<bool, DataLayerError> {
let key = self.keyspace.key(key);
let mut command = cmd("SADD");