Skip to content

Commit 56311de

Browse files
committed
Add runnable OpenResty-style header policy example
1 parent 4224b1c commit 56311de

11 files changed

Lines changed: 140 additions & 43 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ behavior when the change improves security or project direction.
1616
denial, and fail-closed plugin traps.
1717
- Add a dedicated Wasm policy-example smoke to the human test starter and the
1818
opt-in Wasm release gate.
19+
- Add the checked-in nginx Lua/OpenResty-style bounded request/response header
20+
policy and config example, compiled directly by its real listener tests.
1921

2022
### Security
2123

crates/fluxheim-config/src/config_tests_wasm.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ fn irules_access_policy_example_config_validates() {
4444
assert_eq!(config.validate(), Ok(()));
4545
}
4646

47+
#[cfg(feature = "wasm")]
48+
#[test]
49+
fn openresty_header_policy_example_config_validates() {
50+
let config: Config = toml::from_str(include_str!(
51+
"../../../examples/wasm/openresty-header-policy.toml"
52+
))
53+
.unwrap();
54+
55+
assert_eq!(config.validate(), Ok(()));
56+
}
57+
4758
#[cfg(feature = "wasm")]
4859
fn proxy_preview_wasm_config(plugin_fields: &str) -> Config {
4960
toml::from_str(&format!(

crates/fluxheim-server/src/native_http1_route_proxy_tests/wasm.rs

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -477,8 +477,9 @@ async fn native_wasm_cache_admission_budget_is_fair_across_vhosts() {
477477
}
478478

479479
#[tokio::test]
480-
async fn native_wasm_request_and_response_headers_use_bounded_host_calls() {
481-
let fixture = WasmRouteFixture::new(&[("headers", WasmPluginBody::HeaderPolicy)]);
480+
async fn native_wasm_openresty_header_policy_example_uses_bounded_host_calls() {
481+
let fixture =
482+
WasmRouteFixture::new(&[("headers", WasmPluginBody::OpenRestyHeaderPolicyExample)]);
482483
let upstream = upstream_expect_policy_header("/item").await;
483484
let mut config = fixture
484485
.config_with_attachments(upstream, vec![wasm_attachment_all("headers", "route", 100)]);
@@ -1167,7 +1168,7 @@ async fn native_wasm_cache_store_can_set_bounded_stored_response_header() {
11671168
async fn native_wasm_cross_family_chain_runs_in_order_with_cache_hit_metadata() {
11681169
let fixture = WasmRouteFixture::new(&[
11691170
("access", WasmPluginBody::Decision(1)),
1170-
("headers", WasmPluginBody::HeaderPolicy),
1171+
("headers", WasmPluginBody::OpenRestyHeaderPolicyExample),
11711172
("router", WasmPluginBody::RouteDecision),
11721173
("cache_key", WasmPluginBody::CacheLookupDeviceKey),
11731174
("cache_store", WasmPluginBody::CacheStoreHeader),
@@ -1746,7 +1747,8 @@ async fn native_wasm_cache_store_deny_wins_over_earlier_skip() {
17461747
#[cfg(feature = "php-fpm")]
17471748
#[tokio::test]
17481749
async fn native_wasm_response_headers_apply_to_php_fpm_fallback() {
1749-
let fixture = WasmRouteFixture::new(&[("headers", WasmPluginBody::HeaderPolicy)]);
1750+
let fixture =
1751+
WasmRouteFixture::new(&[("headers", WasmPluginBody::OpenRestyHeaderPolicyExample)]);
17501752
let fpm = fastcgi_responder(
17511753
b"Status: 200 OK\r\nContent-Type: text/plain\r\nX-Powered-By: php\r\n\r\nphp-policy",
17521754
)
@@ -1900,7 +1902,7 @@ impl WasmRouteFixture {
19001902
enum WasmPluginBody {
19011903
Decision(i32),
19021904
IrulesAccessPolicyExample,
1903-
HeaderPolicy,
1905+
OpenRestyHeaderPolicyExample,
19041906
ForbiddenHeader,
19051907
RouteDecision,
19061908
CacheLookup,
@@ -1942,43 +1944,8 @@ impl WasmPluginBody {
19421944
Self::IrulesAccessPolicyExample => {
19431945
include_str!("../../../../examples/wasm/irules-access-policy.wat").to_owned()
19441946
}
1945-
Self::HeaderPolicy => {
1946-
r#"
1947-
(module
1948-
(import "fluxheim_policy_v1" "context" (func $context (param i32 i32) (result i32)))
1949-
(import "fluxheim_policy_v1" "set_request_header" (func $set_request_header (param i32 i32) (result i32)))
1950-
(import "fluxheim_policy_v1" "set_response_header" (func $set_response_header (param i32 i32) (result i32)))
1951-
(import "fluxheim_policy_v1" "remove_response_header" (func $remove_response_header (param i32 i32) (result i32)))
1952-
(func (export "fluxheim_request_headers") (result i32)
1953-
i32.const 1
1954-
i32.const 0
1955-
call $context
1956-
i32.const 3
1957-
i32.eq
1958-
if
1959-
i32.const 1
1960-
i32.const 4
1961-
call $set_request_header
1962-
drop
1963-
else
1964-
i32.const 1
1965-
i32.const 1
1966-
call $set_request_header
1967-
drop
1968-
end
1969-
i32.const 0)
1970-
(func (export "fluxheim_response_headers") (result i32)
1971-
i32.const 2
1972-
i32.const 4
1973-
call $set_response_header
1974-
drop
1975-
i32.const 3
1976-
i32.const 0
1977-
call $remove_response_header
1978-
drop
1979-
i32.const 0))
1980-
"#
1981-
.to_owned()
1947+
Self::OpenRestyHeaderPolicyExample => {
1948+
include_str!("../../../../examples/wasm/openresty-header-policy.wat").to_owned()
19821949
}
19831950
Self::ForbiddenHeader => {
19841951
r#"
@@ -2358,7 +2325,7 @@ fn wasm_plugin(root: &Path, name: &str, body: WasmPluginBody) -> fluxheim_config
23582325

23592326
fn wasm_plugin_phases(body: WasmPluginBody) -> Vec<fluxheim_config::WasmPluginPhase> {
23602327
match body {
2361-
WasmPluginBody::HeaderPolicy => vec![
2328+
WasmPluginBody::OpenRestyHeaderPolicyExample => vec![
23622329
fluxheim_config::WasmPluginPhase::RequestHeaders,
23632330
fluxheim_config::WasmPluginPhase::ResponseHeaders,
23642331
],

docs/wasm-policy-example-parity.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ coverage is in
9292
mutation IDs. The ABI remains symbolic and does not expose raw sensitive
9393
headers or bodies.
9494

95+
The `v1.7.9` operator example is checked in as
96+
`examples/wasm/openresty-header-policy.wat` with a complete config fixture. The
97+
shared policy-example smoke compiles that exact source and proves the origin
98+
receives `x-policy-tier: gold`, the client receives
99+
`x-fluxheim-policy-branch: gold`, upstream `x-powered-by` is removed, and an
100+
unknown header mutation ID fails closed before origin dispatch.
101+
95102
### HAProxy Lua/SPOE-Style Routing And Load-Balancer Policy
96103

97104
Capability target:

examples/wasm/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ module receives no raw headers, body, filesystem, network, admin-token, or TLS
1919
secret capability. Use native route and access configuration for classification
2020
instead of attempting arbitrary parsing inside the plugin.
2121

22+
`openresty-header-policy.wat` and `openresty-header-policy.toml` demonstrate
23+
the nginx Lua/OpenResty-style header-policy mapping. On the configured `/gold/`
24+
route, the module reads only Fluxheim's bounded path-class ID, adds the
25+
allow-listed `x-policy-tier: gold` request header, removes the allow-listed
26+
upstream `x-powered-by` response header, and adds
27+
`x-fluxheim-policy-branch: gold` to the client response.
28+
29+
The guest cannot read raw `Authorization`, `Cookie`, or `Set-Cookie` values and
30+
cannot create arbitrary names or values. Unknown IDs, duplicate/oversized
31+
mutations, traps, timeouts, and admission failures follow the configured fail
32+
mode; security-sensitive examples use `fail-closed`.
33+
2234
`cache-lookup-policy.wat` and `cache-store-policy.wat` demonstrate the bounded
2335
1.7.5 cache-policy ABI:
2436

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Compile openresty-header-policy.wat to the configured .wasm path and replace
2+
# the sha256 placeholder with the compiled module digest before use.
3+
4+
[wasm]
5+
enabled = true
6+
plugin_roots = ["/etc/fluxheim/plugins"]
7+
max_total_concurrent_executions = 256
8+
9+
[[wasm.plugins]]
10+
name = "openresty_header_policy"
11+
path = "/etc/fluxheim/plugins/openresty-header-policy.wasm"
12+
sha256 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
13+
abi = "fluxheim-policy-v1"
14+
host_call_namespace = "fluxheim-policy-v1"
15+
phases = ["request-headers", "response-headers"]
16+
fail_mode = "fail-closed"
17+
18+
[wasm.plugins.limits]
19+
max_module_bytes = "1MiB"
20+
max_memory_bytes = "16MiB"
21+
max_table_elements = 10000
22+
fuel = 5000000
23+
timeout_ms = 50
24+
compile_timeout_ms = 500
25+
26+
[[wasm.attachments]]
27+
plugin = "openresty_header_policy"
28+
vhost = "example"
29+
route = "gold"
30+
priority = 100
31+
phases = ["request-headers", "response-headers"]
32+
33+
[[vhosts]]
34+
name = "example"
35+
hosts = ["example.com"]
36+
37+
[[vhosts.routes]]
38+
name = "gold"
39+
path_prefix = "/gold/"
40+
strip_prefix = "/gold"
41+
42+
[vhosts.routes.proxy]
43+
upstreams = ["127.0.0.1:3000"]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
(module
2+
(import "fluxheim_policy_v1" "context"
3+
(func $context (param i32 i32) (result i32)))
4+
(import "fluxheim_policy_v1" "set_request_header"
5+
(func $set_request_header (param i32 i32) (result i32)))
6+
(import "fluxheim_policy_v1" "set_response_header"
7+
(func $set_response_header (param i32 i32) (result i32)))
8+
(import "fluxheim_policy_v1" "remove_response_header"
9+
(func $remove_response_header (param i32 i32) (result i32)))
10+
11+
(func (export "fluxheim_request_headers") (result i32)
12+
;; Context field 1 is Fluxheim's bounded path class. Class 3 is /gold.
13+
i32.const 1
14+
i32.const 0
15+
call $context
16+
i32.const 3
17+
i32.eq
18+
if
19+
;; Header ID 1 is x-policy-tier; value ID 4 is gold.
20+
i32.const 1
21+
i32.const 4
22+
call $set_request_header
23+
drop
24+
else
25+
;; Value ID 1 is standard.
26+
i32.const 1
27+
i32.const 1
28+
call $set_request_header
29+
drop
30+
end
31+
i32.const 0)
32+
33+
(func (export "fluxheim_response_headers") (result i32)
34+
;; Header ID 2 is x-fluxheim-policy-branch; value ID 4 is gold.
35+
i32.const 2
36+
i32.const 4
37+
call $set_response_header
38+
drop
39+
;; Removable response-header ID 3 is x-powered-by.
40+
i32.const 3
41+
i32.const 0
42+
call $remove_response_header
43+
drop
44+
i32.const 0))

packaging/rpm/fluxheim.spec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ fi
157157
* Sun Jul 12 2026 Fluxheim Maintainers <1921261+eldryoth@users.noreply.github.com> - 1.7.9-1
158158
- Start documented and runnable Wasm migration-example parity work.
159159
- Add the F5 iRules-style route access example and real listener smoke.
160+
- Add the nginx Lua/OpenResty-style bounded header-policy example.
160161
- Harden snapshot opens against pathname races and isolate corruption fixtures
161162
behind safe store primitives.
162163

release-notes/RELEASE_NOTES_1.7.9.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ products.
1515
closed.
1616
- Add `scripts/smoke_wasm_policy_examples.sh` to `scripts/test_starter.py` and
1717
the opt-in Wasm release gate.
18+
- Add a checked-in nginx Lua/OpenResty-style header policy and complete config
19+
fixture. Live coverage proves the allow-listed origin request mutation,
20+
client response mutation, upstream-header removal, and fail-closed rejection
21+
of unknown mutation IDs.
1822

1923
## Security
2024

scripts/smoke_wasm_policy_examples.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,9 @@ cargo test --locked -p fluxheim-server --features wasm \
55
native_wasm_irules_policy_example_allows_public_and_denies_admin
66
cargo test --locked -p fluxheim-server --features wasm \
77
native_wasm_access_decision_fails_closed_on_trap
8+
cargo test --locked -p fluxheim-server --features wasm \
9+
native_wasm_openresty_header_policy_example_uses_bounded_host_calls
10+
cargo test --locked -p fluxheim-server --features wasm \
11+
native_wasm_forbidden_header_mutation_fails_closed
812

913
echo "wasm policy examples smoke: ok"

0 commit comments

Comments
 (0)