{"id":50327659,"url":"https://github.com/achetronic/request-validator","last_synced_at":"2026-05-29T07:32:51.208Z","repository":{"id":358945170,"uuid":"1242954021","full_name":"achetronic/request-validator","owner":"achetronic","description":"External Authz for Envoy to allow or deny requests based on CEL expressions. Body included.","archived":false,"fork":false,"pushed_at":"2026-05-19T17:58:50.000Z","size":94,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-19T21:20:23.351Z","etag":null,"topics":["cel","envoy","ext-authz","istio"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/achetronic.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-18T23:16:47.000Z","updated_at":"2026-05-19T18:10:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/achetronic/request-validator","commit_stats":null,"previous_names":["achetronic/request-validator"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/achetronic/request-validator","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/achetronic%2Frequest-validator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/achetronic%2Frequest-validator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/achetronic%2Frequest-validator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/achetronic%2Frequest-validator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/achetronic","download_url":"https://codeload.github.com/achetronic/request-validator/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/achetronic%2Frequest-validator/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33642312,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cel","envoy","ext-authz","istio"],"created_at":"2026-05-29T07:32:50.235Z","updated_at":"2026-05-29T07:32:51.197Z","avatar_url":"https://github.com/achetronic.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# request-validator\n\nA lightweight Envoy and Istio **ext-authz** service that allows or denies requests using simple YAML rules written in [CEL (Common Expression Language)](https://github.com/google/cel-spec).\n\nThis service is built for complex authorization scenarios that standard Istio `AuthorizationPolicy` cannot handle, such as:\n\n- Inspecting the request body (JSON or YAML).\n- Mixing IP networks (CIDRs) and JWT claims with request payload values.\n- Validating OAuth `redirect_uris`.\n- Blocking specific paths during certain hours.\n- Any other custom validation that goes beyond basic method, path, and header matching.\n\n## Two ways to configure policies\n\nYou can drive the engine in two complementary ways, and you can mix them freely:\n\n1. **Declarative YAML**. A policy file checked into Git, typically mounted into the pod as a `ConfigMap`. This is your source of truth and what code review enforces.\n2. **Admin API at runtime**. A small CRUD HTTP API on a separate port that lets you upsert and delete groups, facts, defaults and logging without redeploying. Useful for emergency blocks, A/B experiments, and any change that needs to happen in seconds.\n\nBoth paths feed the same evaluator. The YAML is the declarative floor; the admin API overlays it per key. There is no separate \"YAML mode\" or \"API mode\": every replica always serves the same effective policy, computed as `MergeFromYAML(yaml, admin_overlay)`.\n\n### How the merge works\n\n| Section    | Behaviour                                                                                      |\n| ---------- | ---------------------------------------------------------------------------------------------- |\n| `groups`   | Keyed by `name`. API entry replaces the YAML entry with the same name; new names are appended. |\n| `facts`    | Same as `groups`: per-name overlay.                                                            |\n| `defaults` | Per-field overlay. Fields present in the API replace YAML; absent fields keep the YAML value.  |\n| `logging`  | Same per-field overlay as `defaults`.                                                          |\n\nDeleting an admin entry (`DELETE /api/v1/groups/foo`) restores the YAML entry of the same name if there was one. So you can ship a guard-rail policy in Git and let operators add tactical rules on top without losing the base.\n\nYou can always inspect what is actually being evaluated with `GET /api/v1/config`, which returns the merged result with a `source: \"yaml\" | \"api\"` flag on each group.\n\n### Hot reload\n\nWhatever changes (the YAML file on disk, the admin overlay in the state store, or both) every replica rebuilds atomically:\n\n| Trigger                                                    | Path                                              |\n| ---------------------------------------------------------- | ------------------------------------------------- |\n| Edit the YAML (`kubectl edit cm request-validator-policy`) | fsnotify picks it up within ~200 ms               |\n| `kill -HUP 1` (fallback for NFS / FUSE)                    | manual reload                                     |\n| Admin API `PUT` / `DELETE`                                 | leader commits, every replica's informer rebuilds |\n\nA reload that fails to parse, validate, compile or fetch URL facts is rejected; the previously installed policy keeps serving traffic. Fail-closed by design.\n\n### Writes go to the leader, reads work anywhere\n\nIn multi-replica deployments the admin API uses a Kubernetes `Lease` (`request-validator-leader`) to elect one writer at a time. A write that lands on a follower is transparently proxied to the current leader over an internal HTTP hop, and the leader's response is streamed back to the client. There is no redirect on the wire: the client cannot tell which replica handled the request, and it does not need any direct access to individual pods. Reads are served locally by every replica from its informer cache, with sub-second convergence after a write.\n\n## Quick Tour\n\nHere is a simple policy that allows POST requests to a Keycloak Dynamic Client Registration endpoint, but only if the request comes from an internal private IP range:\n\n```yaml\ndefaults:\n  action: deny\n\ngroups:\n  - name: dcr-internal\n    action: allow\n    rules:\n      - name: from-internal-cidr\n        match: |\n          request.method == 'POST' \u0026\u0026\n          request.path.startsWith('/realms/mcp/clients-registrations') \u0026\u0026\n          inCIDR(request.remoteIp, ['10.0.0.0/8'])\n```\n\nHow it works:\n\n- **`defaults.action`**: The default action when no rules match. We set this to `deny` to ensure unmatched requests are blocked.\n- **`groups`**: Logical collections of rules that share a verdict (like `action: allow`).\n- **`rules`**: Individual checks. The `match` field contains a CEL expression that must return a boolean. If it evaluates to `true`, the rule matches and the group's action is applied.\n- **`inCIDR`**: A helper function provided by request-validator to easily check IP ranges.\n\nThe rest of this guide covers more advanced features and examples.\n\n## Examples\n\n### 1. Limit Admin Access to Office IP and Working Hours\n\n```yaml\ngroups:\n  - name: admin-business-hours\n    action: allow\n    rules:\n      - name: office-during-the-day\n        match: |\n          request.path.startsWith('/admin') \u0026\u0026\n          inCIDR(request.remoteIp, ['203.0.113.0/24']) \u0026\u0026\n          now().getHours('UTC') \u003e= 7 \u0026\u0026\n          now().getHours('UTC') \u003c 19\n```\n\nThe `now()` function returns the current time. You can use standard CEL accessors like `getHours`, `getDayOfWeek`, or `getMonth` to enforce schedules without external dependencies.\n\n### 2. Require a Header on Webhook Paths\n\n```yaml\ngroups:\n  - name: webhook-needs-signature\n    action: allow\n    rules:\n      - name: signed-with-x-hub-signature\n        match: |\n          request.path.startsWith('/hooks/github') \u0026\u0026\n          has('x-hub-signature-256', request.headers) \u0026\u0026\n          request.header['x-hub-signature-256'].startsWith('sha256=')\n```\n\nThe `has()` helper checks if a header exists and is not empty. You can then access its values directly via `request.header`.\n\n### 3. Block Specific Realms on Public Domains\n\n```yaml\ngroups:\n  - name: keep-master-realm-private\n    action: deny\n    rules:\n      - name: no-master-on-public-hosts\n        match: |\n          request.host in ['auth.example-1.com', 'auth.example-2.com'] \u0026\u0026\n          request.path.startsWith('/realms/master')\n```\n\nDeclaring a group with `action: deny` makes it easy to write and read explicit blocklists.\n\n### 4. Validate JSON Request Bodies\n\nThis is a powerful feature that Istio's built-in policies cannot do. Here, we only allow Keycloak client registrations if all listed `redirect_uris` belong to trusted domains:\n\n```yaml\ngroups:\n  - name: dcr-trusted-redirects\n    action: allow\n    match: |\n      request.method == 'POST' \u0026\u0026\n      request.path.matches('^/realms/mcp/clients-registrations(/.*)?$') \u0026\u0026\n      request.body.jsonOk\n    rules:\n      - name: antigravity\n        match: |\n          request.body.json.redirect_uris.all(u,\n            u.startsWith('https://antigravity.google/'))\n\n      - name: chatgpt\n        match: |\n          request.body.json.redirect_uris.all(u,\n            u.matches('^https://([a-z0-9-]+\\\\.)?openai\\\\.com/.+$'))\n```\n\nThe group-level `match` serves as a pre-filter. If it is a POST to the registration endpoint with valid JSON, we then evaluate the individual rules.\n\n### 5. Multi-factor Checks (Defense in Depth)\n\nIf you want a group to require multiple rules to pass before allowing access, set the group `mode` to `all`:\n\n```yaml\ngroups:\n  - name: admin-defence-in-depth\n    action: allow\n    mode: all\n    match: |\n      request.path.startsWith('/admin')\n    rules:\n      - name: from-internal-network\n        match: inCIDR(request.remoteIp, ['10.0.0.0/8', '192.168.0.0/16'])\n      - name: has-admin-claim\n        match: request.header['x-user-groups'].contains('platform-admins')\n      - name: no-debug-header\n        match: '!has(\"x-debug\", request.headers)'\n```\n\nIn `all` mode, every single rule must evaluate to `true`. If even one fails, the group denies the request.\n\nFor a comprehensive real-world policy, take a look at [`examples/policy.yaml`](examples/policy.yaml).\n\n### 6. Push a Tactical Block via the Admin API\n\nSo far every example has lived in the YAML file. Here is the same shape pushed at runtime through the admin API, useful for an incident response when there is no time for a redeploy:\n\n```bash\nTOKEN=$(kubectl -n request-validator get secret request-validator-admin \\\n  -o jsonpath='{.data.token}' | base64 -d)\n\ncurl -sLf -X PUT \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"priority\": -100,\n    \"action\": \"deny\",\n    \"rules\": [\n      { \"name\": \"block-bad-ip\",\n        \"match\": \"request.remoteIp == \\\"203.0.113.5\\\"\" }\n    ]\n  }' \\\n  \"http://request-validator.request-validator.svc:8081/api/v1/groups/incident-2026-05-19\"\n```\n\nThe body is the exact same shape you would write inside `groups:` in the YAML; the wire format and the on-disk format are intentionally identical. `priority: -100` makes the group win against the YAML allow-lists. You can hit any pod (or the Service in front of them); if the pod that receives the request is not the current leader it proxies the write internally and you still get the leader's response back. Reverting the change is a `DELETE` against the same URL; the YAML floor takes over again without a deploy.\n\n## How Policy Evaluation Works\n\nThe engine evaluates groups sequentially in the order they are defined. The first group that matches and produces a verdict determines the outcome. If no group produces a verdict, the default action (`defaults.action`) is applied.\n\nYou can customize this flow using several properties:\n\n- **`priority`**: Assign an integer (e.g., `priority: -100`) to run a group earlier. Groups with lower priority values run first. This is useful for placing global blocklists before allowlist groups.\n- **`match` (Group level)**: A filter that decides if the group should look at the request. If false, the group is skipped.\n- **`mode`**: Controls how rules inside the group are evaluated:\n  - `firstMatch` (default): The first rule that evaluates to `true` wins.\n  - `all`: All rules must evaluate to `true`.\n- **`action` (Rule level)**: You can override the group action on a specific rule.\n- **`fallthrough`**: By default, if a rule does not match, the engine moves to the next rule. You can set `fallthrough: allow` or `fallthrough: deny` to immediately stop group evaluation with that verdict.\n- **`dryRun`**: Set to `true` to test rules in production. The engine evaluates and logs the decision, but will not block the request.\n\n## Available Request Fields in CEL\n\nEvery CEL expression has access to two top-level objects: `request` and `facts`.\n\n| Field                      | Type                        | Description                                        |\n| :------------------------- | :-------------------------- | :------------------------------------------------- |\n| `request.method`           | `string`                    | HTTP method (GET, POST, etc.)                      |\n| `request.scheme`           | `string`                    | http or https (extracted from X-Forwarded-Proto)   |\n| `request.host`             | `string`                    | Request authority (host without port)              |\n| `request.path`             | `string`                    | URL path                                           |\n| `request.remoteIp`         | `string`                    | Client IP (from X-Forwarded-For or remote address) |\n| `request.headers`          | `map\u003cstring, list\u003cstring\u003e\u003e` | All headers with lowercased keys                   |\n| `request.header`           | `map\u003cstring, string\u003e`       | First value of each header                         |\n| `request.queries`          | `map\u003cstring, list\u003cstring\u003e\u003e` | All query parameters                               |\n| `request.query`            | `map\u003cstring, string\u003e`       | First value of each query parameter                |\n| `request.body.raw`         | `string`                    | Raw request body (up to `defaults.maxBodyBytes`)   |\n| `request.body.size`        | `int`                       | Body size in bytes                                 |\n| `request.body.contentType` | `string`                    | Content-Type header shortcut                       |\n| `request.body.json`        | `dyn`                       | Parsed JSON object, or `{}` if not JSON            |\n| `request.body.jsonOk`      | `bool`                      | True if the body is valid JSON                     |\n| `request.body.yaml`        | `dyn`                       | Parsed YAML object, or `{}` if not YAML            |\n| `request.body.yamlOk`      | `bool`                      | True if the body is valid YAML                     |\n\n_Note: For the body to be available, you must configure Envoy/Istio to forward request bodies (see the Istio configuration section below)._\n\n## Facts: Dynamic and External Data\n\nIf you have dynamic data that changes frequently (such as IP blocks published by third parties or lists of active customers), you can load them dynamically as `facts` instead of hardcoding them into your YAML policy.\n\nYou define facts at the top level of your policy and reference them in CEL using `facts.\u003cname\u003e`.\n\nThere are three sources for facts:\n\n| Method  | Source                                        | CEL Type                      |\n| :------ | :-------------------------------------------- | :---------------------------- |\n| `value` | Defined inline in your YAML policy            | The declared YAML type        |\n| `file`  | Read from a local file path on startup/reload | String (file content)         |\n| `url`   | Fetched periodically over HTTP                | String (latest response body) |\n\nExample configuration:\n\n```yaml\nfacts:\n  - name: internalCidrs\n    method: value\n    value:\n      - 10.0.0.0/8\n      - 192.168.0.0/16\n\n  - name: trustedClients\n    method: file\n    file:\n      path: /etc/policy/lists/trusted-clients.yaml\n\n  - name: chatgptFeed\n    method: url\n    url:\n      address: https://openai.com/chatgpt-actions.json\n      interval: 10m\n      timeout: 15s\n      headers:\n        Authorization: \"Bearer $TOKEN\"\n```\n\nTo use them in your CEL rules:\n\n```cel\ninCIDR(request.remoteIp, facts.internalCidrs)\n```\n\nFor facts loaded as raw strings, you can parse them on the fly in CEL:\n\n```cel\ninCIDR(request.remoteIp, parseJSON(facts.chatgptFeed).prefixes.map(p, p.ipv4Prefix))\n```\n\nThe `parseJSON` and `parseYAML` helpers return `{}` if the input is empty or invalid, ensuring your rules do not crash if a fetch fails. It is usually a good idea to protect your rules by checking if the fact is available first:\n\n```yaml\nmatch: |\n  request.path.startsWith('/api') \u0026\u0026\n  facts.chatgptFeed != null \u0026\u0026 facts.chatgptFeed != \"\"\n```\n\n_Note on reliability: If the initial fetch of a URL fact fails during startup, the policy is rejected. If a subsequent background refresh fails, request-validator will log a warning but continue to use the last successfully fetched data. This prevents temporary network issues from blocking requests._\n\n## Logging\n\n`request-validator` writes structured JSON logs for decisions and internal events. You can configure logging directly in your policy:\n\n```yaml\nlogging:\n  level: info # debug | info | warn | error\n  format: json # json | console\n  logBody: false # include the request body in logs (opt-in)\n  redactReveal: 6 # show only the first N characters of redacted values\n  excludeHeaders: # completely exclude these headers from logs\n    - cookie\n    - set-cookie\n  redactHeaders: # mask these header values\n    - authorization\n    - proxy-authorization\n    - x-api-key\n    - x-auth-token\n  redactQueryParams: # mask these query parameters\n    - access_token\n    - id_token\n    - code\n```\n\nThe CLI flags `--log-level` and `--log-format` can be used to override these settings without updating the YAML policy.\n\nAn example log entry in JSON:\n\n```json\n{\n  \"time\": \"2026-05-19T12:14:59.845Z\",\n  \"level\": \"INFO\",\n  \"msg\": \"request decided\",\n  \"decision\": \"allow\",\n  \"rule\": \"dcr-trusted-redirects/antigravity\",\n  \"reason\": \"matched\",\n  \"dry_run\": false,\n  \"duration_ms\": 0.31,\n  \"request\": {\n    \"method\": \"POST\",\n    \"host\": \"auth.example-1.com\",\n    \"path\": \"/realms/mcp/clients-registrations\",\n    \"query\": \"code=***\u0026debug=1\",\n    \"remote_ip\": \"203.0.113.5\",\n    \"headers\": {\n      \"content-type\": \"application/json\",\n      \"authorization\": \"Bearer*********************************\",\n      \"x-api-key\": \"***\"\n    },\n    \"body\": { \"size\": 48, \"content_type\": \"application/json\" }\n  }\n}\n```\n\nThe `console` format outputs values as plain `key=value` lines, which is highly readable when using `kubectl logs -f` during local development.\n\n## CEL Function Reference\n\nOn top of the standard CEL functions and libraries (`ext.Strings()`, `ext.Encoders()`, `ext.Lists()`, `ext.Sets()`, `ext.Math()`, `ext.Bindings()`), the service registers the following custom helper functions.\n\n### Network Functions\n\n- `inCIDR(ip, cidrs)`: Returns `true` if `ip` is in any of the listed CIDR ranges. Supports both IPv4 and IPv6. Plain IPs are automatically treated as `/32` or `/128`.\n- `ipFamily(ip)`: Returns `\"ipv4\"`, `\"ipv6\"`, or `\"\"` if invalid.\n- `isPrivateIP(ip)`: Returns `true` if the IP is in private ranges (RFC1918, RFC4193, or link-local).\n- `isLoopbackIP(ip)`: Returns `true` if the IP is a loopback address (`127.0.0.0/8` or `::1`).\n- `parseURL(url)`: Parses a URL string and returns a map with `scheme`, `host`, `port`, `path`, `query`, `fragment`, `username`, and `password`.\n\n### Glob Matching\n\n- `glob(string, pattern)`: Evaluates a glob pattern. `*` matches anything except slashes, `**` matches everything recursively, `?` matches a single character, and `[abc]` matches character classes.\n- `globAny(string, patterns)`: Returns `true` if any of the glob patterns match.\n\n### Encoding and Security\n\n- `sha256Hex(string)`: Returns the SHA-256 hash of a string as a lowercase hex string.\n- `parseJWTUnverified(token)`: Parses a JWT token and returns a map containing `{header, payload}`. It **does not** verify the signature. Use this only if signature validation is handled by another gateway layer.\n\n### Time\n\n- `now()`: Returns the current UTC timestamp. You can call CEL accessors like `.getHours()`, `.getDayOfWeek()`, or `.getMonth()` on the result.\n\n### Structured Parsing\n\n- `parseJSON(string)`: Parses JSON. Returns `{}` on empty, null, or invalid input.\n- `parseYAML(string)`: Parses YAML. Returns `{}` on empty, null, or invalid input.\n- `jsonPath(object, expression)`: Evaluates a lightweight JSONPath expression (e.g., `$.a.b[*]`, `$..name`, `$[0]`).\n\n### HTTP Conveniences\n\n- `has(name, map)`: Returns `true` if the key exists in the map and has a non-empty value.\n- `firstOr(map, name, default)`: Returns the first value of a key, or the default value if it is empty or missing.\n\n## Deployment and Operation\n\n### Running Locally\n\nTo run request-validator locally with the included example:\n\n```bash\ngo run ./cmd \\\n  --config examples/policy.yaml \\\n  --log-level debug --log-format console \\\n  --no-kubernetes\n```\n\nThe `--no-kubernetes` flag tells the service to run in standalone, in-memory mode without attempting to contact a Kubernetes cluster.\n\n### Deploying to Kubernetes\n\nAn official OCI image is available at: `ghcr.io/achetronic/request-validator:\u003cversion\u003e`.\n\nA ready-to-go deployment setup is located in [`examples/kubernetes/`](examples/kubernetes/). Pick `manifests/` for plain `kubectl` / kustomize, or `helm/` for the bjw-s app-template chart:\n\n```bash\n# Create the namespace and admin secret\nkubectl create namespace request-validator\nkubectl -n request-validator create secret generic request-validator-admin \\\n  --from-literal=token=$(openssl rand -hex 32)\n\n# Deploy the manifests (plain kubectl + kustomize)\nkubectl apply -k examples/kubernetes/manifests/\n\n# …or with Helm (bjw-s app-template 5.0.1)\nhelm repo add bjw-s https://bjw-s-labs.github.io/helm-charts\nhelm upgrade --install request-validator bjw-s/app-template \\\n  --version 5.0.1 -n request-validator \\\n  -f examples/kubernetes/helm/values.yaml\n```\n\nThis sets up a Deployment with 2 replicas, RBAC permissions, a ConfigMap for the policy, a Service, and a PodDisruptionBudget.\n\n### Ports and Endpoints\n\nThe service exposes two ports:\n\n#### 1. ext-authz port (Default: `8080`)\n\nThis is the port Envoy talks to.\n\n- `/`: The ext-authz validation endpoint.\n- `/healthz`: Liveness probe.\n- `/readyz`: Readiness probe (becomes healthy once a policy is successfully loaded).\n- `/metrics`: Prometheus metrics.\n\n#### 2. admin port (Default: `8081`)\n\nExposes a CRUD API to modify policies at runtime. Disabled if `--admin-token-file` is not provided.\n\n- `GET/PUT/DELETE /api/v1/groups[/{name}]`: Manage policy groups.\n- `GET/PUT/DELETE /api/v1/facts[/{name}]`: Manage facts.\n- `GET/PUT/DELETE /api/v1/defaults`: Override global configuration defaults.\n- `GET/PUT/DELETE /api/v1/logging`: Manage logging rules.\n- `GET /api/v1/config`: View the merged running configuration.\n- `GET /api/v1/cluster`: Details about the leader and cluster state.\n- `GET /api/v1/openapi.json`: OpenAPI 3.1 specification.\n\nAll admin requests must include an `Authorization: Bearer \u003ctoken\u003e` header, using the token defined in the admin token file.\n\n### Hot Reloads and Multi-Replica Mode\n\nThese are covered in detail under [Two Ways to Configure Policies](#two-ways-to-configure-policies):\nfile changes propagate via fsnotify (or `SIGHUP` as fallback), admin\nAPI writes go to the elected leader and fan out to every replica's\ninformer cache, and failed reloads keep the previous policy live.\n\n### Diagnostic Headers\n\nEvery decision returned by request-validator includes headers to help you debug and inspect evaluation results:\n\n- `x-rv-result`: The verdict (`allow` or `deny`).\n- `x-rv-rule`: The rule that matched, formatted as `group/rule` (or `\u003cdefaults\u003e`).\n- `x-rv-reason`: A short, human-readable explanation.\n- `x-rv-dry-run`: `true` if the rule was executed in dry-run mode.\n\n## Integration with Istio\n\nTo integrate the service with Istio, apply these two configurations:\n\n### 1. Register the Extension Provider in MeshConfig\n\nAdd request-validator as an extension provider in your Istio installation settings (usually in the `istio-system` namespace's `istio` ConfigMap):\n\n```yaml\nmeshConfig:\n  extensionProviders:\n    - name: request-validator\n      envoyExtAuthzHttp:\n        service: request-validator.\u003cNAMESPACE\u003e.svc.cluster.local\n        port: 8080\n        failOpen: false\n        timeout: 2s\n        includeRequestBodyInCheck:\n          maxRequestBytes: 1048576\n          allowPartialMessage: false\n        headersToDownstreamOnDeny:\n          [content-type, x-rv-result, x-rv-rule, x-rv-reason, x-rv-dry-run]\n        headersToUpstreamOnAllow:\n          [x-rv-result, x-rv-rule, x-rv-reason, x-rv-dry-run]\n        includeRequestHeadersInCheck:\n          - authorization\n          - content-type\n          - cookie\n          - x-api-key\n          - x-user-groups\n          - x-forwarded-for\n          - x-forwarded-proto\n```\n\n### 2. Configure an AuthorizationPolicy\n\nCreate a CUSTOM policy to route matching traffic to the validator:\n\n```yaml\napiVersion: security.istio.io/v1\nkind: AuthorizationPolicy\nmetadata:\n  name: keycloak-dcr-ext-authz\n  namespace: keycloak\nspec:\n  selector:\n    matchLabels:\n      app.kubernetes.io/name: keycloak\n  action: CUSTOM\n  provider:\n    name: request-validator\n  rules:\n    - to:\n        - operation:\n            hosts: [auth.example-1.com, auth.example-2.com]\n            paths:\n              - /realms/*/clients-registrations\n              - /realms/*/clients-registrations/*\n```\n\nFor advanced use cases, such as passing all request headers without listing them individually, see the EnvoyFilter examples in [`examples/config-for-istio.yaml`](examples/config-for-istio.yaml).\n\n## License\n\nApache-2.0.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fachetronic%2Frequest-validator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fachetronic%2Frequest-validator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fachetronic%2Frequest-validator/lists"}