Result Matchers
Builds a deny-by-default JSON-RPC allowlist out of branch result matchers
Versions marked “overview” do not contain this page. Selecting one opens that version’s documentation overview.
Category: Practical
Task: Builds a deny-by-default JSON-RPC allowlist out of branch result matchers
Prerequisites: The product runtime and any backend services referenced by this configuration.
This configuration comes from the selected release. The example has not been run here; external services are not bundled.
Download the source file.
# Branch Result Matchers (contains, not, any_of)
#
# Builds a deny-by-default JSON-RPC allowlist out of
# branch result matchers. The json_rpc filter records
# each call's kind and method as filter results, and four
# branches on it run in order:
#
# 1. `not: request` on kind turns away anything that
# isn't a JSON-RPC request with a 400. `not` also
# fires when the filter recorded no kind at all, so
# a request with no JSON-RPC body is turned away too.
# 2. `contains: health` on method tags health checks
# (health, health_check) with X-Health-Check, then
# keeps going (rejoin: next).
# 3. `any_of` on method lets the listed methods through
# by skipping ahead to the router.
# 4. A branch with no condition answers everything that
# got this far with a 403.
#
# The allowlist plus the unconditional deny is what makes
# this fail closed. A deny branch that matches bad names
# (say `contains: admin`) only fires on a method it can
# see, and json_rpc records no method for a name with
# control characters in it. The unconditional deny needs
# no value to fire: an unlisted method and a method
# json_rpc couldn't record both end up there.
#
# Usage:
# cargo run -p praxis-proxy -- -c examples/configs/branching/result-matchers.yaml
#
# Exercise:
# # Health check: tagged with X-Health-Check and forwarded
# curl -X POST http://localhost:8080/ \
# -H "Content-Type: application/json" \
# -d '{"jsonrpc":"2.0","method":"health","id":1}'
#
# # Unlisted method: 403
# curl -X POST http://localhost:8080/ \
# -H "Content-Type: application/json" \
# -d '{"jsonrpc":"2.0","method":"admin_reset","id":2}'
#
# # Method json_rpc can't record (control character): 403
# curl -X POST http://localhost:8080/ \
# -H "Content-Type: application/json" \
# -d '{"jsonrpc":"2.0","method":"admin\u0001reset","id":3}'
#
# # Notification (no id), or no JSON-RPC body: 400
# curl -X POST http://localhost:8080/ \
# -H "Content-Type: application/json" \
# -d '{"jsonrpc":"2.0","method":"ping"}'
# curl http://localhost:8080/
listeners:
- name: web
address: "127.0.0.1:8080"
filter_chains: [main]
filter_chains:
- name: main
filters:
# Records `kind` (request, notification, ...) and
# `method` as filter results for the branches below
- filter: json_rpc
max_body_bytes: 65536 # 64 KiB
branch_chains:
# Anything but a request: notifications, responses,
# and bodies that aren't JSON-RPC (no kind at all)
- name: reject_non_requests
on_result:
filter: json_rpc
key: kind
result:
not: request
rejoin: terminal
chains:
- name: not_a_request
filters:
- filter: static_response
status: 400
body: "expected a JSON-RPC request"
# Health checks, tagged so the backend can answer
# them cheaply; rejoin: next moves on to the
# allowlist, which still has to let them through
- name: tag_health_checks
on_result:
filter: json_rpc
key: method
result:
contains: health
rejoin: next
chains:
- name: health_tag
filters:
- filter: headers
request_add:
- name: X-Health-Check
value: "true"
# The allowlist: a listed method skips ahead to
# the router, past the deny below. The rejoin does
# all the work, so the chain has nothing to run
- name: allow_listed_methods
on_result:
filter: json_rpc
key: method
result:
any_of: [ping, echo, health, health_check]
rejoin: routing
chains:
- name: allowed
filters: []
# No on_result, so it fires for everything that
# got this far: unlisted methods, and methods
# json_rpc couldn't record
- name: deny_everything_else
rejoin: terminal
chains:
- name: method_not_allowed
filters:
- filter: static_response
status: 403
body: "method not allowed"
# Named target for the allowlist's skip-to rejoin
- filter: router
name: routing
routes:
- path_prefix: "/"
cluster: backend
- filter: load_balancer
clusters:
- name: backend
endpoints:
- "127.0.0.1:3000"
insecure_options:
allow_private_endpoints: true # example proxies to local backends