Skip to content

Commit caa75de

Browse files
committed
Close runtime admission and cache isolation gaps
1 parent 7f0f897 commit caa75de

27 files changed

Lines changed: 883 additions & 274 deletions

CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,26 @@ behavior when the change improves security or project direction.
4646
- Enforce strict host routing in native HTTP/1 and HTTP/2, returning `400` for
4747
missing/invalid identity and `421` for unknown hosts.
4848
- Acquire bounded Wasm admission before blocking-work submission, honor
49-
`queue_limit`, and replace per-invocation watchdog threads with one epoch
49+
`queue_limit`, and replace per-invocation watchdog threads with one
5050
process-wide epoch ticker.
51+
- Replace custom Wasm waiter notification with Tokio semaphores, acquire narrow
52+
policy permits before global capacity, and cap active/queued budgets at 256.
5153
- Add bounded pre-submission external-auth admission and preserve valid
5254
operator access during global invalid-credential throttling.
55+
- Add a process-wide 256-request external-auth ceiling across route-specific
56+
service limits.
5357
- Bound persistent cache index/metadata parsing and keep decoded local cache
5458
encryption keys in `sanitization::SecretBytes<32>`.
59+
- Reject duplicate canonical storage-bin roots, verify persisted cache object
60+
keys before serving, and record strict Host-routing rejections in metrics.
5561
- Pin publishing actions, security-tool installs, and container base images to
5662
immutable reviewed versions and digests.
5763

5864
### Fixed
5965

6066
- Replace storage-bin request-path full-index rewrites and map-scanning LRU
61-
selection with a coalescing persistence worker and ordered eviction index.
67+
selection with one process-wide coalescing persistence worker and ordered
68+
eviction index.
6269

6370
## 1.7.6 - 2026-07-09
6471

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Fluxheim is licensed under the European Union Public Licence 1.2.
7373
| --- | --- | --- |
7474
| Proxy cache || Vhost and route-scoped cache policies. |
7575
| Memory cache || Bounded in-memory cache tier. |
76-
| Disk cache || Filesystem and storage-bin disk backends with ordered eviction and coalesced durable index writes. |
76+
| Disk cache || Filesystem and storage-bin disk backends with ordered eviction, one process-wide persistence worker, unique-root enforcement, and coalesced durable index writes. |
7777
| Tiered cache || Memory plus disk storage plans. |
7878
| Encrypted disk cache || Optional local-key and OpenBao transit encryption paths. |
7979
| Static-file cache || Optional local static-file caching. |
@@ -107,7 +107,7 @@ Fluxheim is licensed under the European Union Public Licence 1.2.
107107
| HTTP/2 origins || Upstream HTTP version controls and bounded HTTP/2 settings. |
108108
| gRPC pass-through || Route-scoped HTTP/2 gRPC policy; no transcoding. |
109109
| WebSocket / HTTP upgrade || `1.4.1`; explicit `proxy.websocket = true` on HTTP/1.1 upstream routes. |
110-
| External auth subrequests || `1.4.1`; `[proxy.auth_request]` with bounded header/body forwarding and pre-submission in-flight admission. |
110+
| External auth subrequests || `1.4.1`; `[proxy.auth_request]` with bounded header/body forwarding plus per-service and process-wide pre-submission admission. |
111111
| Traffic mirroring || `1.4.1`; `traffic-mirror` feature with safe bodyless shadow requests. |
112112
| TCP stream proxying || Optional `stream-proxy` feature with Fluxheim-owned L4 TCP listener/data-path and upstream TLS connector boundaries, source IP/CIDR allow/deny policy, hostname-upstream DNS-rebinding guards, weighted upstream selection, drain/backup policy, bounded idle/lifetime/byte/connect controls, route-local PROXY protocol receive/send, and stream upstream TLS/mTLS controls. |
113113
| UDP/GSLB beta boundary | Limited | `1.5.16`; separate `[udp]` config namespace and `udp-proxy` feature gate with beta DNS-style request/response forwarding, bounded response waits, oversized-response drops, drop-log rate limiting, and syslog one-way forwarding. Public DNS reflector hardening, QUIC pass-through, game proxying, production UDP support, and generic UDP/GSLB platform behavior are not included yet. |

