|
| 1 | +# Multiple policies per VM |
| 2 | + |
| 3 | +Status: Proposed (draft PR, design doc only). |
| 4 | +Tracking: ROADMAP.md → Medium term ("Multiple policies"). |
| 5 | + |
| 6 | +## Motivation |
| 7 | + |
| 8 | +Today the plugin configuration is a single `module`, evaluated against |
| 9 | +a hard-coded target rule named `"allow"`. That's enough for one |
| 10 | +filter doing one job. Real deployments often want: |
| 11 | + |
| 12 | +- Separate authn and authz rules in distinct, independently authored |
| 13 | + modules. |
| 14 | +- An audit policy whose decision is "log this" rather than "block |
| 15 | + this", evaluated alongside the allow policy. |
| 16 | +- Per-route policy bundles where the dispatch happens by package |
| 17 | + name, mirroring how OPA addresses rules as `data.<package>.<rule>`. |
| 18 | + |
| 19 | +Stuffing all of those into one module loses the boundaries that make |
| 20 | +the policies maintainable. |
| 21 | + |
| 22 | +## Goals |
| 23 | + |
| 24 | +1. Plugin config accepts either a single module (current shape, still |
| 25 | + supported) or a list of modules: |
| 26 | + |
| 27 | + ```json |
| 28 | + { "modules": [ |
| 29 | + { "package": "authz", "type": "module", "rules": [ ... ] }, |
| 30 | + { "package": "audit", "type": "module", "rules": [ ... ] } |
| 31 | + ]} |
| 32 | + ``` |
| 33 | + |
| 34 | +1. Each `module` gains an optional `package` string. Defaults to `""` |
| 35 | + (the implicit single-module case). |
| 36 | +1. The proxy-wasm shim grows a new plugin config field `targets[]` to |
| 37 | + pick which `package.rule` pairs are evaluated: |
| 38 | + |
| 39 | + ```yaml |
| 40 | + targets: |
| 41 | + - package: authz |
| 42 | + rule: allow |
| 43 | + on_deny: send_local_response_403 |
| 44 | + - package: audit |
| 45 | + rule: log |
| 46 | + on_deny: noop # decision is recorded out-of-band, not enforced |
| 47 | + ``` |
| 48 | +
|
| 49 | +1. Evaluator gains a fully qualified rule lookup: `evalModule(modules, |
| 50 | + "authz.allow", input)` instead of just rule-by-name within a single |
| 51 | + module. |
| 52 | + |
| 53 | +## Non-goals |
| 54 | + |
| 55 | +- Cross-module data references (`data.audit.events` from inside an |
| 56 | + authz rule). Modules stay independent. If you need shared data, put |
| 57 | + it in `input`. |
| 58 | +- Hot-reload of individual modules. Replace the full plugin config or |
| 59 | + reconfigure the filter; no per-module surgery. |
| 60 | +- A package import system. There's no `import data.helpers` because |
| 61 | + there are no shared helpers. |
| 62 | + |
| 63 | +## Design sketch |
| 64 | + |
| 65 | +### AST builder |
| 66 | + |
| 67 | +`src/ast.zig` gains a `Modules` (plural) container. The existing |
| 68 | +`Module` keeps its `rules` field plus an optional `package` slice. |
| 69 | + |
| 70 | +### Evaluator |
| 71 | + |
| 72 | +`evalModules(target_pkg, target_rule, input)`: |
| 73 | + |
| 74 | +1. Find all modules whose `package` equals `target_pkg`. |
| 75 | +1. Within those, OR-combine all rules whose name equals `target_rule`, |
| 76 | + reusing the existing `evalModule` body. |
| 77 | +1. Apply the same `default` handling. |
| 78 | + |
| 79 | +### Proxy-wasm shim |
| 80 | + |
| 81 | +The `proxy_on_request_headers` callback iterates `targets[]`. For |
| 82 | +each: |
| 83 | + |
| 84 | +- Evaluate `package.rule`. |
| 85 | +- If decision is deny and `on_deny` is `send_local_response_403`, |
| 86 | + short-circuit immediately. |
| 87 | +- If decision is recorded but not enforced (`on_deny: noop`), call |
| 88 | + `proxy_log` with a structured line, continue. |
| 89 | + |
| 90 | +This makes "authz blocks, audit logs" a config decision, not a code |
| 91 | +decision. |
| 92 | + |
| 93 | +### Config compatibility |
| 94 | + |
| 95 | +A bare module (current shape, no `modules` wrapper) is wrapped into a |
| 96 | +synthetic single-element list with `package: ""` and a synthetic |
| 97 | +`targets[]` of `[{ package: "", rule: "allow", on_deny: send_403 }]`. |
| 98 | +Existing configs keep working unchanged. |
| 99 | + |
| 100 | +## API impact |
| 101 | + |
| 102 | +- New plugin config shape `modules[]` + `targets[]` (additive). |
| 103 | +- New AST optional field `package` on `module` (additive). |
| 104 | +- Existing single-module configs still work. |
| 105 | + |
| 106 | +## Test plan |
| 107 | + |
| 108 | +- Unit tests for `evalModules` covering: same package different rules, |
| 109 | + same rule different packages, missing package (returns deny by |
| 110 | + default). |
| 111 | +- Integration test: two modules (`authz`, `audit`) with the audit |
| 112 | + policy logging-only. |
| 113 | +- Envoy example: extend `examples/envoy/envoy.yaml` to demonstrate |
| 114 | + the audit-without-enforce target. |
| 115 | + |
| 116 | +## Open questions |
| 117 | + |
| 118 | +- Naming: `targets` vs `evaluations` vs `gates`? `targets` reads as |
| 119 | + "what to look for", which matches the existing single-target |
| 120 | + vocabulary. |
| 121 | +- Should `on_deny: noop` be allowed without a matching `proxy_log` |
| 122 | + call, i.e., is silent "informational evaluation" useful? A flag-day |
| 123 | + decision; safer default is "log on every audit decision". |
| 124 | +- Order of evaluation within `targets[]`: declaration order, or |
| 125 | + topologically (audit always before allow)? Declaration order is |
| 126 | + simpler and documentable. |
0 commit comments