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
@@ -11,7 +11,11 @@ use aether_data_contracts::repository::candidates::DecisionTrace;
use aether_data_contracts::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
};
use aether_data_contracts::repository::settlement::{StoredUsageSettlement, UsageSettlementInput};
use aether_data_contracts::repository::proxy_nodes::ProxyNodeTrafficMutation;
use aether_data_contracts::repository::settlement::{
ReconcileUsagePolicyCostInput, StoredUsagePolicyCostReservation, StoredUsageSettlement,
UsageSettlementInput,
};
use aether_data_contracts::repository::usage::{
ProxyNodeCounterDelta, StoredRequestUsageAudit, UpsertUsageRecord, UsageWriteRepository,
};
@@ -34,7 +38,7 @@ const LEGACY_REQUEST_LOG_LEVEL_KEY: &str = "request_log_level";
fn usage_request_record_level_from_value(value: Option<&Value>) -> UsageRequestRecordLevel {
let Some(value) = value.and_then(Value::as_str).map(str::trim) else {
return UsageRequestRecordLevel::Full;
return UsageRequestRecordLevel::Basic;
};
if value.eq_ignore_ascii_case("basic")
@@ -45,7 +49,9 @@ fn usage_request_record_level_from_value(value: Option<&Value>) -> UsageRequestR
{
UsageRequestRecordLevel::Basic
} else {
UsageRequestRecordLevel::Full
// Raw HTTP payload capture is disabled at the runtime boundary. The setting remains
// accepted for compatibility, but no longer authorizes collecting request/response data.
UsageRequestRecordLevel::Basic
}
}
@@ -95,6 +101,14 @@ impl StoredVideoTaskReadSide for GatewayDataState {
) -> Result<Option<StoredVideoTask>, DataLayerError> {
GatewayDataState::find_video_task(self, key).await
}
async fn find_stored_video_task_for_user(
&self,
key: VideoTaskLookupKey<'_>,
user_id: &str,
) -> Result<Option<StoredVideoTask>, DataLayerError> {
GatewayDataState::find_video_task_for_user(self, key, user_id).await
}
}
#[async_trait]
@@ -219,6 +233,13 @@ impl UsageSettlementWriter for GatewayDataState {
GatewayDataState::has_settlement_writer(self)
}
async fn reconcile_usage_policy_cost(
&self,
input: ReconcileUsagePolicyCostInput,
) -> Result<Option<StoredUsagePolicyCostReservation>, DataLayerError> {
GatewayDataState::reconcile_usage_policy_cost(self, input).await
}
async fn settle_usage(
&self,
input: UsageSettlementInput,
@@ -285,10 +306,26 @@ impl aether_usage_runtime::ManualProxyNodeCounter for GatewayDataState {
failed_delta: i64,
latency_ms: Option<i64>,
) -> Result<(), DataLayerError> {
// This API predates incarnation fences and only receives a node id. Read
// the selected node first so every durable path can bind the delta to
// the observed generation. If the node disappeared, fail closed rather
// than allowing a bare id to target a replacement node.
let Some(node) = self.find_proxy_node(node_id).await? else {
return Ok(());
};
if !node.is_manual {
return Ok(());
}
let expected_tunnel_generation = node.tunnel_generation.trim().to_string();
if expected_tunnel_generation.is_empty() {
return Ok(());
}
if let Some(repository) = &self.usage_writer {
let enqueued = repository
.enqueue_proxy_node_counter_delta(ProxyNodeCounterDelta {
node_id: node_id.to_string(),
expected_tunnel_generation: Some(expected_tunnel_generation.clone()),
total_requests_delta: total_delta,
failed_requests_delta: failed_delta,
dns_failures_delta: 0,
@@ -300,14 +337,25 @@ impl aether_usage_runtime::ManualProxyNodeCounter for GatewayDataState {
}
}
match &self.proxy_node_writer {
Some(repository) => {
repository
.increment_manual_node_requests(node_id, total_delta, failed_delta, latency_ms)
.await
}
None => Ok(()),
// The legacy increment method has no generation argument and would
// re-read the current row, which is vulnerable to an id reuse between
// the read above and the write. Use the fenced traffic mutation as the
// only fallback. It intentionally omits the legacy latency-only field;
// preserving counters safely is more important than an unfenced write.
if let Some(repository) = &self.proxy_node_writer {
let _ = repository
.record_traffic(&ProxyNodeTrafficMutation {
node_id: node_id.to_string(),
expected_tunnel_generation: Some(expected_tunnel_generation),
total_requests_delta: total_delta,
failed_requests_delta: failed_delta,
dns_failures_delta: 0,
stream_errors_delta: 0,
})
.await?;
}
let _ = latency_ms;
Ok(())
}
}
@@ -452,6 +500,20 @@ mod tests {
assert_eq!(level, UsageRequestRecordLevel::Basic);
}
#[tokio::test]
async fn usage_runtime_access_disables_full_http_capture() {
let state = GatewayDataState::disabled().with_system_config_values_for_tests([(
"request_record_level".to_string(),
json!("full"),
)]);
let level = UsageRuntimeAccess::request_record_level(&state)
.await
.expect("request record level should read");
assert_eq!(level, UsageRequestRecordLevel::Basic);
}
#[tokio::test]
async fn usage_runtime_access_falls_back_to_legacy_request_log_level_alias() {
let state = GatewayDataState::disabled().with_system_config_values_for_tests([(
@@ -467,14 +529,14 @@ mod tests {
}
#[tokio::test]
async fn usage_runtime_access_defaults_missing_request_record_level_to_full() {
async fn usage_runtime_access_defaults_missing_request_record_level_to_basic() {
let state = GatewayDataState::disabled();
let level = UsageRuntimeAccess::request_record_level(&state)
.await
.expect("missing request record level should fall back");
assert_eq!(level, UsageRequestRecordLevel::Full);
assert_eq!(level, UsageRequestRecordLevel::Basic);
}
#[tokio::test]
@@ -488,6 +550,20 @@ mod tests {
.await
.expect("body capture policy should read");
assert_eq!(policy.record_level, UsageRequestRecordLevel::Full);
assert_eq!(policy.record_level, UsageRequestRecordLevel::Basic);
}
#[tokio::test]
async fn usage_runtime_access_fails_closed_for_unknown_record_level() {
let state = GatewayDataState::disabled().with_system_config_values_for_tests([(
"request_record_level".to_string(),
json!("everything"),
)]);
let level = UsageRuntimeAccess::request_record_level(&state)
.await
.expect("request record level should read");
assert_eq!(level, UsageRequestRecordLevel::Basic);
}
}