crates/fluxheim-config/src/config_proxy_auth.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::config_http::valid_http_endpoint_url;
1010

1111
const MAX_AUTH_REQUEST_HEADERS: usize = 32;
1212
const MAX_AUTH_REQUEST_RESPONSE_BYTES: u64 = 1024 * 1024;
13-
const MAX_AUTH_REQUEST_IN_FLIGHT: usize = 100_000;
13+
const MAX_AUTH_REQUEST_IN_FLIGHT: usize = 256;
1414

1515
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
1616
#[serde(deny_unknown_fields)]
@@ -154,7 +154,7 @@ impl AuthRequestConfig {
154154
if self.max_in_flight == 0 || self.max_in_flight > MAX_AUTH_REQUEST_IN_FLIGHT {
155155
return Err(ConfigError::InvalidProxyUpstreamPolicy {
156156
field: scope,
157-
reason: "max_in_flight must be between 1 and 100000",
157+
reason: "max_in_flight must be between 1 and 256",
158158
});
159159
}
160160
Ok(())

crates/fluxheim-config/src/config_tests_proxy_transport.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,27 @@ fn rejects_invalid_proxy_upstream_policy() {
7070
saturated_auth_request.validate(),
7171
Err(ConfigError::InvalidProxyUpstreamPolicy {
7272
field: "proxy.auth_request",
73-
reason: "max_in_flight must be between 1 and 100000",
73+
reason: "max_in_flight must be between 1 and 256",
74+
})
75+
);
76+
77+
let oversized_auth_request: Config = toml::from_str(
78+
r#"
79+
[proxy]
80+
upstream = "127.0.0.1:3001"
81+
82+
[proxy.auth_request]
83+
enabled = true
84+
url = "http://127.0.0.1:4180/auth"
85+
max_in_flight = 257
86+
"#,
87+
)
88+
.unwrap();
89+
assert_eq!(
90+
oversized_auth_request.validate(),
91+
Err(ConfigError::InvalidProxyUpstreamPolicy {
92+
field: "proxy.auth_request",
93+
reason: "max_in_flight must be between 1 and 256",
7494
})
7595
);
7696

crates/fluxheim-config/src/config_tests_wasm.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,73 @@ fn wasm_registry_rejects_invalid_total_admission_budget() {
593593
));
594594
}
595595

596+
#[cfg(feature = "wasm")]
597+
#[test]
598+
fn wasm_registry_rejects_execution_budgets_above_runtime_ceiling() {
599+
let config: Config = toml::from_str(
600+
r#"
601+
[server]
602+
listen = ["127.0.0.1:8080"]
603+
default_vhost = "app"
604+
605+
[wasm]
606+
enabled = true
607+
plugin_roots = ["/srv/fluxheim/plugins"]
608+
max_total_concurrent_executions = 257
609+
610+
[[wasm.plugins]]
611+
name = "headers"
612+
path = "/srv/fluxheim/plugins/headers.wasm"
613+
phases = ["request-headers"]
614+
615+
[[vhosts]]
616+
name = "app"
617+
hosts = ["app.test"]
618+
"#,
619+
)
620+
.unwrap();
621+
622+
assert!(matches!(
623+
config.validate(),
624+
Err(ConfigError::InvalidWasmPolicy {
625+
field: "max_total_concurrent_executions",
626+
..
627+
})
628+
));
629+
630+
let queued: Config = toml::from_str(
631+
r#"
632+
[server]
633+
listen = ["127.0.0.1:8080"]
634+
default_vhost = "app"
635+
636+
[wasm]
637+
enabled = true
638+
plugin_roots = ["/srv/fluxheim/plugins"]
639+
640+
[wasm.default_admission]
641+
queue_limit = 257
642+
643+
[[wasm.plugins]]
644+
name = "headers"
645+
path = "/srv/fluxheim/plugins/headers.wasm"
646+
phases = ["request-headers"]
647+
648+
[[vhosts]]
649+
name = "app"
650+
hosts = ["app.test"]
651+
"#,
652+
)
653+
.unwrap();
654+
assert!(matches!(
655+
queued.validate(),
656+
Err(ConfigError::InvalidWasmPolicy {
657+
field: "queue_limit",
658+
..
659+
})
660+
));
661+
}
662+
596663
#[cfg(feature = "wasm")]
597664
#[test]
598665
fn wasm_registry_rejects_invalid_total_cache_admission_budget() {

crates/fluxheim-config/src/config_wasm.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ pub const MAX_WASM_PLUGINS: usize = 128;
1212
pub const MAX_WASM_ATTACHMENTS: usize = 64;
1313
pub const MAX_WASM_PLUGIN_NAME_BYTES: usize = 64;
1414
pub const MAX_WASM_PLUGIN_PHASES: usize = 16;
15-
pub const MAX_WASM_MAX_CONCURRENT_EXECUTIONS: u32 = 100_000;
16-
pub const MAX_WASM_QUEUE_LIMIT: u32 = 100_000;
15+
pub const MAX_WASM_MAX_CONCURRENT_EXECUTIONS: u32 = 256;
16+
pub const MAX_WASM_QUEUE_LIMIT: u32 = 256;
1717
pub const MAX_WASM_ATTACHMENT_PRIORITY: u32 = 1_000_000;
1818

1919
const DEFAULT_WASM_MAX_MODULE_BYTES: u64 = 1_048_576;

crates/fluxheim-server/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,10 @@ mod native_http1_client_proxy_protocol_tests;
272272
#[path = "native_http1_host_router_tests.rs"]
273273
mod native_http1_host_router_tests;
274274

275+
#[cfg(test)]
276+
#[path = "native_http1_host_router_cache_tests.rs"]
277+
mod native_http1_host_router_cache_tests;
278+
275279
#[cfg(test)]
276280
#[path = "native_http1_proxy_tests.rs"]
277281
mod native_http1_proxy_tests;

crates/fluxheim-server/src/native_http1_cache.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ mod native_http1_cache_state;
3131
mod native_http1_cache_storage_bin;
3232

3333
pub(crate) use native_http1_cache_backend::NativeDiskCacheStoreKey;
34+
pub(crate) use native_http1_cache_backend::prepare_native_storage_bin_layout;
3435
use native_http1_cache_backend::{
3536
NativeDiskCacheBackend, NativeDiskCacheLocation, NativeDiskCacheRecord, NativeDiskCacheState,
3637
};
@@ -61,6 +62,7 @@ pub use native_http1_cache_purge::{
6162
};
6263
use native_http1_cache_state::native_disk_cache_mutation_locks;
6364
use native_http1_cache_storage_bin::NativeStorageBinIndexFlush;
65+
pub(crate) use native_http1_cache_storage_bin::ensure_native_storage_bin_index_service;
6466

6567
#[derive(Clone, Debug, Eq, PartialEq)]
6668
pub struct NativeDiskCacheObjectMetadata {
@@ -151,7 +153,11 @@ impl NativeDiskCache {
151153
cache.root.display()
152154
);
153155
}
154-
cache.index_flush = NativeStorageBinIndexFlush::start(&cache.backend, &cache.state);
156+
cache.index_flush = NativeStorageBinIndexFlush::start(&cache.backend, &cache.state)
157+
.inspect_err(|error| {
158+
log::error!(target: "fluxheim::native_http1", "cache index service: {error}");
159+
})
160+
.ok()?;
155161
cache.persist_storage_bin_index();
156162
Some(cache)
157163
}
@@ -366,6 +372,14 @@ impl NativeDiskCache {
366372
return None;
367373
}
368374
};
375+
if object.combined_key.as_deref() != Some(combined_key) {
376+
log::error!(
377+
target: "fluxheim::security",
378+
"native disk cache object identity mismatch; discarding requested cache record"
379+
);
380+
self.remove_combined(combined_key);
381+
return None;
382+
}
369383
let entry = match native_memory_entry_from_disk_object(&object) {
370384
Some(entry) => entry,
371385
None => {

crates/fluxheim-server/src/native_http1_cache_backend.rs

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -115,29 +115,8 @@ impl NativeDiskCacheBackend {
115115
Ok((root, Self::Filesystem))
116116
}
117117
CacheDiskBackend::StorageBin => {
118-
let plan = DiskTierPlan {
119-
backend: CacheDiskBackend::StorageBin,
120-
path: path.clone(),
121-
max_size_bytes: config.disk.max_size_bytes,
122-
max_object_bytes: config.max_object_bytes,
123-
cache_tag_headers: Vec::new(),
124-
storage_bin: config.disk.storage_bin.clone(),
125-
encryption: config.disk.encryption.clone(),
126-
};
127-
let mut layout = StorageBinLayoutPlan::from_disk_plan(&plan).ok_or_else(|| {
128-
std::io::Error::new(
129-
std::io::ErrorKind::InvalidInput,
130-
"native storage-bin cache requires a storage-bin disk plan",
131-
)
132-
})?;
133-
prepare_storage_bin_layout(&layout)?;
134-
let root = layout.root.canonicalize()?;
135-
layout = StorageBinLayoutPlan {
136-
root: root.clone(),
137-
manifest_path: root.join(STORAGE_BIN_MANIFEST_FILENAME),
138-
data_dir: root.join(STORAGE_BIN_DATA_DIR),
139-
..layout
140-
};
118+
let layout = prepare_native_storage_bin_layout(config)?;
119+
let root = layout.root.clone();
141120
let free_map = StorageBinFreeMap::new(&layout);
142121
let files = StorageBinFileSet::new(layout.clone());
143122
Ok((
@@ -152,3 +131,38 @@ impl NativeDiskCacheBackend {
152131
}
153132
}
154133
}
134+
135+
pub(crate) fn prepare_native_storage_bin_layout(
136+
config: &CacheConfig,
137+
) -> std::io::Result<StorageBinLayoutPlan> {
138+
let path = config.disk.path.as_ref().ok_or_else(|| {
139+
std::io::Error::new(
140+
std::io::ErrorKind::InvalidInput,
141+
"native storage-bin cache requires cache.disk.path",
142+
)
143+
})?;
144+
let plan = DiskTierPlan {
145+
backend: CacheDiskBackend::StorageBin,
146+
path: path.clone(),
147+
max_size_bytes: config.disk.max_size_bytes,
148+
max_object_bytes: config.max_object_bytes,
149+
cache_tag_headers: Vec::new(),
150+
storage_bin: config.disk.storage_bin.clone(),
151+
encryption: config.disk.encryption.clone(),
152+
};
153+
let mut layout = StorageBinLayoutPlan::from_disk_plan(&plan).ok_or_else(|| {
154+
std::io::Error::new(
155+
std::io::ErrorKind::InvalidInput,
156+
"native storage-bin cache requires a storage-bin disk plan",
157+
)
158+
})?;
159+
prepare_storage_bin_layout(&layout)?;
160+
let root = layout.root.canonicalize()?;
161+
layout = StorageBinLayoutPlan {
162+
root: root.clone(),
163+
manifest_path: root.join(STORAGE_BIN_MANIFEST_FILENAME),
164+
data_dir: root.join(STORAGE_BIN_DATA_DIR),
165+
..layout
166+
};
167+
Ok(layout)
168+
}

0 commit comments

Comments
 (0)